Stream進階篇-實體對象數據的生產消費與轉換

前言
在各微服務通信中,主要以json字符串作爲內容載體,在實際業務中,通常會自定義實體亦或是構造Map、List等數據作爲生產消費對象,SpringBoot通過對MessageConvertor的實現,已經很好的協助我們完成了Json與對象的轉換過程,在MVC的REST-API部分已經被充分應用。那麼在Stream中呢,是否還需要我們每次硬編碼實現呢,答案是否定的。本章節將實現驗證Stream對象數據的處理。

本章概要
1、支持Integration原生實現;
2、Spring Cloud Stream對象數據處理;

支持Integration原生實現
既然Spring Cloud Stream是Spring Integration+Spring Boot的整合,那麼必然也會支持Integration原生應用,本小節將介紹如何在Spring Cloud Stream應用下實現Integration原生應用。

1、繼續在MySink中定義一個接收通道,僅僅需要一個名稱:
package com.cloud.shf.stream.sink;
public interface MySink {
    //原生通道
    String ORIGINAL_CHANNEL = "original-channel";
}
Note:
  • 僅僅定義一個名稱,無需定義@Input註解的方法;

2、定義一個生產者,其採用輪詢方式發送消息,消息體爲Date數據:
package com.cloud.shf.stream.source;
@EnableBinding
public class TimerSource {
    @Bean
    @InboundChannelAdapter(value = MySink.ORIGINAL_CHANNEL, poller = @Poller(fixedRate = "4000", maxMessagesPerPoll = "1"))
    public MessageSource<Date> timerMessageSource() {
        return () -> new GenericMessage<>(new Date());
    }
}
Note:
  • 採用@EnableBinding結合@InboundChannelAdapter註解實現消息的發送;

3、接收端通過@ServiceActivator註解實現:
/*********************************Integration 原生實現******************************/
@ServiceActivator(inputChannel = MySink.ORIGINAL_CHANNEL)
public void originalReceiver(Object payload) {
    LOGGER.info("Received from {} channel : {}", MySink.ORIGINAL_CHANNEL, payload.toString());
}
4、此時啓動服務,可以看到控制檯打印如下消息:

Note:
  • 通過原生的註解能夠正常生產和消費數據;

5、如果希望對接收的Date類型數據進行格式化,需要通過@Transformer註解實現,其將對指定通道的消息體進行處理,如下對日期數據格式化:
@Transformer(inputChannel = MySink.ORIGINAL_CHANNEL, outputChannel = MySink.ORIGINAL_CHANNEL)
public Object transform(Date message) {
    return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(message);
}
6、此時再次啓動服務,可以看到控制檯消息如下:
數據格式化成功,並被最終接收打印。

小節:Spring Cloud Stream通過@EnableBinding結合@InboundChannelAdapter、@ServiceActivator即可實現Integration原生應用,在消息接收端,通過@Transformer註解方法,即可實現對數據的轉換、格式化等處理。


Spring Cloud Stream對象數據處理
通過上一小節,可以看到Integration原生應用中,生產和消費間的數據轉換需要一定的代碼成本,通過@Transformer註解方法可以實現指定通道的數據處理。這在Cloud Stream中得到改善,無需類似的硬編碼即可完成數據轉換。通過發出消息時,會包含一個String類型的payload和含有contentType的Header信息。@StreamListener能夠根據發送消息時的頭信息contentType自動完成轉換處理。

1、在MySink中添加如下消息通道,並定義其消息接收方法:
package com.cloud.shf.stream.sink;
public interface MySink {
    /*********************************User對象處理實現******************************/
    String USER_CHANNEL = "user-channel";

    @Input(USER_CHANNEL)
    SubscribableChannel userInput();
}
2、定義一個User實體,作爲後續的消息體實體類型:
package com.cloud.shf.stream.sink.entity;
public class User implements Serializable {
    private static final long serialVersionUID = 695183437612916152L;
    private String username;
    private int age;

    public String getUsername() {
        return username;
    }

    public User setUsername(String username) {
        this.username = username;
        return this;
    }

    public int getAge() {
        return age;
    }

    public User setAge(int age) {
        this.age = age;
        return this;
    }
}
3、SinkReceiver添加如下對監聽:
/*********************************User對象處理實現******************************/
@StreamListener(value = MySink.USER_CHANNEL)
public void userReceive(@Payload User user, @Headers Map headers) {
    LOGGER.info(headers.get("contentType").toString());
    LOGGER.info("Received from {} channel username: {}", MySink.USER_CHANNEL, user.getUsername());
}
Note:
  • @Payload註解作用於接收的消息體;
  • @Headers註解獲取消息頭信息,其爲一個Map鍵值對;

4、啓動服務;

5、編寫單元測試:
@RunWith(SpringRunner.class)
@EnableBinding(value = {
        ReceiverAppTest.UserSender.class})
public class ReceiverAppTest {
    @Autowired
    private UserSender userSender;

    @Test
    public void myUserSenderTester() {
        User user = new User().setUsername("shuaishuai").setAge(12);
        userSender.userChannelSender().send(MessageBuilder.withPayload(user).build());
    }

    public interface UserSender {
        @Output(MySink.USER_CHANNEL)
        MessageChannel userChannelSender();
    }
}
6、執行單元測試,控制檯打印如下:
Note:
  • 可以看到當前的contentType信息爲application/x-java-object;type=com.cloud.shf.stream.sink.entity.User
  • 能夠接收user實體消息,轉換成功;

7、第5步驟中,直接將user作爲消息體發出,下面直接將user對應的json字符串作爲消息體發出,單元測試調整如下:
@Test
public void myUserSenderTester() {
User user = new User().setUsername("shuaishuai").setAge(12);
userSender.userChannelSender().send(MessageBuilder.withPayload(ReflectionToStringBuilder.toString(user, ToStringStyle.JSON_STYLE))
.build());
}
執行結果如下:
可以看到消息發出方(生產者)對應的contentType並不一樣,接收方(消費方)均能夠轉換爲User對象並獲取其屬性值。

8、對於MessageConvertor官方有很詳細的描述,並且支持自定義MessageConvertor實現,其轉換過程主要還是取決於contentType的定義,如下爲詳細MIME和Java類型的轉換關係表:
Source Payload
Target Payload
content-type header (source message)
content-type header (after conversion)
Comments
POJO
JSON String
ignored
application/json
 
Tuple
JSON String
ignored
application/json
JSON is tailored for Tuple
POJO
String (toString())
ignored
text/plain, java.lang.String
 
POJO
byte[] (java.io serialized)
ignored
application/x-java-serialized-object
 
JSON byte[] or String
POJO
application/json (or none)
application/x-java-object
 
byte[] or String
Serializable
application/x-java-serialized-object
application/x-java-object
 
JSON byte[] or String
Tuple
application/json (or none)
application/x-spring-tuple
 
byte[]
String
any
text/plain, java.lang.String
will apply any Charset specified in the content-type header
String
byte[]
any
application/octet-stream
will apply any Charset specified in the content-type header

小節:通過上述示例,在Spring Cloud Stream應用下,幾乎無需我們做任何處理即可完成對象POJO的傳遞,一切來得那麼無感,這也是相比Integration原生的優勢。


總結
@ServiceActivator@StreamListener都能夠對消息通道進行監聽,但@StreamListener提供了諸多消息轉換模型,其依賴於MessageConvertor的定義,無需通過@Transformer註解方法硬編碼轉換。還能通過類似spring.cloud.stream.bindings.user-channel.contentType=application/json配置協助@StreamListener註解選擇消息轉換器。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章