Message Converters
The AmqpTemplate
also defines several methods for sending and receiving messages that delegate to a MessageConverter
.
The MessageConverter
provides a single method for each direction: one for converting to a Message
and another for converting from a Message
.
Notice that, when converting to a Message
, you can also provide properties in addition to the object.
The object
parameter typically corresponds to the Message body.
The following listing shows the MessageConverter
interface definition:
public interface MessageConverter {
Message toMessage(Object object, MessageProperties messageProperties)
throws MessageConversionException;
Object fromMessage(Message message) throws MessageConversionException;
}
The relevant Message
-sending methods on the AmqpTemplate
are simpler than the methods we discussed previously, because they do not require the Message
instance.
Instead, the MessageConverter
is responsible for “creating” each Message
by converting the provided object to the byte array for the Message
body and then adding any provided MessageProperties
.
The following listing shows the definitions of the various methods:
void convertAndSend(Object message) throws AmqpException;
void convertAndSend(String routingKey, Object message) throws AmqpException;
void convertAndSend(String exchange, String routingKey, Object message)
throws AmqpException;
void convertAndSend(Object message, MessagePostProcessor messagePostProcessor)
throws AmqpException;
void convertAndSend(String routingKey, Object message,
MessagePostProcessor messagePostProcessor) throws AmqpException;
void convertAndSend(String exchange, String routingKey, Object message,
MessagePostProcessor messagePostProcessor) throws AmqpException;
On the receiving side, there are only two methods: one that accepts the queue name and one that relies on the template’s “queue” property having been set. The following listing shows the definitions of the two methods:
Object receiveAndConvert() throws AmqpException;
Object receiveAndConvert(String queueName) throws AmqpException;
The MessageListenerAdapter mentioned in Asynchronous Consumer also uses a MessageConverter .
|
SimpleMessageConverter
The default implementation of the MessageConverter
strategy is called SimpleMessageConverter
.
This is the converter that is used by an instance of RabbitTemplate
if you do not explicitly configure an alternative.
It handles text-based content, serialized Java objects, and byte arrays.
Converting From a Message
If the content type of the input Message
begins with "text" (for example,
"text/plain"), it also checks for the content-encoding property to determine the charset to be used when converting the Message
body byte array to a Java String
.
If no content-encoding property had been set on the input Message
, it uses the UTF-8 charset by default.
If you need to override that default setting, you can configure an instance of SimpleMessageConverter
, set its defaultCharset
property, and inject that into a RabbitTemplate
instance.
If the content-type property value of the input Message
is set to "application/x-java-serialized-object", the SimpleMessageConverter
tries to deserialize (rehydrate) the byte array into a Java object.
While that might be useful for simple prototyping, we do not recommend relying on Java serialization, since it leads to tight coupling between the producer and the consumer.
Of course, it also rules out usage of non-Java systems on either side.
With AMQP being a wire-level protocol, it would be unfortunate to lose much of that advantage with such restrictions.
In the next two sections, we explore some alternatives for passing rich domain object content without relying on Java serialization.
For all other content-types, the SimpleMessageConverter
returns the Message
body content directly as a byte array.
See Java Deserialization for important information.
Converting To a Message
When converting to a Message
from an arbitrary Java Object, the SimpleMessageConverter
likewise deals with byte arrays, strings, and serializable instances.
It converts each of these to bytes (in the case of byte arrays, there is nothing to convert), and it sets the content-type property accordingly.
If the Object
to be converted does not match one of those types, the Message
body is null.
SerializerMessageConverter
This converter is similar to the SimpleMessageConverter
except that it can be configured with other Spring Framework
Serializer
and Deserializer
implementations for application/x-java-serialized-object
conversions.
See Java Deserialization for important information.
Jackson2JsonMessageConverter
This section covers using the Jackson2JsonMessageConverter
to convert to and from a Message
.
It has the following sections:
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
:
<bean class="org.springframework.amqp.rabbit.core.RabbitTemplate">
<property name="connectionFactory" ref="rabbitConnectionFactory"/>
<property name="messageConverter">
<bean class="org.springframework.amqp.support.converter.Jackson2JsonMessageConverter">
<!-- if necessary, override the DefaultClassMapper -->
<property name="classMapper" ref="customClassMapper"/>
</bean>
</property>
</bean>
As shown above, Jackson2JsonMessageConverter
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:
<bean id="jsonConverterWithDefaultType"
class="o.s.amqp.support.converter.Jackson2JsonMessageConverter">
<property name="classMapper">
<bean class="org.springframework.amqp.support.converter.DefaultClassMapper">
<property name="defaultType" value="thing1.PurchaseOrder"/>
</bean>
</property>
</bean>
In addition, you can provide custom mappings from the value in the TypeId
header.
The following example shows how to do so:
@Bean
public Jackson2JsonMessageConverter jsonMessageConverter() {
Jackson2JsonMessageConverter jsonConverter = new Jackson2JsonMessageConverter();
jsonConverter.setClassMapper(classMapper());
return jsonConverter;
}
@Bean
public DefaultClassMapper classMapper() {
DefaultClassMapper classMapper = new DefaultClassMapper();
Map<String, Class<?>> idClassMapping = new HashMap<>();
idClassMapping.put("thing1", Thing1.class);
idClassMapping.put("thing2", Thing2.class);
classMapper.setIdClassMapping(idClassMapping);
return classMapper;
}
Now, if the sending system sets the header to thing1
, the converter creates a Thing1
object, and so on.
See the Receiving JSON from Non-Spring Applications sample application for a complete discussion about converting messages from non-Spring applications.
Starting with version 2.4.3, the converter will not add a contentEncoding
message property if the supportedMediaType
has a charset
parameter; this is also used for the encoding.
A new method setSupportedMediaType
has been added:
String utf16 = "application/json; charset=utf-16";
converter.setSupportedContentType(MimeTypeUtils.parseMimeType(utf16));
Converting from a Message
Inbound messages are converted to objects according to the type information added to headers by the sending system.
Starting with version 2.4.3, if there is no contentEncoding
message property, the converter will attempt to detect a charset
parameter in the contentType
message property and use that.
If neither exist, if the supportedMediaType
has a charset
parameter, it will be used for decoding, with a final fallback to the defaultCharset
property.
A new method setSupportedMediaType
has been added:
String utf16 = "application/json; charset=utf-16";
converter.setSupportedContentType(MimeTypeUtils.parseMimeType(utf16));
In versions prior to 1.6, if type information is not present, conversion would fail. Starting with version 1.6, if type information is missing, the converter converts the JSON by using Jackson defaults (usually a map).
Also, starting with version 1.6, when you use @RabbitListener
annotations (on methods), the inferred type information is added to the MessageProperties
.
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.
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.
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).
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.)
If you inject a custom type mapper, you should set the property on the mapper instead.
|
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.
The following listing shows a number of converter calls:
|
@RabbitListener
public void thing1(Thing1 thing1) {...}
@RabbitListener
public void thing1(@Payload Thing1 thing1, @Header("amqp_consumerQueue") String queue) {...}
@RabbitListener
public void thing1(Thing1 thing1, o.s.amqp.core.Message message) {...}
@RabbitListener
public void thing1(Thing1 thing1, o.s.messaging.Message<Foo> message) {...}
@RabbitListener
public void thing1(Thing1 thing1, String bar) {...}
@RabbitListener
public void thing1(Thing1 thing1, o.s.messaging.Message<?> message) {...}
In the first four cases in the preceding listing, the converter tries to convert to the Thing1
type.
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.
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.
|
Starting with version 1.6.11, Jackson2JsonMessageConverter and, therefore, DefaultJackson2JavaTypeMapper (DefaultClassMapper ) provide the trustedPackages option to overcome Serialization Gadgets vulnerability.
By default and for backward compatibility, the Jackson2JsonMessageConverter 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:
@RabbitListener(queues = "op.1")
void listen(@Payload(required = false) Thing payload) {
handleOptional(payload); // payload might be null
}
@RabbitListener(queues = "op.2")
void listen(Optional<Thing> optional) {
handleOptional(optional.orElse(this.emptyThing));
}
To enable this feature, set setNullAsOptionalEmpty
to true
; when false
(default), the converter falls back to the raw message body (byte[]
).
@Bean
Jackson2JsonMessageConverter converter() {
Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
converter.setNullAsOptionalEmpty(true);
return converter;
}
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.
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
.
This is needed for backwards compatibility and to avoid the overhead of an attempted conversion when it would fail (with a standard ObjectMapper
).
Using Spring Data Projection Interfaces
Starting with version 2.2, you can convert JSON to a Spring Data Projection interface instead of a concrete type. This allows very selective, and low-coupled bindings to data, including the lookup of values from multiple places inside the JSON document. For example the following interface can be defined as message payload type:
interface SomeSample {
@JsonPath({ "$.username", "$.user.name" })
String getUsername();
}
@RabbitListener(queues = "projection")
public void projection(SomeSample in) {
String username = in.getUsername();
...
}
Accessor methods will be used to lookup the property name as field in the received JSON document by default.
The @JsonPath
expression allows customization of the value lookup, and even to define multiple JSON path expressions, to lookup values from multiple places until an expression returns an actual value.
To enable this feature, set the useProjectionForInterfaces
to true
on the message converter.
You must also add spring-data:spring-data-commons
and com.jayway.jsonpath:json-path
to the class path.
When used as the parameter to a @RabbitListener
method, the interface type is automatically passed to the converter as normal.
Converting From a Message
With RabbitTemplate
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.
This allows conversion of complex generic types, as shown in the following example:
Thing1<Thing2<Cat, Hat>> thing1 =
rabbitTemplate.receiveAndConvert(new ParameterizedTypeReference<Thing1<Thing2<Cat, Hat>>>() { });
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
Yet another option is the MarshallingMessageConverter
.
It delegates to the Spring OXM library’s implementations of the Marshaller
and Unmarshaller
strategy interfaces.
You can read more about that library here.
In terms of configuration, it is most common to provide only the constructor argument, since most implementations of Marshaller
also implement Unmarshaller
.
The following example shows how to configure a MarshallingMessageConverter
:
<bean class="org.springframework.amqp.rabbit.core.RabbitTemplate">
<property name="connectionFactory" ref="rabbitConnectionFactory"/>
<property name="messageConverter">
<bean class="org.springframework.amqp.support.converter.MarshallingMessageConverter">
<constructor-arg ref="someImplemenationOfMarshallerAndUnmarshaller"/>
</bean>
</property>
</bean>
Jackson2XmlMessageConverter
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
.
The AbstractJackson2MessageConverter class is introduced to replace a removed class: AbstractJsonMessageConverter .
|
The Jackson2XmlMessageConverter
uses the com.fasterxml.jackson
2.x library.
You can use it the same way as Jackson2JsonMessageConverter
, except it supports XML instead of JSON.
The following example configures a Jackson2JsonMessageConverter
:
<bean id="xmlConverterWithDefaultType"
class="org.springframework.amqp.support.converter.Jackson2XmlMessageConverter">
<property name="classMapper">
<bean class="org.springframework.amqp.support.converter.DefaultClassMapper">
<property name="defaultType" value="foo.PurchaseOrder"/>
</bean>
</property>
</bean>
See Jackson2JsonMessageConverter for more information.
Starting with version 2.2, application/xml 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 .
|
ContentTypeDelegatingMessageConverter
This class was introduced in version 1.4.2 and allows delegation to a specific MessageConverter
based on the content type property in the MessageProperties
.
By default, it delegates to a SimpleMessageConverter
if there is no contentType
property or there is a value that matches none of the configured converters.
The following example configures a ContentTypeDelegatingMessageConverter
:
<bean id="contentTypeConverter" class="ContentTypeDelegatingMessageConverter">
<property name="delegates">
<map>
<entry key="application/json" value-ref="jsonMessageConverter" />
<entry key="application/xml" value-ref="xmlMessageConverter" />
</map>
</property>
</bean>
Java Deserialization
This section covers how to deserialize Java objects.
There is a possible vulnerability when deserializing java objects from untrusted sources. If you accept messages from untrusted sources with a By default, the allowed list is empty, meaning no classes will be deserialized. You can set a list of patterns, such as The patterns are checked in order until a match is found.
If there is no match, a You can set the patterns using the |
Message Properties Converters
The MessagePropertiesConverter
strategy interface is used to convert between the Rabbit Client BasicProperties
and Spring AMQP MessageProperties
.
The default implementation (DefaultMessagePropertiesConverter
) is usually sufficient for most purposes, but you can implement your own if needed.
The default properties converter converts BasicProperties
elements of type LongString
to String
instances when the size is not greater than 1024
bytes.
Larger LongString
instances are not converted (see the next paragraph).
This limit can be overridden with a constructor argument.
Starting with version 1.6, headers longer than the long string limit (default: 1024) are now left as
LongString
instances by default by the DefaultMessagePropertiesConverter
.
You can access the contents through the getBytes[]
, toString()
, or getStream()
methods.
Previously, the DefaultMessagePropertiesConverter
“converted” such headers to a DataInputStream
(actually it just referenced the LongString
instance’s DataInputStream
).
On output, this header was not converted (except to a String — for example, java.io.DataInputStream@1d057a39
by calling toString()
on the stream).
Large incoming LongString
headers are now correctly “converted” on output, too (by default).
A new constructor is provided to let you configure the converter to work as before. The following listing shows the Javadoc comment and declaration of the method:
/**
* Construct an instance where LongStrings will be returned
* unconverted or as a java.io.DataInputStream when longer than this limit.
* Use this constructor with 'true' to restore pre-1.6 behavior.
* @param longStringLimit the limit.
* @param convertLongLongStrings LongString when false,
* DataInputStream when true.
* @since 1.6
*/
public DefaultMessagePropertiesConverter(int longStringLimit, boolean convertLongLongStrings) { ... }
Also starting with version 1.6, a new property called correlationIdString
has been added to MessageProperties
.
Previously, when converting to and from BasicProperties
used by the RabbitMQ client, an unnecessary byte[] <→ String
conversion was performed because MessageProperties.correlationId
is a byte[]
, but BasicProperties
uses a String
.
(Ultimately, the RabbitMQ client uses UTF-8 to convert the String
to bytes to put in the protocol message).
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.
For inbound messages:
-
STRING
: Only thecorrelationIdString
property is mapped -
BYTES
: Only thecorrelationId
property is mapped -
BOTH
: Both properties are mapped
For outbound messages:
-
STRING
: Only thecorrelationIdString
property is mapped -
BYTES
: Only thecorrelationId
property is mapped -
BOTH
: Both properties are considered, with theString
property taking precedence
Also starting with version 1.6, the inbound deliveryMode
property is no longer mapped to MessageProperties.deliveryMode
.
It is mapped to MessageProperties.receivedDeliveryMode
instead.
Also, the inbound userId
property is no longer mapped to MessageProperties.userId
.
It is mapped to MessageProperties.receivedUserId
instead.
These changes are to avoid unexpected propagation of these properties if the same MessageProperties
object is used for an outbound message.
Starting with version 2.2, the DefaultMessagePropertiesConverter
converts any custom headers with values of type Class<?>
using getName()
instead of toString()
; this avoids consuming application having to parse the class name out of the toString()
representation.
For rolling upgrades, you may need to change your consumers to understand both formats until all producers are upgraded.