針對事件驅動架構的Spring Cloud Stream

今天我們要分享一個比較有意思的內容。就是如何通過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的內容,你可以移步:微服務業務開發三個難題-拆分、事務、查詢(上)微服務業務開發三個難題-拆分、事務、查詢(下)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章