今天我們要分享一個比較有意思的內容。就是如何通過spring cloud 的stream來改造一個微服務下事件驅動的框架。
爲什麼要改造?我們都知道事件驅動的微服務開發框架,一個非常重要的點就是每次的操作和狀態轉換都是一個事件。而現在的spring cloud stream對這樣的頻繁而不同類型的事件並不是很友好。本文希望通過改造讓cloud stream變成一個對事件驅動的微服務開發更友好更方便的事件驅動框架。
準備工作
我們還是通過spring initializr來新建一個項目吧:
如上,我們引入了web、stream kafka依賴。
然後生成項目並下載,打開項目開始我們的改造之旅吧。
然後我們來看看現在的spring cloud的版本:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Camden.SR6</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
現在的版本是Camden.SR6。而我們今天要演示的stream是最新版本,所以我們得把cloud版本修改爲Brixton.SR7:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Brixton.SR7</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
你也許會在一些事件源框架中,比如Axon中,看到以下類似代碼:
public class MyEventHandler {
@EventHandler
public void handle(CustomerCreatedEvent event) {
...
}
@EventHandler
public void handle(AccountCreatedEvent event) {
...
}
}
沒錯,這其實就是事件源框架中最終所呈現出來的入口最核心的樣子。
現在我們對spring cloud stream進行改造,讓它變成一個真正的或者說像Axon那樣的一個事件源框架。
Cloud Stream 現有處理事件的做法
在開始真正的改造之前,我們還是先看看spring cloud stream 1.1.2(也就是cloud版本爲Camden.SR中的stream版本) 中的消息處理的基本樣子:
@StreamListener(Sink.INPUT)
public void handle(Foo foo){
...
}
沒錯,就是一個通過@StreamListener註解的handle方法,就可監聽到消息流了。
然後我們看看使用最新的Brixton.SR7版本spring cloud stream的樣子:
@EnableBinding
class MyEventHandler {
@StreamListener
target=Sink.INPUT,condition="payload.eventType=='CustomerCreatedEvent'")
public void handleCustomerEvent(@Payload Event event) {
// handle the message</span>
}
@StreamListener
target=Sink.INPUT,condition="payload.eventType=='AccountCreatedEvent'")
public void handleAccountEvent(@Payload Event event) {
// handle the message</span>
}
}
通過上面的代碼,我們知道spring cloud stream可以支持配置一個condition的屬性來讓不同的事件類型路由到不同的handle方法中來處理。其中condition裏邊的表達式叫做SpEL,就是spring 表達式,通過返回true或false來表示是否匹配。
另外上面的支持是4天前才發佈的。也許就是爲了支持最近炒得火熱的CQRS+ES而發佈的。
之前@StreamListener的源碼是這樣的:
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@MessageMapping
@Documented
public @interface StreamListener {
/**
* The name of the binding target (e.g. channel) that the method subscribes to.
*/
String value() default "";
}
發現沒?只有一個value方法屬性。
而最新的已經增加了condition條件。這顯然是爲了支持事件驅動的微服務開發而支持的。
我們點進去看看StreamListener新增加了什麼:
發現新增了兩個方法屬性,一個是target,一個是condition。
而且描述也變成了含有“事件驅動”字樣。
ok,現在我們已經知道了spring cloud stream的基本用法和代碼樣子。
最新版的做法已經算是一種不錯的改進了。不過,從編程的語法上,它也許並沒有我們想要的那麼清晰。當然這只是一種個人的喜好,抑或是我們希望把改造成像Axon那樣。
自定義註解
這裏我們希望把spring cloud stream改造成一個像Axon那樣的風格。因爲這也許對於CQRS + ES 框架來說是一種比較理想的開發入口。
像下面這樣:
@EnableEventHandling
class MyEventHandler {
@EventHandler("CustomerCreatedEvent")
public void handleCustomerEvent(@Payload Event event) {
// handle the message
}
@EventHandler("AccountCreatedEvent")
public void handleAccountEvent(@Payload Event event) {
// handle the message
}
}
我們既然想要上面的樣子,那麼就得新定義上面的這兩個註解。
我們首先來封裝一個@EventHandler註解吧:
@StreamListener
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface EventHandler {
@AliasFor(annotation = StreamListener.class, attribute = "target")
String value() default "";
@AliasFor(annotation = StreamListener.class, attribute = "target")
String target() default Sink.INPUT;
@AliasFor(annotation = StreamListener.class, attribute = "condition")
String condition() default "";
}
我們把@StreamListener封裝上面的註解內。
現在已經很接近了我們上面想要的樣子了:
@EnableBinding
class MyEventHandler{
@EventHandler(condition="payload.eventType=='CustomerCreatedEvent'")
public void handleCustomerEvent(@Payload Event event) {
// handle the message
}
@EventHandler(condition="payload.eventType=='AccountCreatedEvent'")
public void handleAccountEvent(@Payload Event event) {
// handle the message
}
}
但,@EnableEventHandling這個註解還沒有定義。現在我們來定義這個註解:
我們先來搞一個配置類(可橫屏觀看,排版效果更好):
@Configuration
public class EventHandlerConfig {
/*
* 用於允許spring cloud stream binder把事件路由到匹配的方法上的SpEL表達式
*/
private String eventHandlerSpelPattern = "payload.eventType=='%s'";
/**
* 在此bean中自定義processor,然後把eventType屬性轉成condition表達式
* @return
*/
@Bean(name = STREAM_LISTENER_ANNOTATION_BEAN_POST_PROCESSOR_NAME)
public BeanPostProcessor streamListenerAnnotationBeanPostProcessor() {
return new StreamListenerAnnotationBeanPostProcessor() {
@Override
protected StreamListener postProcessAnnotation(StreamListener originalAnnotation, Method annotatedMethod) {
Map<String, Object> attributes = new HashMap<>(
AnnotationUtils.getAnnotationAttributes(originalAnnotation));
if (StringUtils.hasText(originalAnnotation.condition())) {
String spelExpression = String.format(eventHandlerSpelPattern, originalAnnotation.condition());
attributes.put("condition", spelExpression);
}
return AnnotationUtils.synthesizeAnnotation(attributes, StreamListener.class, annotatedMethod);
}
};
}
}
然後我們,再新建@EnableEventHandling註解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EnableBinding
@Import({EventHandlerConfig.class})
public @interface EnableEventHandling {
}
上面的註解我們只是把剛纔的那個配置類import即可。
你也許發現了,其實spring boot中的很多類似@EnableXXXX的註解其實都是一個框架預定義好的配置類,然後在@EnableXXXX的中通過@Import註解導入就好了。本質上是一個配置類。
最後我們再把@EventHandler註解修改一下,把condition修改成eventType作爲condition的一個別名:
@StreamListener
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface EventHandler {
/**
* 方法所訂閱的channel的名稱
* @return 綁定目標的名稱
*/
@AliasFor(annotation=StreamListener.class, attribute="condition")
String value() default "";
/**
* 方法所訂閱對的channel的名稱
* @return 綁定目標的名稱
*/
@AliasFor(annotation=StreamListener.class, attribute="target")
String target() default Sink.INPUT;
/**
* 對 condition的封裝
* @return SpEL 的表達式
*/
@AliasFor(annotation=StreamListener.class, attribute="condition")
String eventType() default "";
}
總結
通過上面一系列的spring算是“奇技淫巧”我們愣是把spring cloud stream改造成了一個CQRS和EventSourcing那樣的事件驅動的全新框架。
@EnableEventHandling
class MyEventHandler{
@EventHandler("CustomerCreatedEvent")
public void handleCustomerEvent(@Payload Event event) {
// handle the message
}
@EventHandler("AccountCreatedEvent")
public void handleAccountEvent(@Payload Event event) {
// handle the message
}
}
上面改造的技術核心其實就是利用@EnableXxx的一貫做法,自定義註解。然後import一個configuration。然後configuration類中則實例化並註冊一個
自定義BeanPostProcessor到context中。而這個自定義的BeanPostProcessor則是在postProcessAnnotation方法中攔截到使用@Import的當前註解@StreamListener,然後動態把要設置到轉化後設置進去,從而實現了改造。
爲什麼要改造?我們都知道事件驅動的微服務開發框架,一個非常重要的點就是每次都操作和狀態轉換都是一個事件。而現在的spring cloud stream對這樣的頻繁而不同類型的事件並不是很友好。通過改造後,開發事件驅動的微服務就變得更加的方便和友好。
本文只是對Spring Cloud Stream的入口做了一個簡單的封裝,並沒有大動任何內部代碼。也許你並不喜歡這樣的風格。你完全可以使用最新的那種基於SpEL的默認做法。
另外有關CQRS以及Event Sourcing的內容,你可以移步:微服務業務開發三個難題-拆分、事務、查詢(上)、微服務業務開發三個難題-拆分、事務、查詢(下)。