訊息轉換器
AmqpTemplate 還定義了幾個用於傳送和接收訊息的方法,這些方法都委託給 MessageConverter。MessageConverter 為每個方向提供了一個方法:一個用於將物件轉換為 Message,另一個用於將物件從 Message 轉換。請注意,在轉換為 Message 時,除了物件之外,您還可以提供屬性。object 引數通常對應於 Message 正文。以下列表顯示了 MessageConverter 介面定義
public interface MessageConverter {
Message toMessage(Object object, MessageProperties messageProperties)
throws MessageConversionException;
Object fromMessage(Message message) throws MessageConversionException;
}
AmqpTemplate 上相關的 Message 傳送方法比我們之前討論的方法更簡單,因為它們不需要 Message 例項。相反,MessageConverter 負責透過將提供的物件轉換為 Message 正文的位元組陣列,然後新增任何提供的 MessageProperties 來“建立”每個 Message。以下列表顯示了各種方法的定義
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;
在接收端,只有兩個方法:一個接受佇列名稱,另一個依賴於模板的“佇列”屬性已設定。以下列表顯示了這兩個方法的定義
Object receiveAndConvert() throws AmqpException;
Object receiveAndConvert(String queueName) throws AmqpException;
非同步消費者 中提到的 MessageListenerAdapter 也使用 MessageConverter。 |
SimpleMessageConverter
MessageConverter 策略的預設實現稱為 SimpleMessageConverter。如果您沒有明確配置替代方案,RabbitTemplate 例項會使用此轉換器。它處理基於文字的內容、序列化的 Java 物件和位元組陣列。
從 Message 轉換
如果輸入 Message 的內容型別以“text”開頭(例如,“text/plain”),它還會檢查 content-encoding 屬性,以確定將 Message 正文位元組陣列轉換為 Java String 時要使用的字元集。如果輸入 Message 上未設定 content-encoding 屬性,則預設使用 UTF-8 字元集。如果您需要覆蓋該預設設定,您可以配置一個 SimpleMessageConverter 例項,設定其 defaultCharset 屬性,並將其注入 RabbitTemplate 例項。
如果輸入 Message 的 content-type 屬性值設定為“application/x-java-serialized-object”,則 SimpleMessageConverter 嘗試將位元組陣列反序列化(重新水化)為 Java 物件。雖然這對於簡單的原型設計可能有用,但我們不建議依賴 Java 序列化,因為它會導致生產者和消費者之間的緊密耦合。當然,它也排除了在任一側使用非 Java 系統。AMQP 是一個線級協議,如果因為這些限制而失去大部分優勢,那將是不幸的。在接下來的兩節中,我們將探討一些在不依賴 Java 序列化的情況下傳遞富領域物件內容的替代方案。
對於所有其他內容型別,SimpleMessageConverter 直接將 Message 正文內容作為位元組陣列返回。
有關重要資訊,請參閱 Java 反序列化。
SerializerMessageConverter
此轉換器類似於 SimpleMessageConverter,不同之處在於它可以配置其他 Spring Framework Serializer 和 Deserializer 實現,用於 application/x-java-serialized-object 轉換。
有關重要資訊,請參閱 Java 反序列化。
JacksonJsonMessageConverter
本節介紹如何使用 JacksonJsonMessageConverter 轉換為和從 Message。它包含以下幾個部分
AbstractJackson2MessageConverter、其實現和相關的 Jackson2JavaTypeMapper API 已在 4.0 版本中棄用,並計劃移除,取而代之的是基於 Jackson 3 的相應類。有關相應的遷移指南,請參閱已棄用類的 JavaDocs。 |
轉換為 Message
如前一節所述,通常不建議依賴 Java 序列化。一種更靈活、更易於跨不同語言和平臺移植的常見替代方案是 JSON(JavaScript 物件表示法)。該轉換器可以在任何 RabbitTemplate 例項上配置,以覆蓋其對 SimpleMessageConverter 預設值的使用。JacksonJsonMessageConverter 使用 Jackson 3.x 庫。以下示例配置了一個 JacksonJsonMessageConverter
<bean class="org.springframework.amqp.rabbit.core.RabbitTemplate">
<property name="connectionFactory" ref="rabbitConnectionFactory"/>
<property name="messageConverter">
<bean class="org.springframework.amqp.support.converter.JacksonJsonMessageConverter">
<!-- if necessary, override the DefaultClassMapper -->
<property name="classMapper" ref="customClassMapper"/>
</bean>
</property>
</bean>
如上所示,JacksonJsonMessageConverter 預設使用 DefaultClassMapper。型別資訊被新增到(並從)MessageProperties 中檢索。如果入站訊息在 MessageProperties 中不包含型別資訊,但您知道預期型別,則可以使用 defaultType 屬性配置靜態型別,如下例所示
<bean id="jsonConverterWithDefaultType"
class="o.s.amqp.support.converter.JacksonJsonMessageConverter">
<property name="classMapper">
<bean class="org.springframework.amqp.support.converter.DefaultClassMapper">
<property name="defaultType" value="thing1.PurchaseOrder"/>
</bean>
</property>
</bean>
此外,您可以提供從 TypeId 頭部的值到自定義對映。以下示例展示瞭如何實現
@Bean
public JacksonJsonMessageConverter jsonMessageConverter() {
JacksonJsonMessageConverter jsonConverter = new JacksonJsonMessageConverter();
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;
}
現在,如果傳送系統將頭部設定為 thing1,轉換器將建立 Thing1 物件,依此類推。有關從非 Spring 應用程式轉換訊息的完整討論,請參閱 從非 Spring 應用程式接收 JSON 示例應用程式。
從版本 2.4.3 開始,如果 supportedMediaType 具有 charset 引數,轉換器將不再新增 contentEncoding 訊息屬性;這也用於編碼。已新增新方法 setSupportedMediaType
String utf16 = "application/json; charset=utf-16";
converter.setSupportedContentType(MimeTypeUtils.parseMimeType(utf16));
從 Message 轉換
根據傳送系統新增到頭部中的型別資訊,入站訊息被轉換為物件。
從版本 2.4.3 開始,如果不存在 contentEncoding 訊息屬性,轉換器將嘗試檢測 contentType 訊息屬性中的 charset 引數並使用它。如果兩者都不存在,如果 supportedMediaType 具有 charset 引數,它將用於解碼,最終回退到 defaultCharset 屬性。已新增新方法 setSupportedMediaType
String utf16 = "application/json; charset=utf-16";
converter.setSupportedContentType(MimeTypeUtils.parseMimeType(utf16));
在 1.6 之前的版本中,如果不存在型別資訊,轉換將失敗。從 1.6 版本開始,如果缺少型別資訊,轉換器將使用 Jackson 預設值(通常是 map)轉換 JSON。
此外,從 1.6 版本開始,當您使用 @RabbitListener 註解(在方法上)時,推斷的型別資訊會新增到 MessageProperties 中。這允許轉換器轉換為目標方法的引數型別。這僅適用於只有一個沒有註解的引數或一個帶有 @Payload 註解的引數。型別為 Message 的引數在分析期間將被忽略。
預設情況下,推斷的型別資訊將覆蓋入站 TypeId 和傳送系統建立的相關頭部。這允許接收系統自動轉換為不同的域物件。這僅適用於引數型別是具體型別(不是抽象或介面),或者它來自 java.util 包。在所有其他情況下,將使用 TypeId 和相關頭部。在某些情況下,您可能希望覆蓋預設行為並始終使用 TypeId 資訊。例如,假設您有一個 @RabbitListener 接受 Thing1 引數,但訊息包含 Thing2,它是 Thing1 的子類(它是具體的)。推斷的型別將不正確。要處理這種情況,請將 JacksonJsonMessageConverter 上的 TypePrecedence 屬性設定為 TYPE_ID 而不是預設的 INFERRED。(該屬性實際上在轉換器的 DefaultJacksonJavaTypeMapper 上,但為了方便,在轉換器上提供了 setter。)如果您注入自定義型別對映器,則應在對映器上設定該屬性。 |
當從 Message 轉換時,傳入的 MessageProperties.getContentType() 必須符合 JSON(使用 contentType.contains("json") 進行檢查)。從版本 2.2 開始,如果不存在 contentType 屬性,或者它的預設值為 application/octet-stream,則假定為 application/json。要恢復到以前的行為(返回未轉換的 byte[]),請將轉換器的 assumeSupportedContentType 屬性設定為 false。如果內容型別不受支援,則會發出 WARN 日誌訊息 Could not convert incoming message with content-type […],並且 message.getBody() 按原樣返回 — 作為 byte[]。因此,為了滿足消費者端的 JacksonJsonMessageConverter 要求,生產者必須新增 contentType 訊息屬性 — 例如,作為 application/json 或 text/x-json,或者使用會自動設定頭部的 JacksonJsonMessageConverter。以下列表顯示了許多轉換器呼叫 |
@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) {...}
在前面列表中的前四種情況中,轉換器嘗試轉換為 Thing1 型別。第五個示例無效,因為我們無法確定哪個引數應該接收訊息負載。在第六個示例中,由於泛型型別是 WildcardType,因此適用 Jackson 預設值。
但是,您可以建立一個自定義轉換器,並使用 targetMethod 訊息屬性來決定將 JSON 轉換為哪種型別。
此型別推斷只能在方法級別宣告 @RabbitListener 註解時實現。對於類級別的 @RabbitListener,轉換後的型別用於選擇要呼叫的 @RabbitHandler 方法。因此,基礎設施提供了 targetObject 訊息屬性,您可以在自定義轉換器中使用它來確定型別。 |
從版本 1.6.11 開始,JacksonJsonMessageConverter,以及 DefaultJacksonJavaTypeMapper(DefaultClassMapper),提供了 trustedPackages 選項,以克服 序列化小工具 漏洞。預設情況下,為了向後相容,JacksonJsonMessageConverter 信任所有包——也就是說,它使用 * 作為該選項。 |
從 2.4.7 版本開始,轉換器可以配置為在 Jackson 反序列化訊息體後返回 Optional.empty(),如果 Jackson 返回 null。這有助於 @RabbitListener 以兩種方式接收 null 負載
@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));
}
要啟用此功能,請將 setNullAsOptionalEmpty 設定為 true;當為 false(預設)時,轉換器將回退到原始訊息體(byte[])。
@Bean
JacksonJsonMessageConverter converter() {
JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter();
converter.setNullAsOptionalEmpty(true);
return converter;
}
反序列化抽象類
在 2.2.8 版本之前,如果 @RabbitListener 的推斷型別是抽象類(包括介面),轉換器將回退到在頭部中查詢型別資訊,如果存在,則使用該資訊;如果不存在,它將嘗試建立抽象類。當使用配置有自定義反序列化器來處理抽象類的自定義 ObjectMapper,但傳入訊息具有無效型別頭部時,這會導致問題。
從 2.2.8 版本開始,預設情況下保留以前的行為。如果您有這樣的自定義 ObjectMapper,並且希望忽略型別頭部,並始終使用推斷型別進行轉換,請將 alwaysConvertToInferredType 設定為 true。這是為了向後相容性,並避免在轉換失敗(使用標準 ObjectMapper)時嘗試轉換的開銷。
使用 Spring Data 投影介面
從 2.2 版本開始,您可以將 JSON 轉換為 Spring Data Projection 介面,而不是具體的型別。這允許對資料進行非常選擇性且低耦合的繫結,包括從 JSON 文件內的多個位置查詢值。例如,以下介面可以定義為訊息負載型別
interface SomeSample {
@JsonPath({ "$.username", "$.user.name" })
String getUsername();
}
@RabbitListener(queues = "projection")
public void projection(SomeSample in) {
String username = in.getUsername();
...
}
預設情況下,訪問器方法將用於在接收到的 JSON 文件中查詢屬性名稱作為欄位。@JsonPath 表示式允許自定義值查詢,甚至可以定義多個 JSON 路徑表示式,從多個位置查詢值,直到某個表示式返回實際值。
要啟用此功能,請在訊息轉換器上將 useProjectionForInterfaces 設定為 true。您還必須將 spring-data:spring-data-commons 和 com.jayway.jsonpath:json-path 新增到類路徑中。
當用作 @RabbitListener 方法的引數時,介面型別會自動傳遞給轉換器。
使用 RabbitTemplate 從 Message 轉換
如前所述,型別資訊在訊息頭部中傳遞,以協助轉換器從訊息轉換。這在大多數情況下都執行良好。但是,當使用泛型型別時,它只能轉換簡單的物件和已知的“容器”物件(列表、陣列和對映)。從 2.0 版本開始,JacksonJsonMessageConverter 實現了 SmartMessageConverter,這使得它可以與接受 ParameterizedTypeReference 引數的新 RabbitTemplate 方法一起使用。這允許轉換複雜的泛型型別,如下例所示
Thing1<Thing2<Cat, Hat>> thing1 =
rabbitTemplate.receiveAndConvert(new ParameterizedTypeReference<Thing1<Thing2<Cat, Hat>>>() { });
MarshallingMessageConverter
另一個選擇是 MarshallingMessageConverter。它委託給 Spring OXM 庫的 Marshaller 和 Unmarshaller 策略介面的實現。您可以在此處閱讀有關該庫的更多資訊。在配置方面,最常見的是隻提供建構函式引數,因為 Marshaller 的大多數實現也實現了 Unmarshaller。以下示例展示瞭如何配置 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="someImplementationOfMarshallerAndUnmarshaller"/>
</bean>
</property>
</bean>
JacksonXmlMessageConverter
此類在 2.1 版本中引入,可用於將訊息轉換為 XML 和從 XML 轉換。
JacksonXmlMessageConverter 和 JacksonJsonMessageConverter 具有相同的基類:AbstractJacksonMessageConverter。
JacksonXmlMessageConverter 使用 Jackson 3.x 庫。
您可以像使用 JacksonJsonMessageConverter 一樣使用它,只是它支援 XML 而不是 JSON。以下示例配置了一個 JacksonJsonMessageConverter
<bean id="xmlConverterWithDefaultType"
class="org.springframework.amqp.support.converter.JacksonXmlMessageConverter">
<property name="classMapper">
<bean class="org.springframework.amqp.support.converter.DefaultClassMapper">
<property name="defaultType" value="foo.PurchaseOrder"/>
</bean>
</property>
</bean>
有關更多資訊,請參閱 JacksonJsonMessageConverter。
從版本 2.2 開始,如果不存在 contentType 屬性,或者它的預設值為 application/octet-stream,則假定為 application/xml。要恢復到以前的行為(返回未轉換的 byte[]),請將轉換器的 assumeSupportedContentType 屬性設定為 false。 |
ContentTypeDelegatingMessageConverter
此類在 1.4.2 版本中引入,並允許根據 MessageProperties 中的內容型別屬性委託給特定的 MessageConverter。預設情況下,如果沒有 contentType 屬性或其值與任何已配置的轉換器都不匹配,則它會委託給 SimpleMessageConverter。以下示例配置了一個 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 反序列化
本節介紹如何反序列化 Java 物件。
|
從不可信來源反序列化 Java 物件時可能存在漏洞。 如果您接受來自不可信來源的 預設情況下,允許列表為空,這意味著不會反序列化任何類。 您可以設定一個模式列表,例如 模式按順序檢查,直到找到匹配項。如果沒有匹配項,則會丟擲 您可以使用這些轉換器上的 |
訊息屬性轉換器
MessagePropertiesConverter 策略介面用於在 Rabbit 客戶端 BasicProperties 和 Spring AMQP MessageProperties 之間進行轉換。預設實現(DefaultMessagePropertiesConverter)通常足以滿足大多數目的,但如果需要,您可以實現自己的。預設屬性轉換器將型別為 LongString 的 BasicProperties 元素轉換為 String 例項,當大小不大於 1024 位元組時。較大的 LongString 例項不會被轉換(請參閱下一段)。此限制可以透過建構函式引數進行覆蓋。
從版本 1.6 開始,超出長字串限制(預設值:1024)的頭部現在預設由 DefaultMessagePropertiesConverter 保留為 LongString 例項。您可以透過 getBytes[]、toString() 或 getStream() 方法訪問其內容。
以前,DefaultMessagePropertiesConverter 將此類頭部“轉換”為 DataInputStream(實際上它只是引用了 LongString 例項的 DataInputStream)。在輸出時,此頭部未轉換(除了轉換為字串——例如,透過在流上呼叫 toString() 得到的 java.io.DataInputStream@1d057a39)。
現在,大型入站 LongString 頭部在輸出時也能正確地“轉換”(預設情況下)。
提供了一個新的建構函式,允許您配置轉換器以像以前一樣工作。以下列表顯示了該方法的 Javadoc 註釋和宣告
/**
* 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) { ... }
同樣從版本 1.6 開始,MessageProperties 中添加了一個名為 correlationIdString 的新屬性。以前,在與 RabbitMQ 客戶端使用的 BasicProperties 之間進行轉換時,執行了不必要的 byte[] <→ String 轉換,因為 MessageProperties.correlationId 是 byte[],但 BasicProperties 使用 String。(最終,RabbitMQ 客戶端使用 UTF-8 將 String 轉換為位元組以放入協議訊息中)。
為了提供最大的向後相容性,已向 DefaultMessagePropertiesConverter 添加了一個名為 correlationIdPolicy 的新屬性。它接受 DefaultMessagePropertiesConverter.CorrelationIdPolicy 列舉引數。預設情況下,它設定為 BYTES,這複製了以前的行為。
對於入站訊息
-
STRING:僅對映correlationIdString屬性 -
BYTES:僅對映correlationId屬性 -
BOTH:對映兩個屬性
對於出站訊息
-
STRING:僅對映correlationIdString屬性 -
BYTES:僅對映correlationId屬性 -
BOTH:同時考慮兩個屬性,其中String屬性優先
同樣從版本 1.6 開始,入站 deliveryMode 屬性不再對映到 MessageProperties.deliveryMode。它改為對映到 MessageProperties.receivedDeliveryMode。此外,入站 userId 屬性不再對映到 MessageProperties.userId。它改為對映到 MessageProperties.receivedUserId。這些更改是為了避免在同一 MessageProperties 物件用於出站訊息時這些屬性的意外傳播。
從 2.2 版本開始,DefaultMessagePropertiesConverter 使用 getName() 而不是 toString() 轉換型別為 Class<?> 的任何自定義頭部值;這避免了消費應用程式必須從 toString() 表示中解析類名。對於滾動升級,您可能需要更改消費者以理解兩種格式,直到所有生產者都升級。