轉換器

訊息轉換器在實現訊息生產者和訊息消費者的松耦合方面扮演著非常重要的角色。無需要求每個訊息生產元件都知道下一個消費者期望什麼型別,您可以在這些元件之間新增轉換器。通用轉換器(例如將 String 轉換為 XML Document 的轉換器)也具有高度的可重用性。

對於某些系統而言,提供規範資料模型可能是最佳選擇,但 Spring Integration 的一般理念是不要求特定的格式。相反,為了實現最大的靈活性,Spring Integration 旨在提供最簡單的擴充套件模型。與其他端點型別一樣,在 XML 或 Java 註解中使用宣告式配置可以將簡單的 POJO 適配為訊息轉換器的角色。本章的其餘部分將介紹這些配置選項。

為了最大限度地提高靈活性,Spring 不要求基於 XML 的訊息負載。儘管如此,如果基於 XML 的負載確實是您應用程式的正確選擇,該框架確實提供了一些方便的轉換器來處理它們。有關這些轉換器的更多資訊,請參閱XML 支援 - 處理 XML Payload

使用 Java 和其他 DSL 配置轉換器

對於簡單的 Java 和註解配置,Spring bean POJO 方法必須使用 @Transformer 註解標記,當從輸入通道消費訊息時,框架會呼叫它

public class SomeService {

    @Transformer(inputChannel = "transformChannel", outputChannel = "nextServiceChannel")
    public OutputData exampleTransformer(InputData payload) {
        ...
    }

}

有關更多資訊,請參閱註解支援

對於 Java、Groovy 或 Kotlin DSL,IntegrationFlow.transform() 運算子表示一個轉換器端點

  • Java DSL

  • Kotlin DSL

  • Groovy DSL

@Bean
public IntegrationFlow someFlow() {
    return IntegrationFlow
             .from("transformChannel")
             .transform(someService, "exampleTransformer")
             .channel("nextServiceChannel")
             .get();
}
@Bean
fun someFlow() =
    integrationFlow("transformChannel") {
        transform(someService, "exampleTransformer")
        channel("nextServiceChannel")
    }
@Bean
someFlow() {
    integrationFlow 'transformChannel',
            {
                transform someService, 'exampleTransformer'
                channel 'nextServiceChannel'
            }
}

有關 DSL 的更多資訊,請參閱相應的章節

使用 XML 配置轉換器

<transformer> 元素用於建立訊息轉換端點。除了 input-channeloutput-channel 屬性外,它還需要一個 ref 屬性。ref 可以指向包含單個方法上帶有 @Transformer 註解的物件(參見使用註解配置轉換器),也可以與 method 屬性中提供的顯式方法名值結合使用。

<int:transformer id="testTransformer" ref="testTransformerBean" input-channel="inChannel"
             method="transform" output-channel="outChannel"/>
<beans:bean id="testTransformerBean" class="org.foo.TestTransformer" />

如果自定義轉換器處理程式實現可以在其他 <transformer> 定義中重用,通常建議使用 ref 屬性。但是,如果自定義轉換器處理程式實現應限於單個 <transformer> 定義,您可以定義內部 bean 定義,如下例所示

<int:transformer id="testTransformer" input-channel="inChannel" method="transform"
                output-channel="outChannel">
  <beans:bean class="org.foo.TestTransformer"/>
</transformer>
在同一個 <transformer> 配置中同時使用 ref 屬性和內部處理程式定義是不允許的,因為它會建立歧義條件並導致丟擲異常。
如果 ref 屬性引用了擴充套件 AbstractMessageProducingHandler 的 bean(例如框架本身提供的轉換器),則透過將輸出通道直接注入處理程式來最佳化配置。在這種情況下,每個 ref 必須指向一個單獨的 bean 例項(或 prototype 作用域的 bean),或者使用內部 <bean/> 配置型別。如果不小心從多個 bean 引用了同一個訊息處理程式,您將收到配置異常。

使用 POJO 時,用於轉換的方法可以期望 Message 型別或入站訊息的 payload 型別。它還可以分別使用 @Header@Headers 引數註解來接受單個訊息頭值或完整的 Map。方法的返回值可以是任何型別。如果返回值本身是 Message,則將其傳遞到轉換器的輸出通道。

自 Spring Integration 2.0 起,訊息轉換器的轉換方法不能再返回 null。返回 null 會導致異常,因為訊息轉換器應始終期望將每個源訊息轉換為有效的目標訊息。換句話說,不應將訊息轉換器用作訊息過濾器,因為為此提供了專用的 <filter> 選項。但是,如果您確實需要這種行為(元件可能返回 null 且不應視為錯誤),則可以使用服務啟用器。其 requires-reply 值預設為 false,但可以將其設定為 true,以便對 null 返回值丟擲異常,就像轉換器一樣。

轉換器與 Spring 表示式語言 (SpEL)

與路由器、聚合器和其他元件一樣,自 Spring Integration 2.0 起,當轉換邏輯相對簡單時,轉換器也可以受益於SpEL 支援。以下示例展示瞭如何使用 SpEL 表示式

<int:transformer input-channel="inChannel"
	output-channel="outChannel"
	expression="payload.toUpperCase() + '- [' + T(System).currentTimeMillis() + ']'"/>

上面的示例在不編寫自定義轉換器的情況下轉換了 payload。我們的 payload(假定為 String)被轉換為大寫,與當前時間戳連線,並應用了一些格式。

常用轉換器

Spring Integration 提供了一些轉換器實現。

Object-to-String 轉換器

由於使用 ObjecttoString() 表示形式相當常見,Spring Integration 提供了一個 ObjectToStringTransformer(另請參見 Transformers 工廠),其中輸出是一個帶有 String payloadMessage。該 String 是對入站 Message 的 payload 呼叫 toString() 操作的結果。以下示例展示瞭如何宣告 object-to-string 轉換器的例項

  • Java DSL

  • Kotlin DSL

  • Groovy DSL

  • XML

@Bean
public IntegrationFlow someFlow() {
    return IntegrationFlow
             .from("in")
             .transform(Transformers.objectToString())
             .channel("out")
             .get();
}
@Bean
fun someFlow() =
    integrationFlow("in") {
        transform(Transformers.objectToString())
        channel("out")
    }
@Bean
someFlow() {
    integrationFlow 'in',
            {
                transform Transformers.objectToString()
                channel 'out'
            }
}
<int:object-to-string-transformer input-channel="in" output-channel="out"/>

此轉換器的一種潛在用途是將任意物件傳送到 file 名稱空間中的 'outbound-channel-adapter'。由於該通道介面卡預設僅支援 String、byte-array 或 java.io.File payloads,因此在介面卡之前立即新增此轉換器可以處理必要的轉換。只要 toString() 呼叫的結果是您想要寫入檔案的資料,這種方法就可以正常工作。否則,您可以使用前面顯示的通用 'transformer' 元素提供基於 POJO 的自定義轉換器。

除錯時,通常不需要此轉換器,因為 logging-channel-adapter 能夠記錄訊息 payload。有關詳細資訊,請參見Wire Tap

object-to-string 轉換器非常簡單。它對入站 payload 呼叫 toString()。自 Spring Integration 3.0 起,此規則有兩個例外:

  • 如果 payload 是 char[],則呼叫 new String(payload)

  • 如果 payload 是 byte[],則呼叫 new String(payload, charset),其中 charset 預設為 UTF-8。可以透過在轉換器上提供 charset 屬性來修改 charset

對於更復雜的功能(例如在執行時動態選擇 charset),您可以改用基於 SpEL 表示式的轉換器,如下例所示

  • Java DSL

  • XML

@Bean
public IntegrationFlow someFlow() {
    return IntegrationFlow
             .from("in")
             .transform("new String(payload, headers['myCharset']")
             .channel("out")
             .get();
}
<int:transformer input-channel="in" output-channel="out"
       expression="new String(payload, headers['myCharset']" />

如果您需要將 Object 序列化為 byte 陣列或將 byte 陣列反序列化回 Object,Spring Integration 提供了對稱的序列化轉換器。這些轉換器預設使用標準的 Java 序列化,但您可以透過分別使用 serializerdeserializer 屬性來提供 Spring SerializerDeserializer 策略的實現。另請參見 Transformers 工廠類。以下示例展示瞭如何使用 Spring 的序列化器和反序列化器

  • Java DSL

  • XML

@Bean
public IntegrationFlow someFlow() {
    return IntegrationFlow
             .from("objectsIn")
             .transform(Transformers.serializer())
             .channel("bytesOut")
             .channel("bytesIn")
             .transform(Transformers.deserializer("com.mycom.*", "com.yourcom.*"))
             .channel("objectsOut")
             .get();
}
<int:payload-serializing-transformer input-channel="objectsIn" output-channel="bytesOut"/>

<int:payload-deserializing-transformer input-channel="bytesIn" output-channel="objectsOut"
    allow-list="com.mycom.*,com.yourcom.*"/>
從不受信任的源反序列化資料時,應考慮新增包和類模式的允許列表(allow-list)。預設情況下,所有類都被反序列化。

Object-to-MapMap-to-Object 轉換器

Spring Integration 還提供了 Object-to-MapMap-to-Object 轉換器,它們使用 JSON 來序列化和反序列化物件圖。物件層次結構被內省到最原始的型別(Stringint 等)。到達此型別的路徑用 SpEL 描述,它成為轉換後的 Map 中的 key。原始型別成為 value。

考慮以下示例

public class Parent{
    private Child child;
    private String name;
    // setters and getters are omitted
}

public class Child{
    private String name;
    private List<String> nickNames;
    // setters and getters are omitted
}

上例中的兩個類被轉換為以下 Map

{person.name=George, person.child.name=Jenna, person.child.nickNames[0]=Jen ...}

基於 JSON 的 Map 允許您在不共享實際型別的情況下描述物件結構,這使您能夠將物件圖恢復並重建為不同型別的物件圖,只要保持結構即可。

例如,透過使用 Map-to-Object 轉換器,可以將上述結構恢復回以下物件圖

public class Father {
    private Kid child;
    private String name;
    // setters and getters are omitted
}

public class Kid {
    private String name;
    private List<String> nickNames;
    // setters and getters are omitted
}

如果需要建立“結構化”的 map,可以提供 flatten 屬性。預設值為 'true'。如果將其設定為 'false',則結構是一個 Map 的 Map 物件。

考慮以下示例

public class Parent {
	private Child child;
	private String name;
	// setters and getters are omitted
}

public class Child {
	private String name;
	private List<String> nickNames;
	// setters and getters are omitted
}

上例中的兩個類被轉換為以下 Map

{name=George, child={name=Jenna, nickNames=[Bimbo, ...]}}

為了配置這些轉換器,Spring Integration 提供了相應的 XML 元件和 Java DSL 工廠

  • Java DSL

  • XML

@Bean
public IntegrationFlow someFlow() {
    return IntegrationFlow
             .from("directInput")
             .transform(Transformers.toMap())
             .channel("output")
             .get();
}
<int:object-to-map-transformer input-channel="directInput" output-channel="output"/>

您還可以將 flatten 屬性設定為 false,如下所示

  • Java DSL

  • XML

@Bean
public IntegrationFlow someFlow() {
    return IntegrationFlow
             .from("directInput")
             .transform(Transformers.toMap(false))
             .channel("output")
             .get();
}
<int:object-to-map-transformer input-channel="directInput" output-channel="output" flatten="false"/>

Spring Integration 為 Map-to-Object 提供了 XML 名稱空間支援,並且 Java DSL 工廠具有 fromMap() 方法,如下例所示

  • Java DSL

  • XML

@Bean
public IntegrationFlow someFlow() {
    return IntegrationFlow
             .from("input")
             .transform(Transformers.fromMap(org.something.Person.class))
             .channel("output")
             .get();
}
<int:map-to-object-transformer input-channel="input"
                         output-channel="output"
                         type="org.something.Person"/>

或者,您可以使用 ref 屬性和 prototype 作用域的 bean,如下例所示

  • Java DSL

  • XML

@Bean
IntegrationFlow someFlow() {
    return IntegrationFlow
             .from("inputA")
             .transform(Transformers.fromMap("person"))
             .channel("outputA")
             .get();
}

@Bean
@Scope("prototype")
Person person() {
    return new Person();
}
<int:map-to-object-transformer input-channel="inputA"
                               output-channel="outputA"
                               ref="person"/>
<bean id="person" class="org.something.Person" scope="prototype"/>
‘ref’ 和 ‘type’ 屬性是互斥的。此外,如果您使用 ‘ref’ 屬性,則必須指向一個 ‘prototype’ 作用域的 bean。否則,會丟擲 BeanCreationException

從 5.0 版本開始,您可以為 ObjectToMapTransformer 提供定製的 JsonObjectMapper — 當您需要特殊格式來處理日期或空集合的 null 值(以及其他用途)時。有關 JsonObjectMapper 實現的更多資訊,請參見JSON 轉換器

Stream 轉換器

StreamTransformerInputStream payloads 轉換為 byte[](如果提供了 charset,則轉換為 String)。

以下示例展示瞭如何在 XML 中使用 stream-transformer 元素

  • Java DSL

  • XML

@Bean
public IntegrationFlow someFlow() {
    return IntegrationFlow
             .from("input")
             .transform(Transformers.fromStream("UTF-8"))
             .channel("output")
             .get();
}
<int:stream-transformer input-channel="directInput" output-channel="output"/> <!-- byte[] -->

<int:stream-transformer id="withCharset" charset="UTF-8"
    input-channel="charsetChannel" output-channel="output"/> <!-- String -->

以下示例展示瞭如何使用 StreamTransformer 類和 @Transformer 註解在 Java 中配置 stream 轉換器

@Bean
@Transformer(inputChannel = "stream", outputChannel = "data")
public StreamTransformer streamToBytes() {
    return new StreamTransformer(); // transforms to byte[]
}

@Bean
@Transformer(inputChannel = "stream", outputChannel = "data")
public StreamTransformer streamToString() {
    return new StreamTransformer("UTF-8"); // transforms to String
}

JSON 轉換器

Spring Integration 提供了 Object-to-JSON 和 JSON-to-Object 轉換器。以下兩對示例展示瞭如何在 XML 中宣告它們

<int:object-to-json-transformer input-channel="objectMapperInput"/>

<int:json-to-object-transformer input-channel="objectMapperInput"
    type="foo.MyDomainObject"/>

預設情況下,上面列表中的轉換器使用普通的 JsonObjectMapper。它基於 classpath 中的實現。您可以提供自己的帶有適當選項或基於所需庫(如 GSON)的自定義 JsonObjectMapper 實現,如下例所示

<int:json-to-object-transformer input-channel="objectMapperInput"
    type="something.MyDomainObject" object-mapper="customObjectMapper"/>

從 3.0 版本開始,object-mapper 屬性引用了一個新的策略介面:JsonObjectMapper 的例項。這個抽象允許使用 JSON 對映器的多種實現。提供了封裝 Jackson 2 的實現,其版本在 classpath 中檢測。該類分別是 Jackson2JsonObjectMapper

您可能需要考慮使用 FactoryBean 或工廠方法來建立具有所需特性的 JsonObjectMapper。以下示例展示瞭如何使用這樣的工廠

public class ObjectMapperFactory {

    public static Jackson2JsonObjectMapper getMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
        return new Jackson2JsonObjectMapper(mapper);
    }
}

以下示例展示瞭如何在 XML 中執行相同的操作

<bean id="customObjectMapper" class="something.ObjectMapperFactory"
            factory-method="getMapper"/>

從 2.2 版本開始,如果輸入訊息尚未包含該頭部,object-to-json-transformer 會預設將 content-type 頭部設定為 application/json

如果您希望將 content-type 頭部設定為其他值,或顯式覆蓋現有頭部(包括 application/json),請使用 content-type 屬性。如果您希望抑制頭部的設定,請將 content-type 屬性設定為空字串("")。這樣做會生成一個沒有 content-type 頭部的訊息,除非輸入訊息中已存在該頭部。

從 3.0 版本開始,ObjectToJsonTransformer 會向訊息新增反映源型別的頭部。類似地,JsonToObjectTransformer 在將 JSON 轉換為物件時可以使用這些型別頭部。這些頭部在 AMQP 介面卡中進行對映,以便它們與 Spring-AMQP JsonMessageConverter 完全相容。

這使得以下流程無需任何特殊配置即可工作

  • …​→amqp-outbound-adapter---→

  • ---→amqp-inbound-adapter→json-to-object-transformer→…​

    其中 outbound 介面卡配置了 JsonMessageConverter,而 inbound 介面卡使用預設的 SimpleMessageConverter

  • …​→object-to-json-transformer→amqp-outbound-adapter---→

  • ---→amqp-inbound-adapter→…​

    其中 outbound 介面卡配置了 SimpleMessageConverter,而 inbound 介面卡使用預設的 JsonMessageConverter

  • …​→object-to-json-transformer→amqp-outbound-adapter---→

  • ---→amqp-inbound-adapter→json-to-object-transformer→

    其中兩個介面卡都配置了 SimpleMessageConverter

使用頭部確定型別時,不應提供 class 屬性,因為它優先於頭部。

除了 JSON 轉換器,Spring Integration 還提供了一個內建的 #jsonPath SpEL 函式用於表示式。有關更多資訊,請參閱Spring 表示式語言 (SpEL)

自 3.0 版本以來,Spring Integration 還提供了一個內建的 #xpath SpEL 函式用於表示式。有關更多資訊,請參閱#xpath SpEL 函式

從 4.0 版本開始,ObjectToJsonTransformer 支援 resultType 屬性,用於指定節點的 JSON 表示形式。結果節點樹的表示取決於所提供的 JsonObjectMapper 的實現。預設情況下,ObjectToJsonTransformer 使用 Jackson2JsonObjectMapper 並將物件轉換為節點樹的委託給 ObjectMapper#valueToTree 方法。當下遊訊息流使用 SpEL 表示式訪問 JSON 資料屬性時,節點 JSON 表示形式提高了使用 JsonPropertyAccessor 的效率。有關更多資訊,請參閱屬性訪問器

從 5.1 版本開始,resultType 可以配置為 BYTES,以便生成帶有 byte[] payload 的訊息,方便與處理此資料型別的下游處理程式一起工作。

從 5.2 版本開始,JsonToObjectTransformer 可以配置 ResolvableType,以在目標 JSON 處理器進行反序列化時支援泛型。此外,此元件現在首先檢查請求訊息頭部是否存在 JsonHeaders.RESOLVABLE_TYPEJsonHeaders.TYPE_ID,否則回退到配置的型別。ObjectToJsonTransformer 現在還會根據請求訊息 payload 填充 JsonHeaders.RESOLVABLE_TYPE 頭部,以應對任何可能的下游場景。

從 5.2.6 版本開始,JsonToObjectTransformer 可以提供 valueTypeExpression,用於在執行時根據請求訊息解析要從 JSON 轉換的 payload 的 ResolvableType。預設情況下,它會查詢請求訊息中的 JsonHeaders。如果此表示式返回 nullResolvableType 構建丟擲 ClassNotFoundException,則轉換器會回退到提供的 targetType。此邏輯作為表示式存在是因為 JsonHeaders 可能沒有實際的類值,而是需要根據外部登錄檔對映到目標類的一些型別 ID。

Apache Avro 轉換器

5.2 版本添加了簡單的轉換器用於與 Apache Avro 之間進行轉換。

它們不夠複雜,因為沒有 schema 登錄檔;轉換器只是使用從 Avro schema 生成的 SpecificRecord 實現中嵌入的 schema。

傳送到 SimpleToAvroTransformer 的訊息必須具有實現 SpecificRecord 的 payload;轉換器可以處理多種型別。SimpleFromAvroTransformer 必須配置 SpecificRecord 類,該類用作預設的反序列化型別。您還可以使用 setTypeExpression 方法指定一個 SpEL 表示式來確定反序列化的型別。預設的 SpEL 表示式是 headers[avro_type] (AvroHeaders.TYPE),SimpleToAvroTransformer 預設使用源類的完全限定類名來填充它。如果表示式返回 null,則使用 defaultType

SimpleToAvroTransformer 也有 setTypeExpression 方法。這允許生產者和消費者解耦,傳送者可以將頭部設定為表示型別的某個標記,然後消費者將該標記對映到型別。

Protocol Buffers 轉換器

6.1 版本增加了對與 Protocol Buffers 資料內容之間進行轉換的支援。

ToProtobufTransformercom.google.protobuf.Message 訊息 payloads 轉換為原生 byte 陣列或 json 文字 payloads。預設使用的 application/x-protobuf 內容型別會產生 byte 陣列輸出 payload。如果內容型別是 application/json 並且在 classpath 中找到 com.google.protobuf:protobuf-java-util,則輸出是文字 json payload。如果未設定 content type 頭部,ToProtobufTransformer 預設為 application/x-protobuf

FromProtobufTransformer 將位元組陣列或文字 protobuf 負載(取決於內容型別)轉換回 com.google.protobuf.Message 例項。FromProtobufTransformer 應該顯式指定一個預期的類型別(使用 setExpectedType 方法),或者使用 SpEL 表示式透過 setExpectedTypeExpression 方法確定要反序列化的型別。預設的 SpEL 表示式是 headers[proto_type] (ProtoHeaders.TYPE),它由 ToProtobufTransformer 填充了源 com.google.protobuf.Message 類的全限定類名。

例如,編譯以下 IDL

syntax = "proto2";
package tutorial;

option java_multiple_files = true;
option java_package = "org.example";
option java_outer_classname = "MyProtos";

message MyMessageClass {
  optional string foo = 1;
  optional string bar = 2;
}

將生成一個新的 org.example.MyMessageClass 類。

然後使用

// Transforms a MyMessageClass instance into a byte array.
ToProtobufTransformer toTransformer = new ToProtobufTransformer();

MyMessageClass test = MyMessageClass.newBuilder()
                                .setFoo("foo")
                                .setBar("bar")
                                .build();
// message1 payload is byte array protocol buffer wire format.
Message message1 = toTransformer.transform(new GenericMessage<>(test));

// Transforms a byte array payload into a MyMessageClass instance.
FromProtobufTransformer fromTransformer = new FromProtobufTransformer();

// message2 payload == test
Message message2 =  fromTransformer.transform(message1);

使用註解配置轉換器

您可以將 @Transformer 註解新增到期望接收 Message 型別或訊息負載型別的方法上。返回值的處理方式與前面描述 <transformer> 元素的章節中描述的完全相同。以下示例展示瞭如何使用 @Transformer 註解將一個 String 轉換為一個 Order

@Transformer
Order generateOrder(String productId) {
    return new Order(productId);
}

轉換器方法也可以接受 @Header@Headers 註解,正如 註解支援 中所記錄的。以下示例展示瞭如何使用 @Header 註解

@Transformer
Order generateOrder(String productId, @Header("customerName") String customer) {
    return new Order(productId, customer);
}

另請參閱 使用註解增強端點

頭部過濾器

有時,您的轉換用例可能就像移除幾個頭部一樣簡單。對於這樣的用例,Spring Integration 提供了一個頭部過濾器,讓您可以指定應該從輸出訊息中移除的特定頭部名稱(例如,出於安全原因或只需要臨時使用的值而移除頭部)。基本上,頭部過濾器與頭部豐富器相反。後者在 頭部豐富器 中討論。以下示例定義了一個頭部過濾器

  • Java DSL

  • XML

@Bean
public IntegrationFlow someFlow() {
    return IntegrationFlow
             .from("inputChannel")
             .headerFilter("lastName", "state")
             .channel("outputChannel")
             .get();
}
<int:header-filter input-channel="inputChannel"
		output-channel="outputChannel" header-names="lastName, state"/>

如您所見,頭部過濾器的配置非常簡單。它是一個典型的端點,帶有輸入和輸出通道以及一個 header-names 屬性。該屬性接受需要被移除的頭部名稱(如果多個則用逗號分隔)。因此,在前面的示例中,名為 ‘lastName’ 和 ‘state’ 的頭部不會出現在出站訊息中。

基於 Codec 的轉換器

參見 Codec