diff --git a/build.gradle b/build.gradle
index 287e5d588..4bd447e87 100644
--- a/build.gradle
+++ b/build.gradle
@@ -40,6 +40,7 @@ ext {
hamcrestVersion = '3.0'
hibernateValidationVersion = '8.0.2.Final'
jacksonBomVersion = '2.19.1'
+ jackson3Version = '3.0.0-rc5'
jaywayJsonPathVersion = '2.9.0'
junitJupiterVersion = '5.13.2'
kotlinCoroutinesVersion = '1.10.2'
@@ -110,6 +111,7 @@ allprojects {
imports {
mavenBom "com.fasterxml.jackson:jackson-bom:$jacksonBomVersion"
+ mavenBom "tools.jackson:jackson-bom:$jackson3Version"
mavenBom "org.junit:junit-bom:$junitJupiterVersion"
mavenBom "org.springframework:spring-framework-bom:$springVersion"
mavenBom "io.projectreactor:reactor-bom:$reactorVersion"
@@ -306,6 +308,7 @@ project('spring-amqp') {
optionalApi 'org.springframework:spring-messaging'
optionalApi 'org.springframework:spring-oxm'
optionalApi 'org.springframework:spring-context'
+
optionalApi 'com.fasterxml.jackson.core:jackson-core'
optionalApi 'com.fasterxml.jackson.core:jackson-databind'
optionalApi 'com.fasterxml.jackson.core:jackson-annotations'
@@ -315,6 +318,14 @@ project('spring-amqp') {
optionalApi 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
optionalApi 'com.fasterxml.jackson.datatype:jackson-datatype-joda'
optionalApi 'com.fasterxml.jackson.module:jackson-module-kotlin'
+
+ optionalApi 'tools.jackson.core:jackson-databind'
+ optionalApi 'tools.jackson.datatype:jackson-datatype-joda'
+ optionalApi 'tools.jackson.dataformat:jackson-dataformat-xml'
+ optionalApi('tools.jackson.module:jackson-module-kotlin') {
+ exclude group: 'org.jetbrains.kotlin'
+ }
+
// Spring Data projection message binding support
optionalApi 'org.springframework.data:spring-data-commons'
optionalApi "com.jayway.jsonpath:json-path:$jaywayJsonPathVersion"
@@ -389,6 +400,9 @@ project('spring-rabbit') {
testRuntimeOnly 'com.fasterxml.jackson.core:jackson-databind'
testRuntimeOnly 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml'
testRuntimeOnly 'com.fasterxml.jackson.module:jackson-module-kotlin'
+
+ testRuntimeOnly 'tools.jackson.core:jackson-databind'
+ testRuntimeOnly 'tools.jackson.dataformat:jackson-dataformat-xml'
}
}
@@ -407,6 +421,8 @@ project('spring-rabbit-stream') {
testRuntimeOnly 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml'
testRuntimeOnly 'com.fasterxml.jackson.module:jackson-module-kotlin'
+ testRuntimeOnly 'tools.jackson.core:jackson-databind'
+
testImplementation 'org.testcontainers:rabbitmq'
testImplementation 'org.testcontainers:junit-jupiter'
testImplementation 'org.apache.logging.log4j:log4j-slf4j-impl'
@@ -430,6 +446,8 @@ project('spring-rabbitmq-client') {
testRuntimeOnly 'com.fasterxml.jackson.core:jackson-databind'
+ testRuntimeOnly 'tools.jackson.core:jackson-databind'
+
testImplementation 'org.testcontainers:rabbitmq'
testImplementation 'org.testcontainers:junit-jupiter'
testImplementation 'org.apache.logging.log4j:log4j-slf4j-impl'
diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJackson2MessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJackson2MessageConverter.java
index 270ddc9ca..be7e71488 100644
--- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJackson2MessageConverter.java
+++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJackson2MessageConverter.java
@@ -51,7 +51,10 @@
* @author Gary Russell
*
* @since 2.1
+ *
+ * @deprecated since 4.0 in favor of {@link AbstractJacksonMessageConverter} for Jackson 3.
*/
+@Deprecated(forRemoval = true, since = "4.0")
public abstract class AbstractJackson2MessageConverter extends AbstractMessageConverter
implements BeanClassLoaderAware, SmartMessageConverter {
diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJacksonMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJacksonMessageConverter.java
new file mode 100644
index 000000000..c06be5644
--- /dev/null
+++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJacksonMessageConverter.java
@@ -0,0 +1,464 @@
+/*
+ * Copyright 2018-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.amqp.support.converter;
+
+import java.io.IOException;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Type;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Optional;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.jspecify.annotations.Nullable;
+import tools.jackson.core.JacksonException;
+import tools.jackson.databind.JavaType;
+import tools.jackson.databind.ObjectMapper;
+
+import org.springframework.amqp.core.Message;
+import org.springframework.amqp.core.MessageProperties;
+import org.springframework.beans.factory.BeanClassLoaderAware;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.MimeType;
+import org.springframework.util.MimeTypeUtils;
+
+/**
+ * Abstract Jackson 3 message converter.
+ *
+ * @author Artem Bilan
+ *
+ * @since 4.0
+ */
+public abstract class AbstractJacksonMessageConverter extends AbstractMessageConverter
+ implements BeanClassLoaderAware, SmartMessageConverter {
+
+ /**
+ * The charset used when converting {@link String} to/from {@code byte[]}.
+ */
+ public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
+
+ protected final Log log = LogFactory.getLog(getClass());
+
+ protected final ObjectMapper objectMapper;
+
+ /**
+ * The supported content type; only the subtype is checked when decoding, e.g.
+ * */json, */xml. If this contains a charset parameter, when encoding, the
+ * contentType header will not be set, when decoding, the raw bytes are passed to
+ * Jackson which can dynamically determine the encoding; otherwise the contentEncoding
+ * or default charset is used.
+ */
+ private MimeType supportedContentType;
+
+ private @Nullable String supportedCTCharset;
+
+ private @Nullable ClassMapper classMapper = null;
+
+ private Charset defaultCharset = DEFAULT_CHARSET;
+
+ private boolean typeMapperSet;
+
+ private @Nullable ClassLoader classLoader = ClassUtils.getDefaultClassLoader();
+
+ private JacksonJavaTypeMapper javaTypeMapper = new DefaultJacksonJavaTypeMapper();
+
+ private boolean useProjectionForInterfaces;
+
+ private @Nullable JacksonProjectingMessageConverter projectingConverter;
+
+ private boolean charsetIsUtf8 = true;
+
+ private boolean assumeSupportedContentType = true;
+
+ private boolean alwaysConvertToInferredType;
+
+ private boolean nullAsOptionalEmpty;
+
+ /**
+ * Construct with the provided {@link ObjectMapper} instance.
+ * @param objectMapper the {@link ObjectMapper} to use.
+ * @param contentType the supported content type; only the subtype is checked when
+ * decoding, e.g. */json, */xml. If this contains a charset parameter, when
+ * encoding, the contentType header will not be set, when decoding, the raw bytes are
+ * passed to Jackson which can dynamically determine the encoding; otherwise the
+ * contentEncoding or default charset is used.
+ * @param trustedPackages the trusted Java packages for deserialization
+ * @see DefaultJacksonJavaTypeMapper#setTrustedPackages(String...)
+ */
+ protected AbstractJacksonMessageConverter(ObjectMapper objectMapper, MimeType contentType,
+ String... trustedPackages) {
+
+ Assert.notNull(objectMapper, "'objectMapper' must not be null");
+ Assert.notNull(contentType, "'contentType' must not be null");
+ this.objectMapper = objectMapper;
+ this.supportedContentType = contentType;
+ this.supportedCTCharset = this.supportedContentType.getParameter("charset");
+ ((DefaultJacksonJavaTypeMapper) this.javaTypeMapper).setTrustedPackages(trustedPackages);
+ }
+
+ /**
+ * Get the supported content type; only the subtype is checked when decoding, e.g.
+ * */json, */xml. If this contains a charset parameter, when encoding, the
+ * contentType header will not be set, when decoding, the raw bytes are passed to
+ * Jackson which can dynamically determine the encoding; otherwise the contentEncoding
+ * or default charset is used.
+ * @return the supportedContentType
+ */
+ protected MimeType getSupportedContentType() {
+ return this.supportedContentType;
+ }
+
+ /**
+ * Set the supported content type; only the subtype is checked when decoding, e.g.
+ * */json, */xml. If this contains a charset parameter, when encoding, the
+ * contentType header will not be set, when decoding, the raw bytes are passed to
+ * Jackson which can dynamically determine the encoding; otherwise the contentEncoding
+ * or default charset is used.
+ * @param supportedContentType the supportedContentType to set.
+ */
+ public void setSupportedContentType(MimeType supportedContentType) {
+ Assert.notNull(supportedContentType, "'supportedContentType' cannot be null");
+ this.supportedContentType = supportedContentType;
+ this.supportedCTCharset = this.supportedContentType.getParameter("charset");
+ }
+
+ /**
+ * When true, if jackson decodes the body as {@code null} convert to {@link Optional#empty()}
+ * instead of returning the original body. Default false.
+ * @param nullAsOptionalEmpty true to return empty.
+ */
+ public void setNullAsOptionalEmpty(boolean nullAsOptionalEmpty) {
+ this.nullAsOptionalEmpty = nullAsOptionalEmpty;
+ }
+
+ @Nullable
+ public ClassMapper getClassMapper() {
+ return this.classMapper;
+ }
+
+ public void setClassMapper(ClassMapper classMapper) {
+ this.classMapper = classMapper;
+ }
+
+ /**
+ * Specify the default charset to use when converting to or from text-based
+ * Message body content. If not specified, the charset will be "UTF-8".
+ * @param defaultCharset The default charset.
+ */
+ public void setDefaultCharset(@Nullable String defaultCharset) {
+ this.defaultCharset =
+ defaultCharset != null
+ ? Charset.forName(defaultCharset)
+ : DEFAULT_CHARSET;
+ this.charsetIsUtf8 = this.defaultCharset.equals(StandardCharsets.UTF_8);
+ }
+
+ public String getDefaultCharset() {
+ return this.defaultCharset.name();
+ }
+
+ @Override
+ public void setBeanClassLoader(ClassLoader classLoader) {
+ this.classLoader = classLoader;
+ if (!this.typeMapperSet) {
+ ((DefaultJacksonJavaTypeMapper) this.javaTypeMapper).setBeanClassLoader(classLoader);
+ }
+ }
+
+ protected @Nullable ClassLoader getClassLoader() {
+ return this.classLoader;
+ }
+
+ public JacksonJavaTypeMapper getJavaTypeMapper() {
+ return this.javaTypeMapper;
+ }
+
+ /**
+ * Whether an explicit java type mapper has been provided.
+ * @return false if the default type mapper is being used.
+ * @see #setJavaTypeMapper(JacksonJavaTypeMapper)
+ */
+ public boolean isTypeMapperSet() {
+ return this.typeMapperSet;
+ }
+
+ public void setJavaTypeMapper(JacksonJavaTypeMapper javaTypeMapper) {
+ Assert.notNull(javaTypeMapper, "'javaTypeMapper' cannot be null");
+ this.javaTypeMapper = javaTypeMapper;
+ this.typeMapperSet = true;
+ }
+
+ /**
+ * Return the type precedence.
+ * @return the precedence.
+ * @see #setTypePrecedence(JacksonJavaTypeMapper.TypePrecedence)
+ */
+ public JacksonJavaTypeMapper.TypePrecedence getTypePrecedence() {
+ return this.javaTypeMapper.getTypePrecedence();
+ }
+
+ /**
+ * Set the precedence for evaluating type information in message properties.
+ * When using {@code @RabbitListener} at the method level, the framework attempts
+ * to determine the target type for payload conversion from the method signature.
+ * If so, this type is provided in the
+ * {@link MessageProperties#getInferredArgumentType() inferredArgumentType}
+ * message property.
+ *
By default, if the type is concrete (not abstract, not an interface), this will
+ * be used ahead of type information provided in the {@code __TypeId__} and
+ * associated headers provided by the sender.
+ *
If you wish to force the use of the {@code __TypeId__} and associated headers
+ * (such as when the actual type is a subclass of the method argument type),
+ * set the precedence to {@link JacksonJavaTypeMapper.TypePrecedence#TYPE_ID}.
+ * @param typePrecedence the precedence.
+ * @see DefaultJacksonJavaTypeMapper#setTypePrecedence(JacksonJavaTypeMapper.TypePrecedence)
+ */
+ public void setTypePrecedence(JacksonJavaTypeMapper.TypePrecedence typePrecedence) {
+ if (this.typeMapperSet) {
+ throw new IllegalStateException("When providing your own type mapper, you should set the precedence on it");
+ }
+ if (this.javaTypeMapper instanceof DefaultJacksonJavaTypeMapper defaultJacksonJavaTypeMapper) {
+ defaultJacksonJavaTypeMapper.setTypePrecedence(typePrecedence);
+ }
+ else {
+ throw new IllegalStateException("Type precedence is available with the DefaultJackson2JavaTypeMapper");
+ }
+ }
+
+ /**
+ * When false (default), fall back to type id headers if the type (or contents of a container
+ * type) is abstract. Set to true if conversion should always be attempted - perhaps because
+ * a custom deserializer has been configured on the {@link ObjectMapper}. If the attempt fails,
+ * fall back to headers.
+ * @param alwaysAttemptConversion true to attempt.
+ */
+ public void setAlwaysConvertToInferredType(boolean alwaysAttemptConversion) {
+ this.alwaysConvertToInferredType = alwaysAttemptConversion;
+ }
+
+ protected boolean isUseProjectionForInterfaces() {
+ return this.useProjectionForInterfaces;
+ }
+
+ /**
+ * Set to true to use Spring Data projection to create the object if the inferred
+ * parameter type is an interface.
+ * @param useProjectionForInterfaces true to use projection.
+ */
+ public void setUseProjectionForInterfaces(boolean useProjectionForInterfaces) {
+ this.useProjectionForInterfaces = useProjectionForInterfaces;
+ if (useProjectionForInterfaces) {
+ if (!ClassUtils.isPresent("org.springframework.data.projection.ProjectionFactory", this.classLoader)) {
+ throw new IllegalStateException("'spring-data-commons' is required to use Projection Interfaces");
+ }
+ this.projectingConverter = new JacksonProjectingMessageConverter(this.objectMapper);
+ }
+ }
+
+ /**
+ * By default, the supported content type is assumed when there is no contentType
+ * property, or it is set to the default ('application/octet-stream'). Set to 'false'
+ * to revert to the previous behavior of returning an unconverted 'byte[]' when this
+ * condition exists.
+ * @param assumeSupportedContentType set false to not assume the content type is
+ * supported.
+ */
+ public void setAssumeSupportedContentType(boolean assumeSupportedContentType) {
+ this.assumeSupportedContentType = assumeSupportedContentType;
+ }
+
+ @Override
+ public Object fromMessage(Message message) throws MessageConversionException {
+ return fromMessage(message, null);
+ }
+
+ /**
+ * {@inheritDoc}
+ * @param conversionHint The conversionHint must be a {@link ParameterizedTypeReference}.
+ */
+ @Override
+ public Object fromMessage(Message message, @Nullable Object conversionHint) throws MessageConversionException {
+ Object content = null;
+ MessageProperties properties = message.getMessageProperties();
+ String contentType = properties.getContentType();
+ if (this.assumeSupportedContentType && contentType.equals(MessageProperties.DEFAULT_CONTENT_TYPE)
+ || contentType.contains(this.supportedContentType.getSubtype())) {
+
+ String encoding = determineEncoding(properties, contentType);
+ content = doFromMessage(message, conversionHint, properties, encoding);
+ }
+ else {
+ if (this.log.isWarnEnabled()) {
+ this.log.warn("Could not convert incoming message with content-type ["
+ + contentType + "], '" + this.supportedContentType.getSubtype() + "' keyword missing.");
+ }
+ }
+ if (content == null) {
+ if (this.nullAsOptionalEmpty) {
+ content = Optional.empty();
+ }
+ else {
+ content = message.getBody();
+ }
+ }
+ return content;
+ }
+
+ private @Nullable String determineEncoding(MessageProperties properties, @Nullable String contentType) {
+ String encoding = properties.getContentEncoding();
+ if (encoding == null && contentType != null) {
+ try {
+ MimeType mimeType = MimeTypeUtils.parseMimeType(contentType);
+ encoding = mimeType.getParameter("charset");
+ }
+ catch (RuntimeException e) {
+ // Ignore
+ }
+ }
+ return encoding;
+ }
+
+ private Object doFromMessage(Message message, @Nullable Object conversionHint, MessageProperties properties,
+ @Nullable String encoding) {
+
+ try {
+ return convertContent(message, conversionHint, properties, encoding);
+ }
+ catch (IOException | JacksonException ex) {
+ throw new MessageConversionException("Failed to convert Message content", ex);
+ }
+ }
+
+ @SuppressWarnings("NullAway") // Dataflow analysis limitation
+ private Object convertContent(Message message, @Nullable Object conversionHint, MessageProperties properties,
+ @Nullable String encoding) throws IOException {
+
+ Object content = null;
+ JavaType inferredType = this.javaTypeMapper.getInferredType(properties);
+ if (inferredType != null && this.useProjectionForInterfaces && inferredType.isInterface()
+ && !inferredType.getRawClass().getPackage().getName().startsWith("java.util")) { // List etc
+ content = this.projectingConverter.convert(message, inferredType.getRawClass());
+ properties.setProjectionUsed(true);
+ }
+ else if (inferredType != null && this.alwaysConvertToInferredType) {
+ content = tryConvertType(message, encoding, inferredType);
+ }
+ if (content == null) {
+ if (conversionHint instanceof ParameterizedTypeReference> parameterizedTypeReference) {
+ content = convertBytesToObject(message.getBody(), encoding,
+ this.objectMapper.getTypeFactory().constructType(parameterizedTypeReference.getType()));
+ }
+ else if (getClassMapper() == null) {
+ JavaType targetJavaType = getJavaTypeMapper().toJavaType(message.getMessageProperties());
+ content = convertBytesToObject(message.getBody(), encoding, targetJavaType);
+ }
+ else {
+ Class> targetClass = getClassMapper().toClass(message.getMessageProperties());
+ content = convertBytesToObject(message.getBody(), encoding, targetClass);
+ }
+ }
+ return content;
+ }
+
+ /*
+ * Unfortunately, mapper.canDeserialize() always returns true (adds an AbstractDeserializer
+ * to the cache); so all we can do is try a conversion.
+ */
+ private @Nullable Object tryConvertType(Message message, @Nullable String encoding, JavaType inferredType) {
+ try {
+ return convertBytesToObject(message.getBody(), encoding, inferredType);
+ }
+ catch (Exception ex) {
+ this.log.trace("Cannot create possibly abstract container contents; falling back to headers", ex);
+ return null;
+ }
+ }
+
+ private Object convertBytesToObject(byte[] body, @Nullable String encoding, Class> targetClass)
+ throws IOException {
+
+ return convertBytesToObject(body, encoding, this.objectMapper.constructType(targetClass));
+ }
+
+ private Object convertBytesToObject(byte[] body, @Nullable String encoding, JavaType targetJavaType)
+ throws IOException {
+
+ String encodingToUse = encoding;
+
+ if (encodingToUse == null) {
+ if (this.charsetIsUtf8 & this.supportedCTCharset == null) {
+ return this.objectMapper.readValue(body, targetJavaType);
+ }
+ encodingToUse = getDefaultCharset();
+ }
+
+ String contentAsString = new String(body, encodingToUse);
+ return this.objectMapper.readValue(contentAsString, targetJavaType);
+ }
+
+ @Override
+ protected Message createMessage(Object objectToConvert, MessageProperties messageProperties)
+ throws MessageConversionException {
+
+ return createMessage(objectToConvert, messageProperties, null);
+ }
+
+ @Override
+ protected Message createMessage(Object objectToConvert, MessageProperties messageProperties,
+ @Nullable Type genericType) throws MessageConversionException {
+
+ byte[] bytes;
+ try {
+ if (this.charsetIsUtf8 && this.supportedCTCharset == null) {
+ bytes = this.objectMapper.writeValueAsBytes(objectToConvert);
+ }
+ else {
+ String jsonString = this.objectMapper.writeValueAsString(objectToConvert);
+ String encoding = this.supportedCTCharset != null ? this.supportedCTCharset : getDefaultCharset();
+ bytes = jsonString.getBytes(encoding);
+ }
+ }
+ catch (IOException | JacksonException ex) {
+ throw new MessageConversionException("Failed to convert Message content", ex);
+ }
+ messageProperties.setContentType(this.supportedContentType.toString());
+ if (this.supportedCTCharset == null) {
+ messageProperties.setContentEncoding(getDefaultCharset());
+ }
+ messageProperties.setContentLength(bytes.length);
+
+ if (getClassMapper() == null) {
+ JavaType type =
+ this.objectMapper.constructType(genericType == null ? objectToConvert.getClass() : genericType);
+ if (genericType != null && !type.isContainerType()
+ && Modifier.isAbstract(type.getRawClass().getModifiers())) {
+
+ type = this.objectMapper.constructType(objectToConvert.getClass());
+ }
+ getJavaTypeMapper().fromJavaType(type, messageProperties);
+ }
+ else {
+ getClassMapper().fromClass(objectToConvert.getClass(), messageProperties);
+ }
+
+ return new Message(bytes, messageProperties);
+ }
+
+}
diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJavaTypeMapper.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJavaTypeMapper.java
index b4c039903..7fcb715f6 100644
--- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJavaTypeMapper.java
+++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJavaTypeMapper.java
@@ -37,7 +37,10 @@
* @author Gary Russell
* @author Ngoc Nhan
* @author Artem Bilan
+ *
+ * @deprecated since 4.0 in favor of {@link DefaultJacksonJavaTypeMapper} for Jackson 3.
*/
+@Deprecated(forRemoval = true, since = "4.0")
public abstract class AbstractJavaTypeMapper implements BeanClassLoaderAware {
public static final String DEFAULT_CLASSID_FIELD_NAME = "__TypeId__";
diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultJackson2JavaTypeMapper.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultJackson2JavaTypeMapper.java
index 7eccc86bf..d5c6447f4 100644
--- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultJackson2JavaTypeMapper.java
+++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultJackson2JavaTypeMapper.java
@@ -37,7 +37,10 @@
* @author Artem Bilan
* @author Gary Russell
* @author Ngoc Nhan
+ *
+ * @deprecated since 4.0 in favor of {@link DefaultJacksonJavaTypeMapper} for Jackson 3.
*/
+@Deprecated(forRemoval = true, since = "4.0")
public class DefaultJackson2JavaTypeMapper extends AbstractJavaTypeMapper implements Jackson2JavaTypeMapper {
private static final List TRUSTED_PACKAGES =
diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultJacksonJavaTypeMapper.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultJacksonJavaTypeMapper.java
new file mode 100644
index 000000000..7154850af
--- /dev/null
+++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultJacksonJavaTypeMapper.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright 2002-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.amqp.support.converter;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.jspecify.annotations.Nullable;
+import tools.jackson.databind.JavaType;
+import tools.jackson.databind.type.TypeFactory;
+
+import org.springframework.amqp.core.MessageProperties;
+import org.springframework.beans.factory.BeanClassLoaderAware;
+import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
+
+/**
+ * Jackson 3 type mapper.
+ *
+ * @author Artem Bilan
+ *
+ * @since 4.0
+ */
+public class DefaultJacksonJavaTypeMapper implements JacksonJavaTypeMapper, BeanClassLoaderAware {
+
+ private static final List TRUSTED_PACKAGES =
+ Arrays.asList(
+ "java.util",
+ "java.lang"
+ );
+
+ private final Set trustedPackages = new LinkedHashSet<>(TRUSTED_PACKAGES);
+
+ private volatile TypePrecedence typePrecedence = TypePrecedence.INFERRED;
+
+ public static final String DEFAULT_CLASSID_FIELD_NAME = "__TypeId__";
+
+ public static final String DEFAULT_CONTENT_CLASSID_FIELD_NAME = "__ContentTypeId__";
+
+ public static final String DEFAULT_KEY_CLASSID_FIELD_NAME = "__KeyTypeId__";
+
+ private final Map> idClassMapping = new HashMap<>();
+
+ private final Map, String> classIdMapping = new HashMap<>();
+
+ private @Nullable ClassLoader classLoader = ClassUtils.getDefaultClassLoader();
+
+ private TypeFactory typeFactory = TypeFactory.createDefaultInstance();
+
+ public String getClassIdFieldName() {
+ return DEFAULT_CLASSID_FIELD_NAME;
+ }
+
+ public String getContentClassIdFieldName() {
+ return DEFAULT_CONTENT_CLASSID_FIELD_NAME;
+ }
+
+ public String getKeyClassIdFieldName() {
+ return DEFAULT_KEY_CLASSID_FIELD_NAME;
+ }
+
+ public void setIdClassMapping(Map> idClassMapping) {
+ this.idClassMapping.putAll(idClassMapping);
+ createReverseMap();
+ }
+
+ @Override
+ public void setBeanClassLoader(ClassLoader classLoader) {
+ this.classLoader = classLoader;
+ this.typeFactory = this.typeFactory.withClassLoader(classLoader);
+ }
+
+ protected @Nullable ClassLoader getClassLoader() {
+ return this.classLoader;
+ }
+
+ protected void addHeader(MessageProperties properties, String headerName, Class> clazz) {
+ if (this.classIdMapping.containsKey(clazz)) {
+ properties.getHeaders().put(headerName, this.classIdMapping.get(clazz));
+ }
+ else {
+ properties.getHeaders().put(headerName, clazz.getName());
+ }
+ }
+
+ protected String retrieveHeader(MessageProperties properties, String headerName) {
+ String classId = retrieveHeaderAsString(properties, headerName);
+ if (classId == null) {
+ throw new MessageConversionException(
+ "failed to convert Message content. Could not resolve " + headerName + " in header");
+ }
+ return classId;
+ }
+
+ protected @Nullable String retrieveHeaderAsString(MessageProperties properties, String headerName) {
+ Map headers = properties.getHeaders();
+ Object classIdFieldNameValue = headers.get(headerName);
+ return classIdFieldNameValue != null
+ ? classIdFieldNameValue.toString()
+ : null;
+ }
+
+ private void createReverseMap() {
+ this.classIdMapping.clear();
+ for (Map.Entry> entry : this.idClassMapping.entrySet()) {
+ String id = entry.getKey();
+ Class> clazz = entry.getValue();
+ this.classIdMapping.put(clazz, id);
+ }
+ }
+
+ public Map> getIdClassMapping() {
+ return Collections.unmodifiableMap(this.idClassMapping);
+ }
+
+ protected boolean hasInferredTypeHeader(MessageProperties properties) {
+ return properties.getInferredArgumentType() != null;
+ }
+
+ protected JavaType fromInferredTypeHeader(MessageProperties properties) {
+ return this.typeFactory.constructType(properties.getInferredArgumentType());
+ }
+
+ /**
+ * Return the precedence.
+ * @return the precedence.
+ * @see #setTypePrecedence(TypePrecedence)
+ */
+ @Override
+ public TypePrecedence getTypePrecedence() {
+ return this.typePrecedence;
+ }
+
+ /**
+ * Set the precedence for evaluating type information in message properties.
+ * When using {@code @RabbitListener} at the method level, the framework attempts
+ * to determine the target type for payload conversion from the method signature.
+ * If so, this type is provided in the
+ * {@link MessageProperties#getInferredArgumentType() inferredArgumentType}
+ * message property.
+ *
+ * By default, if the type is concrete (not abstract, not an interface), this will
+ * be used ahead of type information provided in the {@code __TypeId__} and
+ * associated headers provided by the sender.
+ *
+ * If you wish to force the use of the {@code __TypeId__} and associated headers
+ * (such as when the actual type is a subclass of the method argument type),
+ * set the precedence to {@link TypePrecedence#TYPE_ID}.
+ * @param typePrecedence the precedence.
+ */
+ public void setTypePrecedence(TypePrecedence typePrecedence) {
+ Assert.notNull(typePrecedence, "'typePrecedence' cannot be null");
+ this.typePrecedence = typePrecedence;
+ }
+
+ /**
+ * Specify a set of packages to trust during deserialization.
+ * The asterisk ({@code *}) means trust all.
+ * @param trustedPackages the trusted Java packages for deserialization
+ */
+ public void setTrustedPackages(String @Nullable ... trustedPackages) {
+ if (trustedPackages != null) {
+ for (String trusted : trustedPackages) {
+ if ("*".equals(trusted)) {
+ this.trustedPackages.clear();
+ break;
+ }
+ else {
+ this.trustedPackages.add(trusted);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void addTrustedPackages(String @Nullable ... packages) {
+ setTrustedPackages(packages);
+ }
+
+ @Override
+ public JavaType toJavaType(MessageProperties properties) {
+ JavaType inferredType = getInferredType(properties);
+ if (inferredType != null && canConvert(inferredType)) {
+ return inferredType;
+ }
+
+ String typeIdHeader = retrieveHeaderAsString(properties, getClassIdFieldName());
+
+ if (typeIdHeader != null) {
+ return fromTypeHeader(properties, typeIdHeader);
+ }
+
+ if (hasInferredTypeHeader(properties)) {
+ return fromInferredTypeHeader(properties);
+ }
+
+ return this.typeFactory.constructType(Object.class);
+ }
+
+ private boolean canConvert(JavaType inferredType) {
+ if (inferredType.isAbstract() && !inferredType.isContainerType()) {
+ return false;
+ }
+ if (inferredType.isContainerType() && inferredType.getContentType().isAbstract()) {
+ return false;
+ }
+ return inferredType.getKeyType() == null || !inferredType.getKeyType().isAbstract();
+ }
+
+ private JavaType fromTypeHeader(MessageProperties properties, String typeIdHeader) {
+ JavaType classType = getClassIdType(typeIdHeader);
+ if (!classType.isContainerType() || classType.isArrayType()) {
+ return classType;
+ }
+
+ JavaType contentClassType = getClassIdType(retrieveHeader(properties, getContentClassIdFieldName()));
+ if (classType.getKeyType() == null) {
+ return this.typeFactory.constructCollectionLikeType(classType.getRawClass(), contentClassType);
+ }
+
+ JavaType keyClassType = getClassIdType(retrieveHeader(properties, getKeyClassIdFieldName()));
+ return this.typeFactory.constructMapLikeType(classType.getRawClass(), keyClassType, contentClassType);
+ }
+
+ @Override
+ public @Nullable JavaType getInferredType(MessageProperties properties) {
+ if (this.typePrecedence.equals(TypePrecedence.INFERRED) && hasInferredTypeHeader(properties)) {
+ return fromInferredTypeHeader(properties);
+ }
+ return null;
+ }
+
+ private JavaType getClassIdType(String classId) {
+ if (getIdClassMapping().containsKey(classId)) {
+ return this.typeFactory.constructType(getIdClassMapping().get(classId));
+ }
+ else {
+ try {
+ if (!isTrustedPackage(classId)) {
+ throw new IllegalArgumentException("The class '" + classId + "' is not in the trusted packages: " +
+ this.trustedPackages + ". " +
+ "If you believe this class is safe to deserialize, please provide its name. " +
+ "If the serialization is only done by a trusted source, you can also enable trust all (*).");
+ }
+ else {
+ return this.typeFactory.constructType(ClassUtils.forName(classId, getClassLoader()));
+ }
+ }
+ catch (ClassNotFoundException e) {
+ throw new MessageConversionException("failed to resolve class name. Class not found [" + classId + "]", e);
+ }
+ catch (LinkageError e) {
+ throw new MessageConversionException("failed to resolve class name. Linkage error [" + classId + "]", e);
+ }
+ }
+ }
+
+ private boolean isTrustedPackage(String requestedType) {
+ if (!this.trustedPackages.isEmpty()) {
+ String packageName = ClassUtils.getPackageName(requestedType).replaceFirst("\\[L", "");
+ for (String trustedPackage : this.trustedPackages) {
+ if (packageName.equals(trustedPackage)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public void fromJavaType(JavaType javaType, MessageProperties properties) {
+ addHeader(properties, getClassIdFieldName(), javaType.getRawClass());
+
+ if (javaType.isContainerType() && !javaType.isArrayType()) {
+ addHeader(properties, getContentClassIdFieldName(), javaType.getContentType().getRawClass());
+ }
+
+ if (javaType.getKeyType() != null) {
+ addHeader(properties, getKeyClassIdFieldName(), javaType.getKeyType().getRawClass());
+ }
+ }
+
+ @Override
+ public void fromClass(Class> clazz, MessageProperties properties) {
+ fromJavaType(this.typeFactory.constructType(clazz), properties);
+
+ }
+
+ @Override
+ public Class> toClass(MessageProperties properties) {
+ return toJavaType(properties).getRawClass();
+ }
+
+}
diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2JavaTypeMapper.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2JavaTypeMapper.java
index ecdb8548c..ac0ef8b6f 100644
--- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2JavaTypeMapper.java
+++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2JavaTypeMapper.java
@@ -30,7 +30,10 @@
* @author Sam Nelson
* @author Andreas Asplund
* @author Gary Russell
+ *
+ * @deprecated since 4.0 in favor of {@link JacksonJavaTypeMapper} for Jackson 3.
*/
+@Deprecated(forRemoval = true, since = "4.0")
public interface Jackson2JavaTypeMapper extends ClassMapper {
/**
diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverter.java
index 530b0b1b2..9e392cb16 100644
--- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverter.java
+++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverter.java
@@ -34,7 +34,10 @@
* @author Artem Bilan
* @author Arlo Louis O'Keeffe
* @author Mohammad Hewedy
+ *
+ * @deprecated since 4.0 in favor of {@link JacksonJsonMessageConverter} for Jackson 3.
*/
+@Deprecated(forRemoval = true, since = "4.0")
public class Jackson2JsonMessageConverter extends AbstractJackson2MessageConverter {
/**
diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2XmlMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2XmlMessageConverter.java
index 2ed48611f..f45051669 100644
--- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2XmlMessageConverter.java
+++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2XmlMessageConverter.java
@@ -28,7 +28,10 @@
* @author Mohammad Hewedy
*
* @since 2.1
+ *
+ * @deprecated since 4.0 in favor of {@link JacksonXmlMessageConverter} for Jackson 3.
*/
+@Deprecated(forRemoval = true, since = "4.0")
public class Jackson2XmlMessageConverter extends AbstractJackson2MessageConverter {
/**
diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/JacksonJavaTypeMapper.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/JacksonJavaTypeMapper.java
new file mode 100644
index 000000000..b6d766ce0
--- /dev/null
+++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/JacksonJavaTypeMapper.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2002-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.amqp.support.converter;
+
+import org.jspecify.annotations.Nullable;
+import tools.jackson.databind.JavaType;
+
+import org.springframework.amqp.core.MessageProperties;
+
+/**
+ * Strategy for setting metadata on messages such that one can create the class that needs
+ * to be instantiated when receiving a message.
+ *
+ * @author Artem Bilan
+ *
+ * @since 4.0
+ */
+public interface JacksonJavaTypeMapper extends ClassMapper {
+
+ /**
+ * The precedence for type conversion - inferred from the method parameter or message
+ * headers. Only applies if both exist.
+ */
+ enum TypePrecedence {
+ INFERRED, TYPE_ID
+ }
+
+ /**
+ * Set the message properties according to the type.
+ * @param javaType the type.
+ * @param properties the properties.
+ */
+ void fromJavaType(JavaType javaType, MessageProperties properties);
+
+ /**
+ * Determine the type from the message properties.
+ * @param properties the properties.
+ * @return the type.
+ */
+ JavaType toJavaType(MessageProperties properties);
+
+ /**
+ * Get the type precedence.
+ * @return the precedence.
+ */
+ TypePrecedence getTypePrecedence();
+
+ /**
+ * Add trusted packages.
+ * @param packages the packages.
+ */
+ default void addTrustedPackages(String... packages) {
+ // no op
+ }
+
+ /**
+ * Return the inferred type, if the type precedence is inferred and the
+ * header is present.
+ * @param properties the message properties.
+ * @return the type.
+ */
+ @Nullable
+ JavaType getInferredType(MessageProperties properties);
+
+}
diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/JacksonJsonMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/JacksonJsonMessageConverter.java
new file mode 100644
index 000000000..57893e996
--- /dev/null
+++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/JacksonJsonMessageConverter.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2002-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.amqp.support.converter;
+
+import tools.jackson.databind.DeserializationFeature;
+import tools.jackson.databind.MapperFeature;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.json.JsonMapper;
+
+import org.springframework.amqp.core.MessageProperties;
+import org.springframework.util.MimeTypeUtils;
+
+/**
+ * JSON converter that uses the Jackson 3.
+ *
+ * @author Artem Bilan
+ *
+ * @since 4.0
+ */
+public class JacksonJsonMessageConverter extends AbstractJacksonMessageConverter {
+
+ /**
+ * Construct with an internal {@link ObjectMapper} instance and trusted packed to all ({@code *}).
+ */
+ public JacksonJsonMessageConverter() {
+ this("*");
+ }
+
+ /**
+ * Construct with an internal {@link ObjectMapper} instance.
+ * The {@link DeserializationFeature#FAIL_ON_UNKNOWN_PROPERTIES}
+ * and {@link MapperFeature#DEFAULT_VIEW_INCLUSION} are set to false on the {@link ObjectMapper}.
+ * @param trustedPackages the trusted Java packages for deserialization
+ * @see DefaultJacksonJavaTypeMapper#setTrustedPackages(String...)
+ */
+ public JacksonJsonMessageConverter(String... trustedPackages) {
+ this(JsonMapper.builder()
+ .findAndAddModules(JacksonJsonMessageConverter.class.getClassLoader())
+ .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
+ .disable(MapperFeature.DEFAULT_VIEW_INCLUSION)
+ .build(),
+ trustedPackages);
+ }
+
+ /**
+ * Construct with the provided {@link ObjectMapper} instance and trusted packed to all ({@code *}).
+ * @param jsonObjectMapper the {@link ObjectMapper} to use.
+ */
+ public JacksonJsonMessageConverter(ObjectMapper jsonObjectMapper) {
+ this(jsonObjectMapper, "*");
+ }
+
+ /**
+ * Construct with the provided {@link ObjectMapper} instance.
+ * @param jsonObjectMapper the {@link ObjectMapper} to use.
+ * @param trustedPackages the trusted Java packages for deserialization
+ * @see DefaultJacksonJavaTypeMapper#setTrustedPackages(String...)
+ */
+ public JacksonJsonMessageConverter(ObjectMapper jsonObjectMapper, String... trustedPackages) {
+ super(jsonObjectMapper, MimeTypeUtils.parseMimeType(MessageProperties.CONTENT_TYPE_JSON), trustedPackages);
+ }
+
+}
diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/JacksonProjectingMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/JacksonProjectingMessageConverter.java
new file mode 100644
index 000000000..142169a00
--- /dev/null
+++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/JacksonProjectingMessageConverter.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2019-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.amqp.support.converter;
+
+import java.io.ByteArrayInputStream;
+import java.lang.reflect.Type;
+
+import com.jayway.jsonpath.Configuration;
+import com.jayway.jsonpath.TypeRef;
+import com.jayway.jsonpath.spi.mapper.MappingException;
+import com.jayway.jsonpath.spi.mapper.MappingProvider;
+import org.jspecify.annotations.Nullable;
+import tools.jackson.databind.JavaType;
+import tools.jackson.databind.ObjectMapper;
+
+import org.springframework.amqp.core.Message;
+import org.springframework.core.ResolvableType;
+import org.springframework.data.projection.MethodInterceptorFactory;
+import org.springframework.data.projection.ProjectionFactory;
+import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
+import org.springframework.data.web.JsonProjectingMethodInterceptorFactory;
+import org.springframework.util.Assert;
+
+/**
+ * Uses a Spring Data {@link ProjectionFactory} to bind incoming messages to projection
+ * interfaces.
+ * Based on Jackson 3.
+ *
+ * @author Artem Bilan
+ *
+ * @since 4.0
+ *
+ */
+public class JacksonProjectingMessageConverter {
+
+ private final ProjectionFactory projectionFactory;
+
+ public JacksonProjectingMessageConverter(ObjectMapper mapper) {
+ Assert.notNull(mapper, "'mapper' cannot be null");
+ MappingProvider provider = new Jackson3MappingProvider(mapper);
+ MethodInterceptorFactory interceptorFactory = new JsonProjectingMethodInterceptorFactory(provider);
+
+ SpelAwareProxyProjectionFactory factory = new SpelAwareProxyProjectionFactory();
+ factory.registerMethodInvokerFactory(interceptorFactory);
+
+ this.projectionFactory = factory;
+ }
+
+ public Object convert(Message message, Type type) {
+ return this.projectionFactory.createProjection(ResolvableType.forType(type).resolve(Object.class),
+ new ByteArrayInputStream(message.getBody()));
+ }
+
+ /**
+ * A {@link MappingProvider} implementation for Jackson 3.
+ * Until respective implementation is there in json-path library.
+ * @param objectMapper Jackson 3 {@link ObjectMapper}
+ */
+ private record Jackson3MappingProvider(ObjectMapper objectMapper) implements MappingProvider {
+
+ @Override
+ public @Nullable T map(@Nullable Object source, Class targetType, Configuration configuration) {
+ if (source == null) {
+ return null;
+ }
+ try {
+ return this.objectMapper.convertValue(source, targetType);
+ }
+ catch (Exception ex) {
+ throw new MappingException(ex);
+ }
+ }
+
+ @Override
+ public @Nullable T map(@Nullable Object source, final TypeRef targetType, Configuration configuration) {
+ if (source == null) {
+ return null;
+ }
+ JavaType type = this.objectMapper.constructType(targetType.getType());
+
+ try {
+ return this.objectMapper.convertValue(source, type);
+ }
+ catch (Exception ex) {
+ throw new MappingException(ex);
+ }
+ }
+
+ }
+
+}
diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/JacksonUtils.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/JacksonUtils.java
index 89821b788..32765a387 100644
--- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/JacksonUtils.java
+++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/JacksonUtils.java
@@ -30,7 +30,10 @@
* @author Artem Bilan
*
* @since 3.1.1
+ *
+ * @deprecated since 4.0 in favor of native Jackson 3 {@link tools.jackson.databind.json.JsonMapper#builder()} API.
*/
+@Deprecated(forRemoval = true, since = "4.0")
public final class JacksonUtils {
private static final boolean JDK8_MODULE_PRESENT =
diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/JacksonXmlMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/JacksonXmlMessageConverter.java
new file mode 100644
index 000000000..d0c606461
--- /dev/null
+++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/JacksonXmlMessageConverter.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2018-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.amqp.support.converter;
+
+import tools.jackson.databind.DeserializationFeature;
+import tools.jackson.dataformat.xml.XmlMapper;
+
+import org.springframework.amqp.core.MessageProperties;
+import org.springframework.util.MimeTypeUtils;
+
+/**
+ * XML converter that uses the Jackson 3 XML mapper.
+ *
+ * @author Artem Bilan
+ *
+ * @since 4.0
+ */
+public class JacksonXmlMessageConverter extends AbstractJacksonMessageConverter {
+
+ /**
+ * Construct with an internal {@link XmlMapper} instance
+ * and trusted packed to all ({@code *}).
+ */
+ public JacksonXmlMessageConverter() {
+ this("*");
+ }
+
+ /**
+ * Construct with an internal {@link XmlMapper} instance.
+ * The {@link DeserializationFeature#FAIL_ON_UNKNOWN_PROPERTIES} is set to false on
+ * the {@link XmlMapper}.
+ * @param trustedPackages the trusted Java packages for deserialization
+ * @see DefaultJacksonJavaTypeMapper#setTrustedPackages(String...)
+ */
+ public JacksonXmlMessageConverter(String... trustedPackages) {
+ this(XmlMapper.xmlBuilder()
+ .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
+ .build(),
+ trustedPackages);
+ }
+
+ /**
+ * Construct with the provided {@link XmlMapper} instance
+ * and trusted packed to all ({@code *}).
+ * @param xmlMapper the {@link XmlMapper} to use.
+ */
+ public JacksonXmlMessageConverter(XmlMapper xmlMapper) {
+ this(xmlMapper, "*");
+ }
+
+ /**
+ * Construct with the provided {@link XmlMapper} instance.
+ * @param xmlMapper the {@link XmlMapper} to use.
+ * @param trustedPackages the trusted Java packages for deserialization
+ * @see DefaultJacksonJavaTypeMapper#setTrustedPackages(String...)
+ */
+ public JacksonXmlMessageConverter(XmlMapper xmlMapper, String... trustedPackages) {
+ super(xmlMapper, MimeTypeUtils.parseMimeType(MessageProperties.CONTENT_TYPE_XML), trustedPackages);
+ }
+
+}
diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/ProjectingMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/ProjectingMessageConverter.java
index ab2622007..6d3d5d257 100644
--- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/ProjectingMessageConverter.java
+++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/ProjectingMessageConverter.java
@@ -35,9 +35,12 @@
* interfaces.
*
* @author Gary Russell
+ *
* @since 2.2
*
+ * @deprecated since 4.0 in favor of {@link JacksonProjectingMessageConverter}.
*/
+@Deprecated(since = "4.0", forRemoval = true)
public class ProjectingMessageConverter {
private final ProjectionFactory projectionFactory;
diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/SimpleAmqpHeaderMapperTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/SimpleAmqpHeaderMapperTests.java
index b6e6bca9a..91e22d7c6 100644
--- a/spring-amqp/src/test/java/org/springframework/amqp/support/SimpleAmqpHeaderMapperTests.java
+++ b/spring-amqp/src/test/java/org/springframework/amqp/support/SimpleAmqpHeaderMapperTests.java
@@ -26,7 +26,7 @@
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.core.MessageProperties;
-import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
+import org.springframework.amqp.support.converter.JacksonJsonMessageConverter;
import org.springframework.messaging.MessageHeaders;
import org.springframework.util.MimeTypeUtils;
@@ -39,6 +39,7 @@
* @author Gary Russell
* @author Oleg Zhurakousky
* @author Raylax Grey
+ * @author Artem Bilan
*/
public class SimpleAmqpHeaderMapperTests {
@@ -117,12 +118,11 @@ public void fromHeadersWithLongDelay() {
assertThat(amqpProperties.getDelayLong()).isEqualTo(Long.valueOf(MessageProperties.X_DELAY_MAX));
assertThatThrownBy(() -> amqpProperties.setDelayLong(MessageProperties.X_DELAY_MAX + 1))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessageContaining("Delay cannot exceed");
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Delay cannot exceed");
}
-
@Test
public void fromHeadersWithContentTypeAsMediaType() {
SimpleAmqpHeaderMapper headerMapper = new SimpleAmqpHeaderMapper();
@@ -195,7 +195,7 @@ public void toHeaders() {
@Test // INT-2090
public void jsonTypeIdNotOverwritten() {
SimpleAmqpHeaderMapper headerMapper = new SimpleAmqpHeaderMapper();
- Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
+ JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter();
MessageProperties amqpProperties = new MessageProperties();
converter.toMessage("123", amqpProperties);
Map headerMap = new HashMap<>();
diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/ContentTypeDelegatingMessageConverterTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/ContentTypeDelegatingMessageConverterTests.java
index 44329ae90..d1774e258 100644
--- a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/ContentTypeDelegatingMessageConverterTests.java
+++ b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/ContentTypeDelegatingMessageConverterTests.java
@@ -49,8 +49,8 @@ static void tearDown() {
@Test
public void testDelegationOutbound() {
ContentTypeDelegatingMessageConverter converter = new ContentTypeDelegatingMessageConverter();
- Jackson2JsonMessageConverter messageConverter =
- new Jackson2JsonMessageConverter(ContentTypeDelegatingMessageConverterTests.class.getPackage().getName());
+ JacksonJsonMessageConverter messageConverter =
+ new JacksonJsonMessageConverter(ContentTypeDelegatingMessageConverterTests.class.getPackage().getName());
converter.addDelegate("foo/bar", messageConverter);
converter.addDelegate(MessageProperties.CONTENT_TYPE_JSON, messageConverter);
MessageProperties props = new MessageProperties();
diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverterTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/JacksonJsonMessageConverterTests.java
similarity index 86%
rename from spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverterTests.java
rename to spring-amqp/src/test/java/org/springframework/amqp/support/converter/JacksonJsonMessageConverterTests.java
index 60d630a13..626f2c50d 100644
--- a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverterTests.java
+++ b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/JacksonJsonMessageConverterTests.java
@@ -16,22 +16,22 @@
package org.springframework.amqp.support.converter;
-import java.io.IOException;
import java.math.BigDecimal;
import java.util.Hashtable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
-import com.fasterxml.jackson.core.JsonParser;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.DeserializationContext;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
-import com.fasterxml.jackson.databind.module.SimpleModule;
-import com.fasterxml.jackson.databind.ser.BeanSerializerFactory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import tools.jackson.core.JacksonException;
+import tools.jackson.core.JsonParser;
+import tools.jackson.databind.DeserializationContext;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.deser.std.StdDeserializer;
+import tools.jackson.databind.json.JsonMapper;
+import tools.jackson.databind.module.SimpleModule;
+import tools.jackson.databind.ser.BeanSerializerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
@@ -54,20 +54,20 @@
*/
@SpringJUnitConfig
@DirtiesContext
-public class Jackson2JsonMessageConverterTests {
+public class JacksonJsonMessageConverterTests {
- public static final String TRUSTED_PACKAGE = Jackson2JsonMessageConverterTests.class.getPackage().getName();
+ public static final String TRUSTED_PACKAGE = JacksonJsonMessageConverterTests.class.getPackage().getName();
- private Jackson2JsonMessageConverter converter;
+ private JacksonJsonMessageConverter converter;
private SimpleTrade trade;
@Autowired
- private Jackson2JsonMessageConverter jsonConverterWithDefaultType;
+ private JacksonJsonMessageConverter jsonConverterWithDefaultType;
@BeforeEach
public void before() {
- converter = new Jackson2JsonMessageConverter(TRUSTED_PACKAGE);
+ converter = new JacksonJsonMessageConverter(TRUSTED_PACKAGE);
trade = new SimpleTrade();
trade.setAccountName("Acct1");
trade.setBuyRequest(true);
@@ -89,11 +89,13 @@ public void simpleTrade() {
@Test
public void simpleTradeOverrideMapper() {
- ObjectMapper mapper = new ObjectMapper();
- mapper.setSerializerFactory(BeanSerializerFactory.instance);
- converter = new Jackson2JsonMessageConverter(mapper);
+ ObjectMapper mapper =
+ JsonMapper.builder()
+ .serializerFactory(BeanSerializerFactory.instance)
+ .build();
+ converter = new JacksonJsonMessageConverter(mapper);
- ((DefaultJackson2JavaTypeMapper) this.converter.getJavaTypeMapper())
+ ((DefaultJacksonJavaTypeMapper) this.converter.getJavaTypeMapper())
.setTrustedPackages(TRUSTED_PACKAGE);
Message message = converter.toMessage(trade, new MessageProperties());
@@ -116,7 +118,7 @@ public void nestedBean() {
@Test
@SuppressWarnings("unchecked")
public void hashtable() {
- Hashtable hashtable = new Hashtable();
+ Hashtable hashtable = new Hashtable<>();
hashtable.put("TICKER", "VMW");
hashtable.put("PRICE", "103.2");
@@ -171,7 +173,7 @@ public void testDefaultType() {
MessageProperties messageProperties = new MessageProperties();
messageProperties.setContentType("application/json");
Message message = new Message(bytes, messageProperties);
- Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
+ JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter();
DefaultClassMapper classMapper = new DefaultClassMapper();
classMapper.setDefaultType(Foo.class);
converter.setClassMapper(classMapper);
@@ -279,7 +281,7 @@ public void testInferredGenericMap2() {
@Test
public void testProjection() {
- Jackson2JsonMessageConverter conv = new Jackson2JsonMessageConverter();
+ JacksonJsonMessageConverter conv = new JacksonJsonMessageConverter();
conv.setUseProjectionForInterfaces(true);
MessageProperties properties = new MessageProperties();
properties.setInferredArgumentType(Sample.class);
@@ -297,18 +299,18 @@ public void testMissingContentType() {
byte[] bytes = "{\"name\" : \"foo\" }".getBytes();
MessageProperties messageProperties = new MessageProperties();
Message message = new Message(bytes, messageProperties);
- Jackson2JsonMessageConverter j2Converter = new Jackson2JsonMessageConverter();
+ JacksonJsonMessageConverter jsonMessageConverter = new JacksonJsonMessageConverter();
DefaultClassMapper classMapper = new DefaultClassMapper();
classMapper.setDefaultType(Foo.class);
- j2Converter.setClassMapper(classMapper);
- Object foo = j2Converter.fromMessage(message);
+ jsonMessageConverter.setClassMapper(classMapper);
+ Object foo = jsonMessageConverter.fromMessage(message);
assertThat(foo).isInstanceOf(Foo.class);
- foo = j2Converter.fromMessage(message);
+ foo = jsonMessageConverter.fromMessage(message);
assertThat(foo).isInstanceOf(Foo.class);
- j2Converter.setAssumeSupportedContentType(false);
- foo = j2Converter.fromMessage(message);
+ jsonMessageConverter.setAssumeSupportedContentType(false);
+ foo = jsonMessageConverter.fromMessage(message);
assertThat(foo).isSameAs(bytes);
}
@@ -319,11 +321,10 @@ void customAbstractClass() {
messageProperties.setHeader("__TypeId__", String.class.getName());
messageProperties.setInferredArgumentType(Baz.class);
Message message = new Message(bytes, messageProperties);
- ObjectMapper mapper = new ObjectMapper();
- mapper.registerModule(new BazModule());
- Jackson2JsonMessageConverter j2Converter = new Jackson2JsonMessageConverter(mapper);
- j2Converter.setAlwaysConvertToInferredType(true);
- Baz baz = (Baz) j2Converter.fromMessage(message);
+ ObjectMapper mapper = JsonMapper.builder().addModule(new BazModule()).build();
+ JacksonJsonMessageConverter messageConverter = new JacksonJsonMessageConverter(mapper);
+ messageConverter.setAlwaysConvertToInferredType(true);
+ Baz baz = (Baz) messageConverter.fromMessage(message);
assertThat(((Qux) baz).getField()).isEqualTo("foo");
}
@@ -334,8 +335,8 @@ void fallbackToHeaders() {
messageProperties.setHeader("__TypeId__", Buz.class.getName());
messageProperties.setInferredArgumentType(Baz.class);
Message message = new Message(bytes, messageProperties);
- Jackson2JsonMessageConverter j2Converter = new Jackson2JsonMessageConverter();
- Fiz buz = (Fiz) j2Converter.fromMessage(message);
+ JacksonJsonMessageConverter jsonMessageConverter = new JacksonJsonMessageConverter();
+ Fiz buz = (Fiz) jsonMessageConverter.fromMessage(message);
assertThat(((Buz) buz).getField()).isEqualTo("foo");
}
@@ -346,12 +347,11 @@ void customAbstractClassList() throws Exception {
messageProperties.setHeader("__TypeId__", String.class.getName());
messageProperties.setInferredArgumentType(getClass().getDeclaredMethod("bazLister").getGenericReturnType());
Message message = new Message(bytes, messageProperties);
- ObjectMapper mapper = new ObjectMapper();
- mapper.registerModule(new BazModule());
- Jackson2JsonMessageConverter j2Converter = new Jackson2JsonMessageConverter(mapper);
- j2Converter.setAlwaysConvertToInferredType(true);
+ ObjectMapper mapper = JsonMapper.builder().addModule(new BazModule()).build();
+ JacksonJsonMessageConverter jsonMessageConverter = new JacksonJsonMessageConverter(mapper);
+ jsonMessageConverter.setAlwaysConvertToInferredType(true);
@SuppressWarnings("unchecked")
- List bazs = (List) j2Converter.fromMessage(message);
+ List bazs = (List) jsonMessageConverter.fromMessage(message);
assertThat(bazs).hasSize(1);
assertThat(((Qux) bazs.get(0)).getField()).isEqualTo("foo");
}
@@ -364,11 +364,10 @@ void cantDeserializeFizListUseHeaders() throws Exception {
messageProperties.setHeader("__TypeId__", List.class.getName());
messageProperties.setHeader("__ContentTypeId__", Buz.class.getName());
Message message = new Message(bytes, messageProperties);
- ObjectMapper mapper = new ObjectMapper();
- mapper.registerModule(new BazModule());
- Jackson2JsonMessageConverter j2Converter = new Jackson2JsonMessageConverter(mapper);
+ ObjectMapper mapper = JsonMapper.builder().addModule(new BazModule()).build();
+ JacksonJsonMessageConverter jsonMessageConverter = new JacksonJsonMessageConverter(mapper);
@SuppressWarnings("unchecked")
- List buzs = (List) j2Converter.fromMessage(message);
+ List buzs = (List) jsonMessageConverter.fromMessage(message);
assertThat(buzs).hasSize(1);
assertThat(((Buz) buzs.get(0)).getField()).isEqualTo("foo");
}
@@ -381,9 +380,9 @@ void concreteInListRegression() throws Exception {
messageProperties.setHeader("__TypeId__", List.class.getName());
messageProperties.setHeader("__ContentTypeId__", Object.class.getName());
Message message = new Message(bytes, messageProperties);
- Jackson2JsonMessageConverter j2Converter = new Jackson2JsonMessageConverter();
+ JacksonJsonMessageConverter jsonMessageConverter = new JacksonJsonMessageConverter();
@SuppressWarnings("unchecked")
- List foos = (List) j2Converter.fromMessage(message);
+ List foos = (List) jsonMessageConverter.fromMessage(message);
assertThat(foos).hasSize(1);
assertThat(foos.get(0).getName()).isEqualTo("bar");
}
@@ -397,10 +396,10 @@ void concreteInMapRegression() throws Exception {
messageProperties.setHeader("__KeyTypeId__", String.class.getName());
messageProperties.setHeader("__ContentTypeId__", Object.class.getName());
Message message = new Message(bytes, messageProperties);
- Jackson2JsonMessageConverter j2Converter = new Jackson2JsonMessageConverter();
+ JacksonJsonMessageConverter jsonMessageConverter = new JacksonJsonMessageConverter();
@SuppressWarnings("unchecked")
- Map foos = (Map) j2Converter.fromMessage(message);
+ Map foos = (Map) jsonMessageConverter.fromMessage(message);
assertThat(foos).hasSize(1);
assertThat(foos.keySet().iterator().next()).isEqualTo("test");
assertThat(foos.values().iterator().next().getField()).isEqualTo("baz");
@@ -409,7 +408,7 @@ void concreteInMapRegression() throws Exception {
@Test
void charsetInContentType() {
trade.setUserName("John Doe ∫");
- Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
+ JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter();
String utf8 = "application/json;charset=utf-8";
converter.setSupportedContentType(MimeTypeUtils.parseMimeType(utf8));
Message message = converter.toMessage(trade, new MessageProperties());
@@ -444,7 +443,7 @@ void charsetInContentType() {
@Test
void noConfigForCharsetInContentType() {
trade.setUserName("John Doe ∫");
- Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
+ JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter();
Message message = converter.toMessage(trade, new MessageProperties());
int bodyLength8 = message.getBody().length;
SimpleTrade marshalledTrade = (SimpleTrade) converter.fromMessage(message);
@@ -640,14 +639,11 @@ public BazDeserializer() {
}
@Override
- public Baz deserialize(JsonParser p, DeserializationContext ctxt)
- throws IOException, JsonProcessingException {
-
- p.nextFieldName();
- String field = p.nextTextValue();
+ public Baz deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException {
+ p.nextName();
+ String field = p.nextStringValue();
p.nextToken();
return new Qux(field);
-
}
}
diff --git a/spring-amqp/src/test/resources/org/springframework/amqp/support/converter/Jackson2JsonMessageConverterTests-context.xml b/spring-amqp/src/test/resources/org/springframework/amqp/support/converter/JacksonJsonMessageConverterTests-context.xml
similarity index 71%
rename from spring-amqp/src/test/resources/org/springframework/amqp/support/converter/Jackson2JsonMessageConverterTests-context.xml
rename to spring-amqp/src/test/resources/org/springframework/amqp/support/converter/JacksonJsonMessageConverterTests-context.xml
index 86648b01b..97e05576a 100644
--- a/spring-amqp/src/test/resources/org/springframework/amqp/support/converter/Jackson2JsonMessageConverterTests-context.xml
+++ b/spring-amqp/src/test/resources/org/springframework/amqp/support/converter/JacksonJsonMessageConverterTests-context.xml
@@ -3,12 +3,12 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
-
+
-
+ value="org.springframework.amqp.support.converter.JacksonJsonMessageConverterTests$Foo" />
+
diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AsyncListenerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AsyncListenerTests.java
index 5a9aae290..74953545f 100644
--- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AsyncListenerTests.java
+++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AsyncListenerTests.java
@@ -44,7 +44,7 @@
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.junit.RabbitAvailable;
import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry;
-import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
+import org.springframework.amqp.support.converter.JacksonJsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.amqp.utils.test.TestUtils;
import org.springframework.beans.factory.annotation.Autowired;
@@ -58,6 +58,8 @@
/**
* @author Gary Russell
+ * @author Artem Bilan
+ *
* @since 2.1
*
*/
@@ -152,7 +154,7 @@ public static class EnableRabbitConfig {
@Bean
public MessageConverter converter() {
- return new Jackson2JsonMessageConverter();
+ return new JacksonJsonMessageConverter();
}
@Bean
diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ComplexTypeJsonIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ComplexTypeJsonIntegrationTests.java
index 5bc432360..2d3c05fe9 100644
--- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ComplexTypeJsonIntegrationTests.java
+++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ComplexTypeJsonIntegrationTests.java
@@ -18,6 +18,7 @@
import java.util.concurrent.TimeUnit;
+import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.AsyncRabbitTemplate;
@@ -30,7 +31,7 @@
import org.springframework.amqp.rabbit.junit.RabbitAvailable;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter;
-import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
+import org.springframework.amqp.support.converter.JacksonJsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
@@ -44,15 +45,17 @@
/**
* @author Gary Russell
+ * @author Artem Bilan
+ *
* @since 2.0
*
*/
@SpringJUnitConfig
@DirtiesContext
-@RabbitAvailable(queues = { ComplexTypeJsonIntegrationTests.TEST_QUEUE, ComplexTypeJsonIntegrationTests.TEST_QUEUE2 })
-@LogLevels(classes = { RabbitTemplate.class,
+@RabbitAvailable(queues = {ComplexTypeJsonIntegrationTests.TEST_QUEUE, ComplexTypeJsonIntegrationTests.TEST_QUEUE2})
+@LogLevels(classes = {RabbitTemplate.class,
MessagingMessageListenerAdapter.class,
- SimpleMessageListenerContainer.class })
+ SimpleMessageListenerContainer.class})
public class ComplexTypeJsonIntegrationTests {
public static final String TEST_QUEUE = "test.complex.send.and.receive";
@@ -68,8 +71,8 @@ public class ComplexTypeJsonIntegrationTests {
private static Foo> makeAFoo() {
Foo> foo = new Foo<>();
Bar bar = new Bar<>();
- bar.setaField(new Baz("foo"));
- bar.setbField(new Qux(42));
+ bar.setAField(new Baz("foo"));
+ bar.setBField(new Qux(42));
foo.setField(bar);
return foo;
}
@@ -78,22 +81,34 @@ private static Foo> makeAFoo() {
* Covers all flavors of convertSendAndReceiveAsType
*/
@Test
- public void testSendAndReceive() throws Exception {
+ public void testSendAndReceive() {
verifyFooBarBazQux(this.rabbitTemplate.convertSendAndReceiveAsType("foo",
- new ParameterizedTypeReference>>() { }));
+ new ParameterizedTypeReference>>() {
+
+ }));
verifyFooBarBazQux(this.rabbitTemplate.convertSendAndReceiveAsType("foo",
m -> m,
- new ParameterizedTypeReference>>() { }));
+ new ParameterizedTypeReference>>() {
+
+ }));
verifyFooBarBazQux(this.rabbitTemplate.convertSendAndReceiveAsType(TEST_QUEUE, "foo",
- new ParameterizedTypeReference>>() { }));
+ new ParameterizedTypeReference>>() {
+
+ }));
verifyFooBarBazQux(this.rabbitTemplate.convertSendAndReceiveAsType(TEST_QUEUE, "foo",
m -> m,
- new ParameterizedTypeReference>>() { }));
+ new ParameterizedTypeReference>>() {
+
+ }));
verifyFooBarBazQux(this.rabbitTemplate.convertSendAndReceiveAsType("", TEST_QUEUE, "foo",
- new ParameterizedTypeReference>>() { }));
+ new ParameterizedTypeReference>>() {
+
+ }));
verifyFooBarBazQux(this.rabbitTemplate.convertSendAndReceiveAsType("", TEST_QUEUE, "foo",
m -> m,
- new ParameterizedTypeReference>>() { }));
+ new ParameterizedTypeReference>>() {
+
+ }));
}
@Test
@@ -103,17 +118,21 @@ public void testReceive() {
return m;
});
verifyFooBarBazQux(
- this.rabbitTemplate.receiveAndConvert(10_000, new ParameterizedTypeReference>>() { }));
+ this.rabbitTemplate.receiveAndConvert(10_000, new ParameterizedTypeReference>>() {
+
+ }));
}
@Test
- public void testReceiveNoWait() throws Exception {
+ public void testReceiveNoWait() {
this.rabbitTemplate.convertAndSend(TEST_QUEUE2, makeAFoo(), m -> {
m.getMessageProperties().getHeaders().remove("__TypeId__");
return m;
});
Foo> foo = await().until(
- () -> this.rabbitTemplate.receiveAndConvert(new ParameterizedTypeReference>>() { }),
+ () -> this.rabbitTemplate.receiveAndConvert(new ParameterizedTypeReference>>() {
+
+ }),
msg -> msg != null);
verifyFooBarBazQux(foo);
}
@@ -121,33 +140,45 @@ public void testReceiveNoWait() throws Exception {
@Test
public void testAsyncSendAndReceive() throws Exception {
verifyFooBarBazQux(this.asyncTemplate.convertSendAndReceiveAsType("foo",
- new ParameterizedTypeReference>>() { }));
+ new ParameterizedTypeReference>>() {
+
+ }));
verifyFooBarBazQux(this.asyncTemplate.convertSendAndReceiveAsType("foo",
m -> m,
- new ParameterizedTypeReference>>() { }));
+ new ParameterizedTypeReference>>() {
+
+ }));
verifyFooBarBazQux(this.asyncTemplate.convertSendAndReceiveAsType(TEST_QUEUE, "foo",
- new ParameterizedTypeReference>>() { }));
+ new ParameterizedTypeReference>>() {
+
+ }));
verifyFooBarBazQux(this.asyncTemplate.convertSendAndReceiveAsType(TEST_QUEUE, "foo",
m -> m,
- new ParameterizedTypeReference>>() { }));
+ new ParameterizedTypeReference>>() {
+
+ }));
verifyFooBarBazQux(this.asyncTemplate.convertSendAndReceiveAsType("", TEST_QUEUE, "foo",
- new ParameterizedTypeReference>>() { }));
+ new ParameterizedTypeReference>>() {
+
+ }));
verifyFooBarBazQux(this.asyncTemplate.convertSendAndReceiveAsType("", TEST_QUEUE, "foo",
m -> m,
- new ParameterizedTypeReference>>() { }));
+ new ParameterizedTypeReference>>() {
+
+ }));
}
private void verifyFooBarBazQux(RabbitConverterFuture>> future) throws Exception {
verifyFooBarBazQux(future.get(10, TimeUnit.SECONDS));
}
- private void verifyFooBarBazQux(Foo> foo) {
+ private void verifyFooBarBazQux(@Nullable Foo> foo) {
assertThat(foo).isNotNull();
Bar, ?> bar;
assertThat(foo.getField()).isInstanceOf(Bar.class);
bar = (Bar, ?>) foo.getField();
- assertThat(bar.getaField()).isInstanceOf(Baz.class);
- assertThat(bar.getbField()).isInstanceOf(Qux.class);
+ assertThat(bar.getAField()).isInstanceOf(Baz.class);
+ assertThat(bar.getBField()).isInstanceOf(Qux.class);
}
@Configuration
@@ -175,7 +206,7 @@ public AsyncRabbitTemplate asyncTemplate() {
@Bean
public MessageConverter jsonMessageConverter() {
- return new Jackson2JsonMessageConverter();
+ return new JacksonJsonMessageConverter();
}
@Bean
@@ -221,25 +252,25 @@ public String toString() {
}
- public static class Bar {
+ public static class Bar {
private A aField;
private B bField;
- public A getaField() {
- return this.aField;
+ public A getAField() {
+ return aField;
}
- public void setaField(A aField) {
+ public void setAField(A aField) {
this.aField = aField;
}
- public B getbField() {
- return this.bField;
+ public B getBField() {
+ return bField;
}
- public void setbField(B bField) {
+ public void setBField(B bField) {
this.bField = bField;
}
@@ -254,7 +285,7 @@ public static class Baz {
private String baz;
- Baz() {
+ public Baz() {
}
public Baz(String string) {
@@ -280,7 +311,7 @@ public static class Qux {
private Integer qux;
- Qux() {
+ public Qux() {
}
public Qux(int i) {
diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitBatchJsonIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitBatchJsonIntegrationTests.java
index f1125e56b..c192f8104 100644
--- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitBatchJsonIntegrationTests.java
+++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitBatchJsonIntegrationTests.java
@@ -31,7 +31,7 @@
import org.springframework.amqp.rabbit.core.BatchingRabbitTemplate;
import org.springframework.amqp.rabbit.junit.RabbitAvailable;
import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition;
-import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
+import org.springframework.amqp.support.converter.JacksonJsonMessageConverter;
import org.springframework.amqp.support.converter.SimpleMessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
@@ -52,7 +52,7 @@
*/
@SpringJUnitConfig
@DirtiesContext
-@RabbitAvailable(queues = { "json.batch.1", "json.batch.2" })
+@RabbitAvailable(queues = {"json.batch.1", "json.batch.2"})
public class EnableRabbitBatchJsonIntegrationTests {
@Autowired
@@ -109,8 +109,8 @@ public BatchingRabbitTemplate template() {
}
@Bean
- public Jackson2JsonMessageConverter converter() {
- return new Jackson2JsonMessageConverter();
+ public JacksonJsonMessageConverter converter() {
+ return new JacksonJsonMessageConverter();
}
@Bean
diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java
index bb0c27501..85d3e5c0f 100644
--- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java
+++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java
@@ -89,10 +89,10 @@
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.amqp.support.ConsumerTagStrategy;
import org.springframework.amqp.support.converter.DefaultClassMapper;
-import org.springframework.amqp.support.converter.DefaultJackson2JavaTypeMapper;
-import org.springframework.amqp.support.converter.Jackson2JavaTypeMapper.TypePrecedence;
-import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
-import org.springframework.amqp.support.converter.Jackson2XmlMessageConverter;
+import org.springframework.amqp.support.converter.DefaultJacksonJavaTypeMapper;
+import org.springframework.amqp.support.converter.JacksonJavaTypeMapper;
+import org.springframework.amqp.support.converter.JacksonJsonMessageConverter;
+import org.springframework.amqp.support.converter.JacksonXmlMessageConverter;
import org.springframework.amqp.support.converter.RemoteInvocationAwareMessageConverterAdapter;
import org.springframework.amqp.support.converter.SimpleMessageConverter;
import org.springframework.amqp.utils.test.TestUtils;
@@ -621,13 +621,13 @@ public void testConverted() {
RabbitTemplate template = ctx.getBean(RabbitTemplate.class);
Foo1 foo1 = new Foo1();
foo1.setBar("bar");
- Jackson2JsonMessageConverter converter = ctx.getBean(Jackson2JsonMessageConverter.class);
- converter.setTypePrecedence(TypePrecedence.TYPE_ID);
+ JacksonJsonMessageConverter converter = ctx.getBean(JacksonJsonMessageConverter.class);
+ converter.setTypePrecedence(JacksonJavaTypeMapper.TypePrecedence.TYPE_ID);
Object returned = template.convertSendAndReceive("test.converted", foo1);
assertThat(returned).isInstanceOf(Foo2.class);
assertThat(((Foo2) returned).getBar()).isEqualTo("bar");
assertThat(TestUtils.getPropertyValue(ctx.getBean("foo1To2Converter"), "converted", Boolean.class)).isTrue();
- converter.setTypePrecedence(TypePrecedence.INFERRED);
+ converter.setTypePrecedence(JacksonJavaTypeMapper.TypePrecedence.INFERRED);
// No type info in message
template.setMessageConverter(new SimpleMessageConverter());
@@ -720,10 +720,10 @@ public void testConverted() {
assertThat(returned).isInstanceOf(byte[].class);
assertThat(new String((byte[]) returned)).isEqualTo("\"SomeUsernameSomeName\"");
- Jackson2JsonMessageConverter jsonConverter = ctx.getBean(Jackson2JsonMessageConverter.class);
+ JacksonJsonMessageConverter jsonConverter = ctx.getBean(JacksonJsonMessageConverter.class);
- DefaultJackson2JavaTypeMapper mapper = TestUtils.getPropertyValue(jsonConverter, "javaTypeMapper",
- DefaultJackson2JavaTypeMapper.class);
+ DefaultJacksonJavaTypeMapper mapper = TestUtils.getPropertyValue(jsonConverter, "javaTypeMapper",
+ DefaultJacksonJavaTypeMapper.class);
Mockito.verify(mapper).setBeanClassLoader(ctx.getClassLoader());
ctx.close();
@@ -736,13 +736,13 @@ public void testXmlConverted() {
RabbitTemplate template = ctx.getBean(RabbitTemplate.class);
Foo1 foo1 = new Foo1();
foo1.setBar("bar");
- Jackson2XmlMessageConverter converter = ctx.getBean(Jackson2XmlMessageConverter.class);
- converter.setTypePrecedence(TypePrecedence.TYPE_ID);
+ JacksonXmlMessageConverter converter = ctx.getBean(JacksonXmlMessageConverter.class);
+ converter.setTypePrecedence(JacksonJavaTypeMapper.TypePrecedence.TYPE_ID);
Object returned = template.convertSendAndReceive("test.converted", foo1);
assertThat(returned).isInstanceOf(Foo2.class);
assertThat(((Foo2) returned).getBar()).isEqualTo("bar");
assertThat(TestUtils.getPropertyValue(ctx.getBean("foo1To2Converter"), "converted", Boolean.class)).isTrue();
- converter.setTypePrecedence(TypePrecedence.INFERRED);
+ converter.setTypePrecedence(JacksonJavaTypeMapper.TypePrecedence.INFERRED);
// No type info in message
template.setMessageConverter(new SimpleMessageConverter());
@@ -811,10 +811,10 @@ public void testXmlConverted() {
assertThat(returned).isInstanceOf(byte[].class);
assertThat(new String((byte[]) returned)).isEqualTo("GenericMessageLinkedHashMap ");
- Jackson2XmlMessageConverter xmlConverter = ctx.getBean(Jackson2XmlMessageConverter.class);
+ JacksonXmlMessageConverter xmlConverter = ctx.getBean(JacksonXmlMessageConverter.class);
- DefaultJackson2JavaTypeMapper mapper = TestUtils.getPropertyValue(xmlConverter, "javaTypeMapper",
- DefaultJackson2JavaTypeMapper.class);
+ DefaultJacksonJavaTypeMapper mapper = TestUtils.getPropertyValue(xmlConverter, "javaTypeMapper",
+ DefaultJacksonJavaTypeMapper.class);
Mockito.verify(mapper).setBeanClassLoader(ctx.getClassLoader());
ctx.close();
@@ -1726,7 +1726,7 @@ public SimpleRabbitListenerContainerFactory jsonListenerContainerFactory() {
factory.setConnectionFactory(rabbitConnectionFactory());
factory.setErrorHandler(errorHandler());
factory.setConsumerTagStrategy(consumerTagStrategy());
- Jackson2JsonMessageConverter messageConverter = new Jackson2JsonMessageConverter();
+ JacksonJsonMessageConverter messageConverter = new JacksonJsonMessageConverter();
DefaultClassMapper classMapper = new DefaultClassMapper();
Map> idClassMapping = new HashMap<>();
idClassMapping.put(
@@ -1745,7 +1745,7 @@ public SimpleRabbitListenerContainerFactory jsonListenerContainerFactoryNoClassM
factory.setConnectionFactory(rabbitConnectionFactory());
factory.setErrorHandler(errorHandler());
factory.setConsumerTagStrategy(consumerTagStrategy());
- Jackson2JsonMessageConverter messageConverter = new Jackson2JsonMessageConverter();
+ JacksonJsonMessageConverter messageConverter = new JacksonJsonMessageConverter();
factory.setMessageConverter(messageConverter);
factory.setReceiveTimeout(10L);
factory.setConcurrentConsumers(2);
@@ -1763,7 +1763,7 @@ public SimpleRabbitListenerContainerFactory simpleJsonListenerContainerFactory()
factory.setConnectionFactory(rabbitConnectionFactory());
factory.setErrorHandler(errorHandler());
factory.setConsumerTagStrategy(consumerTagStrategy());
- Jackson2JsonMessageConverter messageConverter = new Jackson2JsonMessageConverter();
+ JacksonJsonMessageConverter messageConverter = new JacksonJsonMessageConverter();
messageConverter.getJavaTypeMapper().addTrustedPackages("*");
factory.setMessageConverter(messageConverter);
factory.setReceiveTimeout(10L);
@@ -1979,7 +1979,7 @@ public RabbitTemplate rabbitTemplate() {
@Bean
public RabbitTemplate jsonRabbitTemplate() {
RabbitTemplate rabbitTemplate = new RabbitTemplate(rabbitConnectionFactory());
- rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
+ rabbitTemplate.setMessageConverter(new JacksonJsonMessageConverter());
return rabbitTemplate;
}
@@ -2262,10 +2262,10 @@ public RabbitTemplate jsonRabbitTemplate() {
}
@Bean
- public Jackson2JsonMessageConverter jsonConverter() {
- Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter();
- DefaultJackson2JavaTypeMapper mapper = Mockito.spy(TestUtils.getPropertyValue(jackson2JsonMessageConverter,
- "javaTypeMapper", DefaultJackson2JavaTypeMapper.class));
+ public JacksonJsonMessageConverter jsonConverter() {
+ JacksonJsonMessageConverter jackson2JsonMessageConverter = new JacksonJsonMessageConverter();
+ DefaultJacksonJavaTypeMapper mapper = Mockito.spy(TestUtils.getPropertyValue(jackson2JsonMessageConverter,
+ "javaTypeMapper", DefaultJacksonJavaTypeMapper.class));
new DirectFieldAccessor(jackson2JsonMessageConverter).setPropertyValue("javaTypeMapper", mapper);
jackson2JsonMessageConverter.setUseProjectionForInterfaces(true);
return jackson2JsonMessageConverter;
@@ -2350,10 +2350,10 @@ public RabbitTemplate xmlRabbitTemplate() {
}
@Bean
- public Jackson2XmlMessageConverter xmlConverter() {
- Jackson2XmlMessageConverter jackson2XmlMessageConverter = new Jackson2XmlMessageConverter();
- DefaultJackson2JavaTypeMapper mapper = Mockito.spy(TestUtils.getPropertyValue(jackson2XmlMessageConverter,
- "javaTypeMapper", DefaultJackson2JavaTypeMapper.class));
+ public JacksonXmlMessageConverter xmlConverter() {
+ JacksonXmlMessageConverter jackson2XmlMessageConverter = new JacksonXmlMessageConverter();
+ DefaultJacksonJavaTypeMapper mapper = Mockito.spy(TestUtils.getPropertyValue(jackson2XmlMessageConverter,
+ "javaTypeMapper", DefaultJacksonJavaTypeMapper.class));
new DirectFieldAccessor(jackson2XmlMessageConverter).setPropertyValue("javaTypeMapper", mapper);
return jackson2XmlMessageConverter;
}
diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitReturnTypesTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitReturnTypesTests.java
index 6e31217e1..f45c7784f 100644
--- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitReturnTypesTests.java
+++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitReturnTypesTests.java
@@ -32,7 +32,7 @@
import org.springframework.amqp.rabbit.listener.adapter.ReplyPostProcessor;
import org.springframework.amqp.rabbit.listener.api.RabbitListenerErrorHandler;
import org.springframework.amqp.support.converter.ContentTypeDelegatingMessageConverter;
-import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
+import org.springframework.amqp.support.converter.JacksonJsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.amqp.support.converter.SimpleMessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
@@ -46,13 +46,15 @@
/**
* @author Gary Russell
+ * @author Artem Bilan
+ *
* @since 2.2
*
*/
@SpringJUnitConfig
@DirtiesContext
-@RabbitAvailable(queues = { "EnableRabbitReturnTypesTests.1", "EnableRabbitReturnTypesTests.2",
- "EnableRabbitReturnTypesTests.3", "EnableRabbitReturnTypesTests.4", "EnableRabbitReturnTypesTests.5" })
+@RabbitAvailable(queues = {"EnableRabbitReturnTypesTests.1", "EnableRabbitReturnTypesTests.2",
+ "EnableRabbitReturnTypesTests.3", "EnableRabbitReturnTypesTests.4", "EnableRabbitReturnTypesTests.5"})
public class EnableRabbitReturnTypesTests {
@Test
@@ -100,7 +102,7 @@ public static class Config {
@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(CachingConnectionFactory cf,
- Jackson2JsonMessageConverter converter) {
+ JacksonJsonMessageConverter converter) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(cf);
@@ -110,7 +112,7 @@ public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(Cachi
}
@Bean
- public RabbitTemplate template(CachingConnectionFactory cf, Jackson2JsonMessageConverter converter) {
+ public RabbitTemplate template(CachingConnectionFactory cf, JacksonJsonMessageConverter converter) {
RabbitTemplate template = new RabbitTemplate(cf);
template.setMessageConverter(converter);
template.setReplyTimeout(30_000);
@@ -128,8 +130,8 @@ public RabbitAdmin admin(CachingConnectionFactory cf) {
}
@Bean
- public Jackson2JsonMessageConverter converter() {
- return new Jackson2JsonMessageConverter();
+ public JacksonJsonMessageConverter converter() {
+ return new JacksonJsonMessageConverter();
}
@Bean
diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/OptionalPayloadTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/OptionalPayloadTests.java
index 8d162cc12..e15e131f7 100644
--- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/OptionalPayloadTests.java
+++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/OptionalPayloadTests.java
@@ -35,7 +35,7 @@
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.junit.RabbitAvailable;
import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition;
-import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
+import org.springframework.amqp.support.converter.JacksonJsonMessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -47,11 +47,13 @@
/**
* @author Gary Russell
+ * @author Artem Bilan
+ *
* @since 2.8
*
*/
@SpringJUnitConfig
-@RabbitAvailable(queues = { "op.1", "op.2" })
+@RabbitAvailable(queues = {"op.1", "op.2"})
@DirtiesContext
public class OptionalPayloadTests {
@@ -107,8 +109,8 @@ ConnectionFactory rabbitConnectionFactory() {
}
@Bean
- Jackson2JsonMessageConverter converter() {
- Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
+ JacksonJsonMessageConverter converter() {
+ JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter();
converter.setNullAsOptionalEmpty(true);
return converter;
}
diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/AsyncReplyToTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/AsyncReplyToTests.java
index 599758a8d..a28670734 100644
--- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/AsyncReplyToTests.java
+++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/AsyncReplyToTests.java
@@ -16,11 +16,9 @@
package org.springframework.amqp.rabbit.listener;
-import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
import com.rabbitmq.client.Channel;
import org.junit.jupiter.api.Test;
@@ -38,7 +36,7 @@
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.junit.RabbitAvailable;
import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition;
-import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
+import org.springframework.amqp.support.converter.JacksonJsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
@@ -47,58 +45,62 @@
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
/**
* @author Gary Russell
+ * @author Artem Bilan
+ *
* @since 2.2.21
*
*/
@SpringJUnitConfig
-@RabbitAvailable(queues = { "async1", "async2" })
+@RabbitAvailable(queues = {"async1", "async2"})
@DirtiesContext
public class AsyncReplyToTests {
@Test
void ackSingleWhenFatalSMLC(@Autowired Config config, @Autowired RabbitListenerEndpointRegistry registry,
- @Autowired RabbitTemplate template, @Autowired RabbitAdmin admin) throws IOException, InterruptedException {
+ @Autowired RabbitTemplate template, @Autowired RabbitAdmin admin) throws InterruptedException {
template.send("async1", MessageBuilder.withBody("\"foo\"".getBytes()).andProperties(
- MessagePropertiesBuilder.newInstance()
- .setContentType("application/json")
- .setReplyTo("nowhere")
- .build())
+ MessagePropertiesBuilder.newInstance()
+ .setContentType("application/json")
+ .setReplyTo("nowhere")
+ .build())
.build());
template.send("async1", MessageBuilder.withBody("junk".getBytes()).andProperties(
- MessagePropertiesBuilder.newInstance()
- .setContentType("application/json")
- .setReplyTo("nowhere")
- .build())
+ MessagePropertiesBuilder.newInstance()
+ .setContentType("application/json")
+ .setReplyTo("nowhere")
+ .build())
.build());
assertThat(config.smlcLatch.await(10, TimeUnit.SECONDS)).isTrue();
registry.getListenerContainer("smlc").stop();
- assertThat(admin.getQueueInfo("async1").getMessageCount()).isEqualTo(1);
+ await().untilAsserted(() -> assertThat(admin.getQueueInfo("async1").getMessageCount()).isEqualTo(1));
}
- @Test
- void ackSingleWhenFatalDMLC(@Autowired Config config, @Autowired RabbitListenerEndpointRegistry registry,
- @Autowired RabbitTemplate template, @Autowired RabbitAdmin admin) throws IOException, InterruptedException {
+ @Test
+ void ackSingleWhenFatalDMLC(@Autowired Config config, @Autowired RabbitListenerEndpointRegistry registry,
+ @Autowired RabbitTemplate template, @Autowired RabbitAdmin admin) throws InterruptedException {
template.send("async2", MessageBuilder.withBody("\"foo\"".getBytes()).andProperties(
- MessagePropertiesBuilder.newInstance()
- .setContentType("application/json")
- .setReplyTo("nowhere")
- .build())
+ MessagePropertiesBuilder.newInstance()
+ .setContentType("application/json")
+ .setReplyTo("nowhere")
+ .build())
.build());
template.send("async2", MessageBuilder.withBody("junk".getBytes()).andProperties(
- MessagePropertiesBuilder.newInstance()
- .setContentType("application/json")
- .setReplyTo("nowhere")
- .build())
+ MessagePropertiesBuilder.newInstance()
+ .setContentType("application/json")
+ .setReplyTo("nowhere")
+ .build())
.build());
assertThat(config.dmlcLatch.await(10, TimeUnit.SECONDS)).isTrue();
registry.getListenerContainer("dmlc").stop();
- assertThat(admin.getQueueInfo("async2").getMessageCount()).isEqualTo(0);
- }
+
+ await().untilAsserted(() -> assertThat(admin.getQueueInfo("async2").getMessageCount()).isEqualTo(0));
+ }
@Configuration
@EnableRabbit
@@ -122,11 +124,11 @@ CompletableFuture listen2(String in, Channel channel) {
@Bean
MessageConverter converter() {
- return new Jackson2JsonMessageConverter();
+ return new JacksonJsonMessageConverter();
}
@Bean
- ConnectionFactory cf() throws IOException, TimeoutException {
+ ConnectionFactory cf() {
return new CachingConnectionFactory(RabbitAvailableCondition.getBrokerRunning().getConnectionFactory());
}
@@ -177,4 +179,5 @@ RabbitAdmin admin(ConnectionFactory cf) {
}
}
+
}
diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapterTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapterTests.java
index 2d5387056..e33de2812 100644
--- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapterTests.java
+++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapterTests.java
@@ -21,7 +21,6 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
-import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.core.Message;
@@ -37,7 +36,7 @@
import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition;
import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
-import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
+import org.springframework.amqp.support.converter.JacksonJsonMessageConverter;
import org.springframework.amqp.utils.test.TestUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
@@ -50,7 +49,8 @@
/**
* @author Gary Russell
- * @author heng zhang
+ * @author Heng Zhang
+ * @author Artem Bilan
*
* @since 3.0
*
@@ -70,7 +70,7 @@ void compatibleMethod() throws Exception {
Method badMethod = getClass().getDeclaredMethod("listen", String.class);
assertThatIllegalStateException().isThrownBy(() ->
new BatchMessagingMessageListenerAdapter(this, badMethod, false, null, null)
- ).withMessageStartingWith("Mis-configuration");
+ ).withMessageStartingWith("Mis-configuration");
}
public void listen(String in) {
@@ -79,10 +79,9 @@ public void listen(String in) {
public void listen(List in) {
}
-
@Test
public void errorMsgConvert(@Autowired BatchMessagingMessageListenerAdapterTests.Config config,
- @Autowired RabbitTemplate template) throws Exception {
+ @Autowired RabbitTemplate template) throws Exception {
Message message = MessageBuilder.withBody("""
{
@@ -111,12 +110,12 @@ public void errorMsgConvert(@Autowired BatchMessagingMessageListenerAdapterTests
assertThat(config.countDownLatch.await(config.count * 1000L, TimeUnit.SECONDS)).isTrue();
}
-
-
@Configuration
@EnableRabbit
public static class Config {
+
volatile int count = 5;
+
volatile CountDownLatch countDownLatch = new CountDownLatch(count);
@RabbitListener(
@@ -145,8 +144,7 @@ public RabbitListenerContainerFactory rc(Connect
factory.setBatchSize(3);
factory.setConsumerBatchEnabled(true);
- Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter(new ObjectMapper());
- factory.setMessageConverter(jackson2JsonMessageConverter);
+ factory.setMessageConverter(new JacksonJsonMessageConverter());
return factory;
}
@@ -156,10 +154,12 @@ RabbitTemplate template(ConnectionFactory cf) {
return new RabbitTemplate(cf);
}
-
}
+
public static class Model {
+
String name;
+
String age;
public String getName() {
@@ -177,6 +177,7 @@ public String getAge() {
public void setAge(String age) {
this.age = age;
}
+
}
}
diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapterTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapterTests.java
index 12159b563..f07250d33 100644
--- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapterTests.java
+++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapterTests.java
@@ -18,7 +18,6 @@
import java.lang.reflect.Method;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -35,7 +34,7 @@
import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException;
import org.springframework.amqp.rabbit.test.MessageTestUtils;
import org.springframework.amqp.support.AmqpHeaders;
-import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
+import org.springframework.amqp.support.converter.JacksonJsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConversionException;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.amqp.support.converter.SimpleMessageConverter;
@@ -68,7 +67,6 @@ public class MessagingMessageListenerAdapterTests {
private final SampleBean sample = new SampleBean();
-
@BeforeEach
public void setup() {
initializeFactory(factory);
@@ -187,7 +185,7 @@ public void genericMessageTest1() throws Exception {
org.springframework.amqp.core.Message message = MessageTestUtils.createTextMessage("\"foo\"");
Channel channel = mock(Channel.class);
MessagingMessageListenerAdapter listener = getSimpleInstance("withGenericMessageAnyType", Message.class);
- listener.setMessageConverter(new Jackson2JsonMessageConverter());
+ listener.setMessageConverter(new JacksonJsonMessageConverter());
message.getMessageProperties().setContentType("application/json");
listener.onMessage(message, channel);
assertThat(this.sample.payload.getClass()).isEqualTo(String.class);
@@ -211,19 +209,18 @@ public void genericMessageTest2() throws Exception {
org.springframework.amqp.core.Message message = MessageTestUtils.createTextMessage("{ \"foo\" : \"bar\" }");
Channel channel = mock(Channel.class);
MessagingMessageListenerAdapter listener = getSimpleInstance("withGenericMessageFooType", Message.class);
- listener.setMessageConverter(new Jackson2JsonMessageConverter());
+ listener.setMessageConverter(new JacksonJsonMessageConverter());
message.getMessageProperties().setContentType("application/json");
listener.onMessage(message, channel);
assertThat(this.sample.payload.getClass()).isEqualTo(Foo.class);
}
-
@Test
public void genericMessageTest3() throws Exception {
org.springframework.amqp.core.Message message = MessageTestUtils.createTextMessage("{ \"foo\" : \"bar\" }");
Channel channel = mock(Channel.class);
MessagingMessageListenerAdapter listener = getSimpleInstance("withNonGenericMessage", Message.class);
- listener.setMessageConverter(new Jackson2JsonMessageConverter());
+ listener.setMessageConverter(new JacksonJsonMessageConverter());
message.getMessageProperties().setContentType("application/json");
listener.onMessage(message, channel);
assertThat(this.sample.payload.getClass()).isEqualTo(LinkedHashMap.class);
@@ -236,10 +233,10 @@ public void batchAmqpMessagesTest() {
message1.getMessageProperties().setContentType("application/json");
Channel channel = mock(Channel.class);
BatchMessagingMessageListenerAdapter listener = getBatchInstance("withAmqpMessageBatch");
- listener.setMessageConverter(new Jackson2JsonMessageConverter());
+ listener.setMessageConverter(new JacksonJsonMessageConverter());
// when
- listener.onMessageBatch(Arrays.asList(message1), channel);
+ listener.onMessageBatch(List.of(message1), channel);
// then
assertThat(this.sample.batchPayloads.get(0).getClass()).isEqualTo(String.class);
@@ -252,10 +249,10 @@ public void batchTypedMessagesTest() {
message1.getMessageProperties().setContentType("application/json");
Channel channel = mock(Channel.class);
BatchMessagingMessageListenerAdapter listener = getBatchInstance("withTypedMessageBatch");
- listener.setMessageConverter(new Jackson2JsonMessageConverter());
+ listener.setMessageConverter(new JacksonJsonMessageConverter());
// when
- listener.onMessageBatch(Arrays.asList(message1), channel);
+ listener.onMessageBatch(List.of(message1), channel);
// then
assertThat(this.sample.batchPayloads.get(0).getClass()).isEqualTo(Foo.class);
@@ -268,10 +265,10 @@ public void batchTypedObjectTest() {
message1.getMessageProperties().setContentType("application/json");
Channel channel = mock(Channel.class);
BatchMessagingMessageListenerAdapter listener = getBatchInstance("withFooBatch");
- listener.setMessageConverter(new Jackson2JsonMessageConverter());
+ listener.setMessageConverter(new JacksonJsonMessageConverter());
// when
- listener.onMessageBatch(Arrays.asList(message1), channel);
+ listener.onMessageBatch(List.of(message1), channel);
// then
assertThat(this.sample.batchPayloads.get(0).getClass()).isEqualTo(Foo.class);
@@ -419,6 +416,7 @@ private void initializeFactory(DefaultMessageHandlerMethodFactory factory) {
private static class SampleBean {
private Object payload;
+
private List batchPayloads;
SampleBean() {
diff --git a/src/reference/antora/modules/ROOT/pages/amqp/message-converters.adoc b/src/reference/antora/modules/ROOT/pages/amqp/message-converters.adoc
index c028af61b..ab9e3ffa4 100644
--- a/src/reference/antora/modules/ROOT/pages/amqp/message-converters.adoc
+++ b/src/reference/antora/modules/ROOT/pages/amqp/message-converters.adoc
@@ -95,31 +95,32 @@ This converter is similar to the `SimpleMessageConverter` except that it can be
See <> for important information.
[[json-message-converter]]
-== Jackson2JsonMessageConverter
+== JacksonJsonMessageConverter
-This section covers using the `Jackson2JsonMessageConverter` to convert to and from a `Message`.
+This section covers using the `JacksonJsonMessageConverter` to convert to and from a `Message`.
It has the following sections:
-* xref:amqp/message-converters.adoc#Jackson2JsonMessageConverter-to-message[Converting to a `Message`]
-* xref:amqp/message-converters.adoc#Jackson2JsonMessageConverter-from-message[Converting from a `Message`]
+* xref:amqp/message-converters.adoc#JacksonJsonMessageConverter-to-message[Converting to a `Message`]
+* xref:amqp/message-converters.adoc#JacksonJsonMessageConverter-from-message[Converting from a `Message`]
-[[Jackson2JsonMessageConverter-to-message]]
+NOTE: The `AbstractJackson2MessageConverter`, its implementations and related `Jackson2JavaTypeMapper` API have been deprecated for removal in `4.0` version in favor of respective classes based on Jackson 3.
+See JavaDocs of the deprecated classes for the respective migration guide.
+
+[[JacksonJsonMessageConverter-to-message]]
=== Converting to a `Message`
As mentioned in the previous section, relying on Java serialization is generally not recommended.
-One rather common alternative that is more flexible and portable across different languages and platforms is JSON
-(JavaScript Object Notation).
-The converter can be configured on any `RabbitTemplate` instance to override its usage of the `SimpleMessageConverter`
-default.
-The `Jackson2JsonMessageConverter` uses the `com.fasterxml.jackson` 2.x library.
-The following example configures a `Jackson2JsonMessageConverter`:
+One rather common alternative that is more flexible and portable across different languages and platforms is JSON (JavaScript Object Notation).
+The converter can be configured on any `RabbitTemplate` instance to override its usage of the `SimpleMessageConverter` default.
+The `JacksonJsonMessageConverter` uses the Jackson 3.x library.
+The following example configures a `JacksonJsonMessageConverter`:
[source,xml]
----
-
+
@@ -127,15 +128,14 @@ The following example configures a `Jackson2JsonMessageConverter`:
----
-As shown above, `Jackson2JsonMessageConverter` uses a `DefaultClassMapper` by default.
+As shown above, `JacksonJsonMessageConverter` uses a `DefaultClassMapper` by default.
Type information is added to (and retrieved from) `MessageProperties`.
-If an inbound message does not contain type information in `MessageProperties`, but you know the expected type, you
-can configure a static type by using the `defaultType` property, as the following example shows:
+If an inbound message does not contain type information in `MessageProperties`, but you know the expected type, you can configure a static type by using the `defaultType` property, as the following example shows:
[source,xml]
----
+ class="o.s.amqp.support.converter.JacksonJsonMessageConverter">
@@ -150,8 +150,8 @@ The following example shows how to do so:
[source, java]
----
@Bean
-public Jackson2JsonMessageConverter jsonMessageConverter() {
- Jackson2JsonMessageConverter jsonConverter = new Jackson2JsonMessageConverter();
+public JacksonJsonMessageConverter jsonMessageConverter() {
+ JacksonJsonMessageConverter jsonConverter = new JacksonJsonMessageConverter();
jsonConverter.setClassMapper(classMapper());
return jsonConverter;
}
@@ -179,7 +179,7 @@ String utf16 = "application/json; charset=utf-16";
converter.setSupportedContentType(MimeTypeUtils.parseMimeType(utf16));
----
-[[Jackson2JsonMessageConverter-from-message]]
+[[JacksonJsonMessageConverter-from-message]]
=== Converting from a `Message`
Inbound messages are converted to objects according to the type information added to headers by the sending system.
@@ -202,27 +202,22 @@ This lets the converter convert to the argument type of the target method.
This only applies if there is one parameter with no annotations or a single parameter with the `@Payload` annotation.
Parameters of type `Message` are ignored during the analysis.
-IMPORTANT: By default, the inferred type information will override the inbound `__TypeId__` and related headers created
-by the sending system.
+IMPORTANT: By default, the inferred type information will override the inbound `__TypeId__` and related headers created by the sending system.
This lets the receiving system automatically convert to a different domain object.
-This applies only if the parameter type is concrete (not abstract or an interface) or it is from the `java.util`
-package.
+This applies only if the parameter type is concrete (not abstract or an interface), or it is from the `java.util` package.
In all other cases, the `__TypeId__` and related headers is used.
There are cases where you might wish to override the default behavior and always use the `__TypeId__` information.
-For example, suppose you have a `@RabbitListener` that takes a `Thing1` argument but the message contains a `Thing2` that
-is a subclass of `Thing1` (which is concrete).
+For example, suppose you have a `@RabbitListener` that takes a `Thing1` argument but the message contains a `Thing2` that is a subclass of `Thing1` (which is concrete).
The inferred type would be incorrect.
-To handle this situation, set the `TypePrecedence` property on the `Jackson2JsonMessageConverter` to `TYPE_ID` instead
-of the default `INFERRED`.
-(The property is actually on the converter's `DefaultJackson2JavaTypeMapper`, but a setter is provided on the converter
-for convenience.)
+To handle this situation, set the `TypePrecedence` property on the `JacksonJsonMessageConverter` to `TYPE_ID` instead of the default `INFERRED`.
+(The property is actually on the converter's `DefaultJacksonJavaTypeMapper`, but a setter is provided on the converter for convenience.)
If you inject a custom type mapper, you should set the property on the mapper instead.
NOTE: When converting from the `Message`, an incoming `MessageProperties.getContentType()` must be JSON-compliant (`contentType.contains("json")` is used to check).
Starting with version 2.2, `application/json` is assumed if there is no `contentType` property, or it has the default value `application/octet-stream`.
To revert to the previous behavior (return an unconverted `byte[]`), set the converter's `assumeSupportedContentType` property to `false`.
If the content type is not supported, a `WARN` log message `Could not convert incoming message with content-type [...]`, is emitted and `message.getBody()` is returned as is -- as a `byte[]`.
-So, to meet the `Jackson2JsonMessageConverter` requirements on the consumer side, the producer must add the `contentType` message property -- for example, as `application/json` or `text/x-json` or by using the `Jackson2JsonMessageConverter`, which sets the header automatically.
+So, to meet the `JacksonJsonMessageConverter` requirements on the consumer side, the producer must add the `contentType` message property -- for example, as `application/json` or `text/x-json` or by using the `JacksonJsonMessageConverter`, which sets the header automatically.
The following listing shows a number of converter calls:
[source, java]
@@ -250,16 +245,14 @@ In the first four cases in the preceding listing, the converter tries to convert
The fifth example is invalid because we cannot determine which argument should receive the message payload.
With the sixth example, the Jackson defaults apply due to the generic type being a `WildcardType`.
-You can, however, create a custom converter and use the `targetMethod` message property to decide which type to convert
-the JSON to.
+You can, however, create a custom converter and use the `targetMethod` message property to decide which type to convert the JSON to.
NOTE: This type inference can only be achieved when the `@RabbitListener` annotation is declared at the method level.
With class-level `@RabbitListener`, the converted type is used to select which `@RabbitHandler` method to invoke.
-For this reason, the infrastructure provides the `targetObject` message property, which you can use in a custom
-converter to determine the type.
+For this reason, the infrastructure provides the `targetObject` message property, which you can use in a custom converter to determine the type.
-IMPORTANT: Starting with version 1.6.11, `Jackson2JsonMessageConverter` and, therefore, `DefaultJackson2JavaTypeMapper` (`DefaultClassMapper`) provide the `trustedPackages` option to overcome https://pivotal.io/security/cve-2017-4995[Serialization Gadgets] vulnerability.
-By default and for backward compatibility, the `Jackson2JsonMessageConverter` trusts all packages -- that is, it uses `*` for the option.
+IMPORTANT: Starting with version 1.6.11, `JacksonJsonMessageConverter` and, therefore, `DefaultJacksonJavaTypeMapper` (`DefaultClassMapper`) provide the `trustedPackages` option to overcome https://pivotal.io/security/cve-2017-4995[Serialization Gadgets] vulnerability.
+By default, and for backward compatibility, the `JacksonJsonMessageConverter` trusts all packages -- that is, it uses `*` for the option.
Starting with version 2.4.7, the converter can be configured to return `Optional.empty()` if Jackson returns `null` after deserializing the message body.
This facilitates `@RabbitListener` s to receive null payloads, in two ways:
@@ -282,8 +275,8 @@ To enable this feature, set `setNullAsOptionalEmpty` to `true`; when `false` (de
[source, java]
----
@Bean
-Jackson2JsonMessageConverter converter() {
- Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
+JacksonJsonMessageConverter converter() {
+ JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter();
converter.setNullAsOptionalEmpty(true);
return converter;
}
@@ -292,7 +285,7 @@ Jackson2JsonMessageConverter converter() {
[[jackson-abstract]]
=== Deserializing Abstract Classes
-Prior to version 2.2.8, if the inferred type of a `@RabbitListener` was an abstract class (including interfaces), the converter would fall back to looking for type information in the headers and, if present, used that information; if that was not present, it would try to create the abstract class.
+Prior to version 2.2.8, if the inferred type of the `@RabbitListener` was an abstract class (including interfaces), the converter would fall back to looking for type information in the headers and, if present, used that information; if that was not present, it would try to create the abstract class.
This caused a problem when a custom `ObjectMapper` that is configured with a custom deserializer to handle the abstract class is used, but the incoming message has invalid type headers.
Starting with version 2.2.8, the previous behavior is retained by default. If you have such a custom `ObjectMapper` and you want to ignore type headers, and always use the inferred type for conversion, set the `alwaysConvertToInferredType` to `true`.
@@ -338,7 +331,7 @@ When used as the parameter to a `@RabbitListener` method, the interface type is
As mentioned earlier, type information is conveyed in message headers to assist the converter when converting from a message.
This works fine in most cases.
However, when using generic types, it can only convert simple objects and known "`container`" objects (lists, arrays, and maps).
-Starting with version 2.0, the `Jackson2JsonMessageConverter` implements `SmartMessageConverter`, which lets it be used with the new `RabbitTemplate` methods that take a `ParameterizedTypeReference` argument.
+Starting with version 2.0, the `JacksonJsonMessageConverter` implements `SmartMessageConverter`, which lets it be used with the new `RabbitTemplate` methods that take a `ParameterizedTypeReference` argument.
This allows conversion of complex generic types, as shown in the following example:
[source, java]
@@ -347,10 +340,6 @@ Thing1> thing1 =
rabbitTemplate.receiveAndConvert(new ParameterizedTypeReference>>() { });
----
-NOTE: Starting with version 2.1, the `AbstractJsonMessageConverter` class has been removed.
-It is no longer the base class for `Jackson2JsonMessageConverter`.
-It has been replaced by `AbstractJackson2MessageConverter`.
-
[[marshallingmessageconverter]]
== `MarshallingMessageConverter`
@@ -372,24 +361,22 @@ The following example shows how to configure a `MarshallingMessageConverter`:
----
-[[jackson2xml]]
-== `Jackson2XmlMessageConverter`
+[[jackson-xml]]
+== `JacksonXmlMessageConverter`
This class was introduced in version 2.1 and can be used to convert messages from and to XML.
-Both `Jackson2XmlMessageConverter` and `Jackson2JsonMessageConverter` have the same base class: `AbstractJackson2MessageConverter`.
-
-NOTE: The `AbstractJackson2MessageConverter` class is introduced to replace a removed class: `AbstractJsonMessageConverter`.
+Both `JacksonXmlMessageConverter` and `JacksonJsonMessageConverter` have the same base class: `AbstractJacksonMessageConverter`.
-The `Jackson2XmlMessageConverter` uses the `com.fasterxml.jackson` 2.x library.
+The `JacksonXmlMessageConverter` uses the Jackson 3.x library.
-You can use it the same way as `Jackson2JsonMessageConverter`, except it supports XML instead of JSON.
-The following example configures a `Jackson2JsonMessageConverter`:
+You can use it the same way as `JacksonJsonMessageConverter`, except it supports XML instead of JSON.
+The following example configures a `JacksonJsonMessageConverter`:
[source,xml]
----
+ class="org.springframework.amqp.support.converter.JacksonXmlMessageConverter">
@@ -488,7 +475,7 @@ Previously, when converting to and from `BasicProperties` used by the RabbitMQ c
To provide maximum backwards compatibility, a new property called `correlationIdPolicy` has been added to the
`DefaultMessagePropertiesConverter`.
This takes a `DefaultMessagePropertiesConverter.CorrelationIdPolicy` enum argument.
-By default it is set to `BYTES`, which replicates the previous behavior.
+By default, it is set to `BYTES`, which replicates the previous behavior.
For inbound messages:
diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-1-since-2-0.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-1-since-2-0.adoc
index 07f3bacc9..ce353ea72 100644
--- a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-1-since-2-0.adoc
+++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-1-since-2-0.adoc
@@ -75,7 +75,7 @@ See `setReplyErrorHandler` on the `RabbitTemplate`.
== Message Conversion
We introduced a new `Jackson2XmlMessageConverter` to support converting messages from and to XML format.
-See xref:amqp/message-converters.adoc#jackson2xml[`Jackson2XmlMessageConverter`] for more information.
+See xref:amqp/message-converters.adoc#jackson-xml[`Jackson2XmlMessageConverter`] for more information.
[[management-rest-api]]
== Management REST API
diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-2-since-2-1.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-2-since-2-1.adoc
index 90e38330b..5f48da244 100644
--- a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-2-since-2-1.adoc
+++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-2-since-2-1.adoc
@@ -57,10 +57,10 @@ Spring Data Projection interfaces are now supported by the `Jackson2JsonMessageC
See xref:amqp/message-converters.adoc#data-projection[Using Spring Data Projection Interfaces] for more information.
The `Jackson2JsonMessageConverter` now assumes the content is JSON if there is no `contentType` property, or it is the default (`application/octet-string`).
-See xref:amqp/message-converters.adoc#Jackson2JsonMessageConverter-from-message[Converting from a `Message`] for more information.
+See xref:amqp/message-converters.adoc#JacksonJsonMessageConverter-from-message[Converting from a `Message`] for more information.
Similarly. the `Jackson2XmlMessageConverter` now assumes the content is XML if there is no `contentType` property, or it is the default (`application/octet-string`).
-See xref:amqp/message-converters.adoc#jackson2xml[`Jackson2XmlMessageConverter`] for more information.
+See xref:amqp/message-converters.adoc#jackson-xml[`Jackson2XmlMessageConverter`] for more information.
When a `@RabbitListener` method returns a result, the bean and `Method` are now available in the reply message properties.
This allows configuration of a `beforeSendReplyMessagePostProcessor` to, for example, set a header in the reply to indicate which method was invoked on the server.
diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-3-0-since-2-4.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-3-0-since-2-4.adoc
index 63562059c..0540a3455 100644
--- a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-3-0-since-2-4.adoc
+++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-3-0-since-2-4.adoc
@@ -47,7 +47,7 @@ When setting the container factory `consumerBatchEnabled` to `true`, the `batchL
See xref:amqp/receiving-messages/batch.adoc[@RabbitListener with Batching] for more information.
`MessageConverter` s can now return `Optional.empty()` for a null value; this is currently implemented by the `Jackson2JsonMessageConverter`.
-See xref:amqp/message-converters.adoc#Jackson2JsonMessageConverter-from-message[Converting from a `Message`] for more information
+See xref:amqp/message-converters.adoc#JacksonJsonMessageConverter-from-message[Converting from a `Message`] for more information
You can now configure a `ReplyPostProcessor` via the container factory rather than via a property on `@RabbitListener`.
See xref:amqp/receiving-messages/async-annotation-driven/reply.adoc[Reply Management] for more information.
diff --git a/src/reference/antora/modules/ROOT/pages/sample-apps.adoc b/src/reference/antora/modules/ROOT/pages/sample-apps.adoc
index 842c5c72a..4a66fc823 100644
--- a/src/reference/antora/modules/ROOT/pages/sample-apps.adoc
+++ b/src/reference/antora/modules/ROOT/pages/sample-apps.adoc
@@ -203,7 +203,7 @@ That involves a private `replyTo` queue that is sent by the client within the or
The server's core configuration is in the `RabbitServerConfiguration` class within the `org.springframework.amqp.rabbit.stocks.config.server` package.
It extends the `AbstractStockAppRabbitConfiguration`.
That is where the resources common to the server and client are defined, including the market data topic exchange (whose name is 'app.stock.marketdata') and the queue that the server exposes for stock trades (whose name is 'app.stock.request').
-In that common configuration file, you also see that a `Jackson2JsonMessageConverter` is configured on the `RabbitTemplate`.
+In that common configuration file, you also see that a `JacksonJsonMessageConverter` is configured on the `RabbitTemplate`.
The server-specific configuration consists of two things.
First, it configures the market data exchange on the `RabbitTemplate` so that it does not need to provide that exchange name with every call to send a `Message`.
diff --git a/src/reference/antora/modules/ROOT/pages/whats-new.adoc b/src/reference/antora/modules/ROOT/pages/whats-new.adoc
index 3479e22af..2d3e19578 100644
--- a/src/reference/antora/modules/ROOT/pages/whats-new.adoc
+++ b/src/reference/antora/modules/ROOT/pages/whats-new.adoc
@@ -27,5 +27,13 @@ See xref:rabbitmq-amqp-client.adoc[] for more information.
[[x40-junit4-deprecation]]
=== Deprecation of JUnit 4 utilities
-The latest JUnit 4 release was `4.13.2` in February 2021 and the next JUnit 6 will be based on JAva 17.
-There is no need to keep out-dated utilities and recommendation is to migrate to respective tools for JUnit 5.
\ No newline at end of file
+The latest JUnit 4 release was `4.13.2` in February 2021 and the next JUnit 6 will be based on Java 17.
+There is no need to keep out-dated utilities and recommendation is to migrate to respective tools for JUnit 5.
+
+[[x40-jackson3-support]]
+=== The Jackson 3 Support
+
+The Jackson 2 has been deprecated for removal in whole Spring portfolio.
+Respective new classes have been introduced to support Jackson 3.
+
+See xref:amqp/message-converters.adoc[] for more information.
\ No newline at end of file