訊息轉換器

AmqpTemplate 也定義了數個用於傳送和接收訊息的方法,這些方法委派給 MessageConverterMessageConverter 為每個方向提供單一方法:一個用於轉換 Message,另一個用於 Message 轉換。 請注意,在轉換為 Message 時,除了物件之外,您還可以提供屬性。 object 參數通常對應於訊息主體。 以下清單顯示 MessageConverter 介面定義

public interface MessageConverter {

    Message toMessage(Object object, MessageProperties messageProperties)
            throws MessageConversionException;

    Object fromMessage(Message message) throws MessageConversionException;

}

AmqpTemplate 上相關的 Message 傳送方法比我們先前討論的方法更簡單,因為它們不需要 Message 實例。 相反地,MessageConverter 負責「建立」每個 Message,方法是將提供的物件轉換為 Message 主體的位元組陣列,然後新增任何提供的 MessageProperties。 以下清單顯示各種方法的定義

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;

在接收端,只有兩種方法:一種接受佇列名稱,另一種依賴於範本的「queue」屬性已設定。 以下清單顯示這兩種方法的定義

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 反序列化 以取得重要資訊。

轉換為 Message

當從任意 Java 物件轉換為 Message 時,SimpleMessageConverter 同樣處理位元組陣列、字串和可序列化實例。 它將這些物件的每一個都轉換為位元組 (對於位元組陣列,沒有任何轉換),並相應地設定 content-type 屬性。 如果要轉換的 Object 與其中一種型別不符,則 Message 主體為 null。

SerializerMessageConverter

此轉換器與 SimpleMessageConverter 類似,不同之處在於它可以設定其他 Spring Framework SerializerDeserializer 實作,以用於 application/x-java-serialized-object 轉換。

請參閱 Java 反序列化 以取得重要資訊。

Jackson2JsonMessageConverter

本節涵蓋如何使用 Jackson2JsonMessageConverter 來回轉換 Message。 它包含以下章節

轉換為 Message

如上一節所述,通常不建議依賴 Java 序列化。 一種更常見的替代方案是 JSON (JavaScript Object Notation),它在不同的語言和平台之間更具彈性和可攜性。 可以在任何 RabbitTemplate 實例上設定轉換器,以覆寫其 SimpleMessageConverter 預設值的使用。 Jackson2JsonMessageConverter 使用 com.fasterxml.jackson 2.x 程式庫。 以下範例設定 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>

如上所示,Jackson2JsonMessageConverter 預設使用 DefaultClassMapper。 型別資訊會新增至 (和從) MessageProperties 擷取。 如果輸入訊息在 MessageProperties 中不包含型別資訊,但您知道預期的型別,則可以使用 defaultType 屬性來設定靜態型別,如下列範例所示

<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>

此外,您可以提供從 TypeId 標頭中的值到自訂對應。 以下範例示範如何執行此操作

@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;
}

現在,如果傳送系統將標頭設定為 thing1,則轉換器會建立 Thing1 物件,依此類推。 請參閱 從非 Spring 應用程式接收 JSON 範例應用程式,以取得關於從非 Spring 應用程式轉換訊息的完整討論。

從 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 的子類別 (它是具體的)。 推斷的型別將不正確。 若要處理這種情況,請將 Jackson2JsonMessageConverter 上的 TypePrecedence 屬性設定為 TYPE_ID,而不是預設的 INFERRED。 (該屬性實際上位於轉換器的 DefaultJackson2JavaTypeMapper 上,但在轉換器上提供了一個 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[]。 因此,為了滿足消費者端的 Jackson2JsonMessageConverter 需求,生產者必須新增 contentType 訊息屬性 — 例如,作為 application/jsontext/x-json,或使用 Jackson2JsonMessageConverter,它會自動設定標頭。 以下清單顯示一些轉換器呼叫
@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 版開始,Jackson2JsonMessageConverter,因此,DefaultJackson2JavaTypeMapper (DefaultClassMapper) 提供 trustedPackages 選項以克服 Serialization Gadgets 漏洞。 預設情況下,為了向後相容性,Jackson2JsonMessageConverter 信任所有套件 — 也就是說,它使用 * 作為選項。

從 2.4.7 版開始,如果 Jackson 在還原序列化訊息主體後傳回 null,則可以將轉換器設定為傳回 Optional.empty()。 這有助於 @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
Jackson2JsonMessageConverter converter() {
    Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
    converter.setNullAsOptionalEmpty(true);
    return converter;
}

還原序列化抽象類別

在 2.2.8 之前的版本中,如果 @RabbitListener 的推斷型別是抽象類別 (包括介面),則轉換器會退回到在標頭中尋找型別資訊,如果存在,則使用該資訊;如果不存在,它會嘗試建立抽象類別。 當使用自訂 ObjectMapper (已設定自訂還原序列化器以處理抽象類別) 時,但傳入訊息具有無效的型別標頭時,這會導致問題。

從 2.2.8 版開始,預設情況下會保留先前的行為。 如果您有此類自訂 ObjectMapper,並且想要忽略型別標頭,並始終使用推斷的型別進行轉換,請將 alwaysConvertToInferredType 設定為 true。 為了向後相容性,並避免在轉換失敗時 (使用標準 ObjectMapper) 嘗試轉換的額外負荷,這是必要的。

使用 Spring Data Projection 介面

從 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-commonscom.jayway.jsonpath:json-path 新增至類別路徑。

當用作 @RabbitListener 方法的參數時,介面型別會自動正常傳遞至轉換器。

使用 RabbitTemplateMessage 轉換

如先前所述,型別資訊在訊息標頭中傳達,以協助轉換器從訊息轉換時。 這在大多數情況下都能正常運作。 但是,當使用泛型型別時,它只能轉換簡單物件和已知的「容器」物件 (清單、陣列和 Map)。 從 2.0 版開始,Jackson2JsonMessageConverter 實作 SmartMessageConverter,這使其可以與採用 ParameterizedTypeReference 引數的新 RabbitTemplate 方法一起使用。 這允許轉換複雜的泛型型別,如下列範例所示

Thing1<Thing2<Cat, Hat>> thing1 =
    rabbitTemplate.receiveAndConvert(new ParameterizedTypeReference<Thing1<Thing2<Cat, Hat>>>() { });
從 2.1 版開始,已移除 AbstractJsonMessageConverter 類別。 它不再是 Jackson2JsonMessageConverter 的基底類別。 它已由 AbstractJackson2MessageConverter 取代。

MarshallingMessageConverter

另一個選項是 MarshallingMessageConverter。 它委派給 Spring OXM 程式庫的 MarshallerUnmarshaller 策略介面的實作。 您可以在 這裡 閱讀有關該程式庫的更多資訊。 就設定而言,最常見的做法是僅提供建構函式引數,因為 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="someImplemenationOfMarshallerAndUnmarshaller"/>
        </bean>
    </property>
</bean>

Jackson2XmlMessageConverter

此類別在 2.1 版中引入,可用於來回轉換 XML 訊息。

Jackson2XmlMessageConverterJackson2JsonMessageConverter 都具有相同的基底類別:AbstractJackson2MessageConverter

引入 AbstractJackson2MessageConverter 類別是為了取代已移除的類別:AbstractJsonMessageConverter

Jackson2XmlMessageConverter 使用 com.fasterxml.jackson 2.x 程式庫。

您可以使用它,方式與 Jackson2JsonMessageConverter 相同,但它支援 XML 而不是 JSON。 以下範例設定 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>

請參閱 Jackson2JsonMessageConverter 以取得更多資訊。

從 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 物件時,可能存在漏洞。

如果您接受來自不受信任來源且 content-typeapplication/x-java-serialized-object 的訊息,您應考慮設定允許還原序列化的套件和類別。 當 SimpleMessageConverterSerializerMessageConverter 設定為隱含或透過設定使用 DefaultDeserializer 時,這兩者都適用。

預設情況下,允許清單為空,表示不會還原序列化任何類別。

您可以設定模式清單,例如 thing1., thing1.thing2.Cat.MySafeClass

會依序檢查模式,直到找到相符項為止。 如果沒有相符項,則會擲回 SecurityException

您可以使用這些轉換器上的 allowedListPatterns 屬性來設定模式。 或者,如果您信任所有訊息來源,您可以將環境變數 SPRING_AMQP_DESERIALIZATION_TRUST_ALL 或系統屬性 spring.amqp.deserialization.trust.all 設定為 true

訊息屬性轉換器

MessagePropertiesConverter 策略介面用於在 Rabbit Client BasicProperties 和 Spring AMQP MessageProperties 之間進行轉換。 預設實作 (DefaultMessagePropertiesConverter) 通常足以滿足大多數用途,但如果需要,您可以實作自己的實作。 當大小不大於 1024 位元組時,預設屬性轉換器會將 LongString 型別的 BasicProperties 元素轉換為 String 實例。 較大的 LongString 實例不會轉換 (請參閱下一段)。 此限制可以使用建構函式引數覆寫。

從 1.6 版開始,DefaultMessagePropertiesConverter 預設現在將長度超過長字串限制 (預設值:1024) 的標頭保留為 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 版開始,已將名為 correlationIdString 的新屬性新增至 MessageProperties。 先前,在來回轉換 RabbitMQ 用戶端使用的 BasicProperties 時,執行了不必要的 byte[] <→ String 轉換,因為 MessageProperties.correlationIdbyte[],但 BasicProperties 使用 String。 (最終,RabbitMQ 用戶端使用 UTF-8 將 String 轉換為位元組,以放入協定訊息中)。

為了提供最大的向後相容性,已將名為 correlationIdPolicy 的新屬性新增至 DefaultMessagePropertiesConverter。 這採用 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() 表示法中剖析類別名稱。 對於滾動升級,您可能需要變更您的消費者以了解兩種格式,直到所有生產者都升級。