淺析 Spring 中的事件驅動機制">淺析 Spring 中的事件驅動機制

分類:IT技術 時間:2017-09-30

今天來簡單地聊聊事件驅動,其實寫這篇文章挺令我挺苦惱的,因為事件驅動這個名詞,我沒有找到很好的定性解釋,擔心自己的表述有誤,而說到事件驅動可能立刻聯想到如此眾多的概念:觀察者模式,發布訂閱模式,消息隊列MQ,消息驅動,事件,EventSourcing…為了不產生歧義,筆者把自己所了解的這些模棱兩可的概念都列了出來,再開始今天的分享。

  • 設計模式 中,觀察者模式可以算得上是一個非常經典的行為型設計模式,貓叫了,主人醒了,老鼠跑了,這一經典的例子,是事件驅動模型在設計層面的體現。
  • 另一模式,發布訂閱模式往往被人們等同於觀察者模式,但我的理解是兩者唯一區別,是發布訂閱模式需要有一個調度中心,而觀察者模式不需要,例如觀察者的列表可以直接由被觀察者維護。不過兩者即使被混用,互相替代,通常不影響表達。
  • MQ,中間件級別的消息隊列(e.g. ActiveMQ,RabbitMQ),可以認為是發布訂閱模式的一個具體體現。事件驅動->發布訂閱->MQ,從抽象到具體。
  • Java和spring中都擁有Event的抽象,分別代表了語言級別和三方框架級別對事件的支持。
  • EventSourcing這個概念就要關聯到領域驅動設計,DDD對事件驅動也是非常地青睞,領域對象的狀態完全是由事件驅動來控制,由其衍生出了CQRS架構,具體實現框架有AxonFramework。
  • Nginx可以作為高性能的應用服務器(e.g. openResty),以及Nodejs事件驅動的特性,這些也是都是事件驅動的體現。

本文涵蓋的內容主要是前面4點。

Spring對Event的支持

Spring的文檔對Event的支持翻譯之後描述如下:

ApplicationContext通過ApplicationEvent類和ApplicationListener接口進行事件處理。 如果將實現ApplicationListener接口的bean註入到上下文中,則每次使用ApplicationContext發布ApplicationEvent時,都會通知該bean。 本質上,這是標準的觀察者設計模式。

而在spring4.2之後,提供了註解式的支持,我們可以使用任意的java對象配合註解達到同樣的效果,首先來看看不適用註解如何在Spring中使用事件驅動機制。

定義業務需求:用戶註冊後,系統需要給用戶發送郵件告知用戶註冊成功,需要給用戶初始化積分;隱含的設計需求,用戶註冊後,後續需求可能會添加其他操作,如再發送一條短信等等,希望程序具有擴展性,以及符合開閉原則。

如果不使用事件驅動,代碼可能會像這樣子:

public class UserService {
  
    @Autowired
    EmailService emailService;
    @Autowired
    ScoreService scoreService;
    @Autowired
    OtherService otherService;
    public void register(String name) {
        system.out.println("用戶:" + name + " 已註冊!");
        emailService.sendEmail(name);
        scoreService.initScore(name);
        otherService.execute(name);
    }
  
}

要說有什麽毛病,其實也不算有,因為可能大多數人在開發中都會這麽寫,喜歡寫同步代碼。但這麽寫,實際上並不是特別的符合隱含的設計需求,假設增加更多的註冊項service,我們需要修改register的方法,並且讓UserService註入對應的Service。而實際上,register並不關心這些“額外”的操作,如何將這些多余的代碼抽取出去呢?便可以使用Spring提供的Event機制。

定義用戶註冊事件

public class UserRegisterEvent extends ApplicationEvent{
    public UserRegisterEvent(String name) { //name即source
        super(name);
    }
}

ApplicationEvent是由Spring提供的所有Event類的基類,為了簡單起見,註冊事件只傳遞了name(可以復雜的對象,但註意要了解清楚序列化機制)。

定義用戶註冊服務(事件發布者)

@Service // <1>
public class UserService implements ApplicationEventPublisherAware { // <2>
    public void register(String name) {
        System.out.println("用戶:" + name + " 已註冊!");
        applicationEventPublisher.publishEvent(new UserRegisterEvent(name));// <3>
    }
    private ApplicationEventPublisher applicationEventPublisher; // <2>
    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { // <2>
        this.applicationEventPublisher = applicationEventPublisher;
    }
}

<1> 服務必須交給Spring容器托管

<2> ApplicationEventPublisherAware是由Spring提供的用於為Service註入ApplicationEventPublisher事件發布器的接口,使用這個接口,我們自己的Service就擁有了發布事件的能力。

<3> 用戶註冊後,不再是顯示調用其他的業務Service,而是發布一個用戶註冊事件。

定義郵件服務,積分服務,其他服務(事件訂閱者)

@Service // <1>
public class EmailService implements ApplicationListener<UserRegisterEvent> { // <2>
    @Override
    public void onApplicationEvent(UserRegisterEvent userRegisterEvent) {
        System.out.println("郵件服務接到通知,給 " + userRegisterEvent.getSource() + " 發送郵件...");// <3>
    }
}

<1> 事件訂閱者的服務同樣需要托管於Spring容器

<2> ApplicationListener<E extends ApplicationEvent>接口是由Spring提供的事件訂閱者必須實現的接口,我們一般把該Service關心的事件類型作為泛型傳入。

<3> 處理事件,通過event.getSource()即可拿到事件的具體內容,在本例中便是用戶的姓名。

其他兩個Service,也同樣編寫,實際的業務操作僅僅是打印一句內容即可,篇幅限制,這裏省略。

編寫啟動類

@SpringBootApplication
@RestController
public class EventDemoApp {
    public static void main(String[] args) {
        SpringApplication.run(EventDemoApp.class, args);
    }
    @Autowired
    UserService userService;
    @RequestMapping("/register")
    public String register(){
        userService.register("kirito");
        return "success";
    }
}

當我們調用userService.register(“kirito”);方法時,控制臺打印信息如下:

他們的順序是無序的,如果需要控制順序,需要重寫order接口,這點不做介紹。其次,我們完成了用戶註冊和其他服務的解耦,這也是事件驅動的最大特性之一,如果需要在用戶註冊時完成其他操作,只需要再添加相應的事件訂閱者即可。

Spring 對Event的註解支持

上述的幾個接口已經非常清爽了,如果習慣使用註解,Spring也提供了,不再需要顯示實現

註解式的事件發布者

@Service
public class UserService {
    public void register(String name) {
        System.out.println("用戶:" + name + " 已註冊!");
        applicationEventPublisher.publishEvent(new UserRegisterEvent(name));
    }
    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;
}

Spring4.2之後,ApplicationEventPublisher自動被註入到容器中,采用Autowired即可獲取。

註解式的事件訂閱者

@Service
public class EmailService {
    @EventListener
    public void listenUserRegisterEvent(UserRegisterEvent userRegisterEvent) {
        System.out.println("郵件服務接到通知,給 " + userRegisterEvent.getSource() + " 發送郵件...");
    }
}

@EventListener註解完成了ApplicationListener<E extends ApplicationEvent>接口的使命。

更多的特性可以參考SpringFramework的文檔。

Spring中事件的應用

在以往閱讀Spring源碼的經驗中,接觸了不少使用事件的地方,大概列了以下幾個,加深以下印象:

  • Spring Security中使用AuthenticationEventPublisher處理用戶認證成功,認證失敗的消息處理。
public interface AuthenticationEventPublisher {
   void publishAuthenticationSuccess(Authentication authentication);
   void publishAuthenticationFailure(AuthenticationException exception,
         Authentication authentication);
}
  • Hibernate中持久化對象屬性的修改是如何被框架得知的?正是采用了一系列持久化相關的事件,如DefaultSaveEventListener,DefaultupdateEventListener,事件非常多,有興趣可以去org.hibernate.event包下查看。
  • Spring Cloud Zuul中刷新路由信息使用到的ZuulRefreshListener
private static class ZuulRefreshListener implements ApplicationListener<ApplicationEvent> {
       ...
        public void onApplicationEvent(ApplicationEvent event) {
            if(!(event instanceof ContextRefreshedEvent) && !(event instanceof RefreshScopeRefreshedEvent) && !(event instanceof RoutesRefreshedEvent)) {
                if(event instanceof HeartbeatEvent && this.heartbeatMonitor.update(((HeartbeatEvent)event).getValue())) {
                    this.zuulHandlerMapping.setDirty(true);
                }
            } else {
                this.zuulHandlerMapping.setDirty(true);
            }
        }
    }
  • Spring容器生命周期相關的一些默認Event
ContextRefreshedEvent,ContextStartedEvent,ContextStoppedEvent,ContextClosedEvent,RequestHandledEvent

。。。其實吧,非常多。。。

總結

本文暫時只介紹了Spring中的一些簡單的事件驅動機制,相信如果之後再看到Event,Publisher,EventListener一類的單詞後綴時,也能立刻和事件機制聯系上了。再閱讀Spring源碼時,如果發現出現了某個Event,但由於不是同步調用,所以很容易被忽視,我一般習慣下意識的去尋找有沒有提供默認的Listener,這樣不至於漏掉一些“隱藏”的特性。下一篇文章打算聊一聊分布式場景下,事件驅動使用的註意點。

公眾號剛剛創立,如果覺得文章不錯,希望能分享到您的朋友圈,如果對文章有什麽想法和建議,可以與我溝通。


Tags: 驅動 事件 觀察者 模式 訂閱 發布

文章來源:


ads
ads

相關文章
ads

相關文章

ad