設計模式之釋出訂閱模式(3) 深入Spring Events事件驅動模型

西召發表於2019-05-02

之前文章中我們講解了 釋出訂閱模式的核心概念 ,並通過 Redis的 Pub/Sub 命令 演示了其分散式場景下的實現。相比後面要講到的 Guava EventBus,可以說 Spring Events 的使用更加普遍,其功能也更加強大。

事件(Events)是框架中經常被忽略的、重要的功能,也是釋出/訂閱模式的一種常見實現。Spring框架本身就是事件驅動的。

下面我們就一起看一下Spring容器中的事件驅動模型,然後一起快速實現一個自定義的事件釋出和監聽,接著再分別探討一下同步和非同步的事件監聽如何實現,再接著介紹一下如何定義監聽器的順序,最後提供一個基於SpEL(Spring Expression Language )實現的條件化的事件監聽機制。

學完本節課程,將你會發現:

  1. 通過事件機制將程式碼解耦,會讓自己的程式碼非常乾淨,且擴充套件性極強。
  2. 非同步的事件處理機制,可以大大提高程式的響應速度,且內部的執行緒池會大大提高程式的併發效率。
  3. 條件化和泛型化的監聽器可以讓你減少很多顯式的邏輯判斷,從而讓每個事件監聽的原子性更強。

? 本文原始碼Github地址

Spring本身的事件驅動模型

Spring 容器與事件模型

Spring的事件機制主要提供瞭如下幾個介面和類:

設計模式之釋出訂閱模式(3) 深入Spring Events事件驅動模型

  • ApplicationContextEvent

Spring提供的事件抽象類,你可以繼承它來實現自定義的事件。

  • ApplicationEventMulticaster

ApplicationEventMulticaster是一個事件廣播器, 它的作用是把Applicationcontext釋出的Event廣播給所有的監聽器。

  • ApplicationListener

ApplicationListener繼承自EventListener, 所有的監聽器都要實現這個介面。

這個介面只有一個onApplicationEvent()方法, 該方法接受一個ApplicationEvent或其子類物件作為引數, 在方法體中,可以通過不同對Event類的判斷來進行相應的處理。

當事件觸發時所有的監聽器都會收到訊息, 如果你需要對監聽器的接收順序有要求,可是實現該介面的一個實現SmartApplicationListener, 通過這個介面可以指定監聽器接收事件的順序。

  • ApplicationContext

實現事件機制需要三個部分:事件源、事件和事件監聽器。 上面介紹的ApplicationEvent相當於事件, ApplicationListener相當於事件監聽器, 這裡的事件源說的就是ApplicationContext

ApplicationContext是Spring中的全域性容器, 也叫"應用上下文", 它負責讀取bean的配置, 管理bean的載入, 維護bean之間的依賴關係, 也就是負責管理bean的整個生命週期。

ApplicationContext就是我們平時所說的IOC容器。

  • ApplicationContextAware

當一個類實現了ApplicationContextAware介面之後,Aware介面的Bean在被初始之後,可以取得一些相對應的資源,這個類可以直接獲取 spring 配置檔案中所有注入的bean物件。

Spring提供了很多以Aware結尾的介面,通過實現這些介面,你就獲得了獲取Spring容器內資源的能力。

Spring本身實現瞭如下4個Event:

  • ContextStartedEvent (容器啟動)
  • ContextStoppedEvent (容器停止)
  • ContextClosedEvent (容器關閉)
  • ContextRefreshedEvent (容器重新整理)

自定義 Spring Events 實現

自定義Spring的事件模型需要三個角色:事件(Event)、釋出者(Publisher)、監聽者(Listerner)。

設計模式之釋出訂閱模式(3) 深入Spring Events事件驅動模型

自定義Event

下面自定義了一個註冊事件,這個Event的建構函式提供了兩個引數,一個是釋出源(source),一個是釋出的訊息(message)。

package net.ijiangtao.tech.designpattern.pubsub.spring.common;

import lombok.Getter;
import org.springframework.context.ApplicationEvent;

/**
 * 註冊事件
 * @author ijiangtao
 * @create 2019-05-02 12:59
 **/
@Getter
public class RegisterEvent extends ApplicationEvent {

    private String message;

    public RegisterEvent(Object source, String message) {
        super(source);
        this.message = message;
    }

}
複製程式碼

自定義Publisher

下面提供了一個釋出自定義事件的釋出器,我們通過ApplicationEventPublisher來把事件釋出出去。

package net.ijiangtao.tech.designpattern.pubsub.spring.common;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;

import java.time.LocalTime;

/**
 * 註冊事件釋出器
 * @author ijiangtao
 * @create 2019-05-02 13:01
 **/
@Component
@Slf4j
public class RegisterEventPublisher {

    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    public void publish(final String message) {
        log.info("publis a RegisterEvent,message:{}", message + " time: " + LocalTime.now());
        RegisterEvent registerEvent = new RegisterEvent(this, message);
        applicationEventPublisher.publishEvent(registerEvent);
    }
}

複製程式碼

自定義Listener

下面提供幾個自定義事件的監聽器,它們都實現了ApplicationListener<RegisterEvent>介面,同時為了模擬處理事件的過程,這裡讓當前執行緒休眠了3秒。因為實現過程類似,這裡僅提供一個實現。

package net.ijiangtao.tech.designpattern.pubsub.spring.common;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

import java.time.LocalTime;

/**
 * 傳送註冊成功郵件提醒
 *
 * @author ijiangtao
 * @create 2019-05-02 13:07
 **/
@Component
@Slf4j
public class SendRegisterEmailListener implements ApplicationListener<RegisterEvent> {
    @Override
    public void onApplicationEvent(RegisterEvent event) {
        try {
            Thread.sleep(3 * 1000);
        } catch (Exception e) {
            log.error("{}", e);
        }
        log.info("SendRegisterEmailListener message: " + event.getMessage()+" time: "+ LocalTime.now());
    }

}
複製程式碼

測試自定義事件機制

下面通過一個單元測試釋出了自定義事件。通過觀察log輸出,發現事件釋出以後,每個監聽器都依次輸出了監聽日誌。

package net.ijiangtao.tech.designpattern.pubsub.spring;

import lombok.extern.slf4j.Slf4j;
import net.ijiangtao.tech.designpattern.pubsub.spring.common.RegisterEventPublisher;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * Spring Events
 *
 * @author ijiangtao
 * @create 2019-05-02 12:53
 **/
@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class SpringEventsCommonTests {

    @Autowired
    private RegisterEventPublisher registerEventPublisher;

    @Test
    public void test1(){
        registerEventPublisher.publish(" Danny is here.");
        try {
            Thread.sleep(10 * 1000);
        } catch (Exception e) {
            log.error("{}", e);
        }
    }

}
複製程式碼

這樣,一個基於Spring Events的事件監聽器就實現了。

實現非同步的 Spring Events

通過觀察日誌中列印的時間你會發現,上面註冊的所有監聽器,都是依次執行的,也就是Spring Events的事件處理預設是同步的。同步的事件監聽耗時比較長,需要等待上一個監聽處理結束,下一個監聽器才能執行。

那麼能不能改成非同步監聽呢?答案是肯定的。下面介紹兩種實現方式。

配置

通過JDK提供的SimpleApplicationEventMulticaster將事件廣播出去,就可以實現非同步併發地讓多個監聽器同時執行事件監聽動作。

package net.ijiangtao.tech.designpattern.pubsub.spring.async.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ApplicationEventMulticaster;
import org.springframework.context.event.SimpleApplicationEventMulticaster;
import org.springframework.core.task.SimpleAsyncTaskExecutor;

/**
 * 非同步事件監聽配置
 *
 * @author ijiangtao
 * @create 2019-05-02 13:23
 **/
@Configuration
public class AsynchronousSpringEventsConfig {

    @Bean(name = "applicationEventMulticaster")
    public ApplicationEventMulticaster simpleApplicationEventMulticaster() {
        SimpleApplicationEventMulticaster eventMulticaster  = new SimpleApplicationEventMulticaster();
        eventMulticaster.setTaskExecutor(new SimpleAsyncTaskExecutor());
        return eventMulticaster;
    }
}
複製程式碼

通過SimpleApplicationEventMulticaster的原始碼可以看到它的multicastEvent方法會通過執行緒池併發執行事件釋出動作。

public void multicastEvent(ApplicationEvent event, @Nullable ResolvableType eventType) {
     ResolvableType type = eventType != null ? eventType : this.resolveDefaultEventType(event);
     Iterator var4 = this.getApplicationListeners(event, type).iterator();

     while(var4.hasNext()) {
         ApplicationListener<?> listener = (ApplicationListener)var4.next();
         Executor executor = this.getTaskExecutor();
         if (executor != null) {
             executor.execute(() -> {
                 this.invokeListener(listener, event);
             });
         } else {
             this.invokeListener(listener, event);
         }
     }

 }
複製程式碼

註解

通過註解的方式釋出事件,只需要在Listener上加上@Async,並且在釋出事件的地方加上@EnableAsync註解即可。

package net.ijiangtao.tech.designpattern.pubsub.spring.async.annotation;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

import java.time.LocalTime;

/**
 * 傳送優惠券
 *
 * @author ijiangtao
 * @create 2019-05-02 13:07
 **/
@Component
@Slf4j
public class UserActionListenerAsyncAnnotation implements ApplicationListener<RegisterEvent> {

    @Async
    @Override
    public void onApplicationEvent(RegisterEvent event) {
        try {
            Thread.sleep(3 * 1000);
        } catch (Exception e) {
            log.error("{}", e);
        }
        log.info("UserActionListener message: " + event.getMessage()+" time: "+ LocalTime.now());
    }

}
複製程式碼
package net.ijiangtao.tech.designpattern.pubsub.spring;

import lombok.extern.slf4j.Slf4j;
import net.ijiangtao.tech.designpattern.pubsub.spring.async.annotation.RegisterEventPublisherAsyncAnnotation;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * Spring Events
 *
 * @author ijiangtao
 * @create 2019-05-02 12:53
 **/
@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
@EnableAsync
public class SpringEventsAsyncAnnotationTests {

    @Autowired
    private RegisterEventPublisherAsyncAnnotation registerEventPublisherAsyncAnnotation;

    @Test
    public void test2() {
        registerEventPublisherAsyncAnnotation.publish(" Danny is here (Async).");
        try {
            Thread.sleep(10 * 1000);
        } catch (Exception e) {
            log.error("{}", e);
        }
    }

}
複製程式碼

實現 Smart Listener

通過實現SmartApplicationListener介面,可以自定義監聽器的執行順序、支援的事件型別等。

package net.ijiangtao.tech.designpattern.pubsub.spring.smart;

import lombok.Getter;
import org.springframework.context.ApplicationEvent;

/**
 * event
 *
 * @author ijiangtao
 * @create 2019-05-02 15:33
 **/
@Getter
public class SmartEvent extends ApplicationEvent {

    private String message;

    public SmartEvent(Object source, String message) {
        super(source);
    }

}
複製程式碼
package net.ijiangtao.tech.designpattern.pubsub.spring.smart;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.event.SmartApplicationListener;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;

/**
 * SmartApplicationListener
 *
 * @author ijiangtao
 * @create 2019-05-02 15:32
 **/
@Component
@Slf4j
public class CustomSmartApplicationListener1 implements SmartApplicationListener {


    /**
     * 自定義支援的事件型別
     * @param eventType
     * @return
     */
    @Override
    public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
        return eventType == SmartEvent.class;
    }

    /**
     * 定義支援的事件源型別
     * @param sourceType
     * @return
     */
    @Override
    public boolean supportsSourceType(Class<?> sourceType) {
        return sourceType == String.class;
    }

    /**
     * 自定義優先順序別
     * @return
     */
    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }

    @Override
    public void onApplicationEvent(ApplicationEvent applicationEvent) {
        log.info("CustomSmartApplicationListener {}",applicationEvent.getSource());
    }

}
複製程式碼

條件化的事件監聽

有時候我們希望一個監聽器希望監聽多個事件,例如一個系統安全監聽器(SecurityEventListener),可以監聽各種系統安全問題(NetWorkSecurityEvent、SQLSecurityEvent、AuthorizationSecurityEvent,等等),這個是時候你可以讓監聽器監聽這些Event的父類SecurityEvent,這樣你的監聽器就可以監聽到所有該Event的子型別。

有時候我們需要根據同一個事件丟擲的訊息的某個值來決定用哪個監聽器來處理。例如SecurityEvent有個安全級別level屬性,你定義了5個level,每個level都有不同的處理機制。按照傳統的實現方式需要通過條件判斷(if/else或者switch/case等)來實現,程式碼的封裝性不好。這種情況下,你可以在你的Listener的監聽方法上增加@EventListener註解,並通過condition引數來指定過濾條件。例如 condition = "#event.success eq false")就是通過SpEL表示:當方法的引數event變數的success屬性等於false的時候才執行監聽方法。

下面我們就演示一下實現過程。

提供泛型的Event基類:

package net.ijiangtao.tech.designpattern.pubsub.spring.generic;

import lombok.Getter;

/**
 * GenericSpringEvent
 *
 * @author ijiangtao
 * @create 2019-05-02 13:47
 **/
@Getter
public class GenericSpringEvent<T> {

    private T what;

    protected boolean success;

    public GenericSpringEvent(T what, boolean success) {
        this.what = what;
        this.success = success;
    }

}
複製程式碼

基於Event基類自定義Event實現

package net.ijiangtao.tech.designpattern.pubsub.spring.generic.checkout;

import lombok.Getter;
import net.ijiangtao.tech.designpattern.pubsub.spring.generic.GenericSpringEvent;

/**
 * GenericSpringEventCheckout
 *
 * @author ijiangtao
 * @create 2019-05-02 13:58
 **/
@Getter
public class GenericSpringEventCheckout extends GenericSpringEvent<Long> {

    private Long userId;

    public GenericSpringEventCheckout(Long userId, boolean success) {
        super(userId, success);
    }

}
複製程式碼

提供條件化監聽器

監聽器監聽基類Event的所有字類,並且通過@EventListener註解和SpEL定義監聽的Event的過濾條件。

package net.ijiangtao.tech.designpattern.pubsub.spring.generic;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

/**
 * @author ijiangtao
 * @create 2019-05-02 13:52
 **/
@Component
@Slf4j
public class GenericSpringEventSuccessListenerLong {

    @EventListener(condition = "#event.success")
    public void handle(GenericSpringEvent<Long> event) {
        log.info("Handling generic event Success (conditional). {}",event.getWhat());
    }

}
複製程式碼
package net.ijiangtao.tech.designpattern.pubsub.spring.generic;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

/**
 * @author ijiangtao
 * @create 2019-05-02 13:52
 **/
@Component
@Slf4j
public class GenericSpringEventFailListenerLong {

    @EventListener(condition = "#event.success eq false")
    public void handle(GenericSpringEvent<Long> event) {
        log.info("Handling generic event  Fail (conditional). {}",event.getWhat());
    }

}
複製程式碼

自定義事件釋出器

package net.ijiangtao.tech.designpattern.pubsub.spring.generic.checkout;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;

import java.time.LocalTime;

/**
 * GenericSpringEventPublisher
 *
 * @author ijiangtao
 * @create 2019-05-02 13:55
 **/
@Component
@Slf4j
public class GenericSpringEventPublisherCheckout {

    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    public void publish(final Long userId, boolean success) {

        log.info("publis a GenericSpringEventPublisher, userId:{}", userId + " time: " + LocalTime.now());

        GenericSpringEventCheckout eventCheckout = new GenericSpringEventCheckout(userId, success);

        applicationEventPublisher.publishEvent(eventCheckout);
    }

}
複製程式碼

單元測試

下面提供了一個測試方法,通過觀察日誌發現,不同的條件,觸發了不同的監聽器。

package net.ijiangtao.tech.designpattern.pubsub.spring;

import lombok.extern.slf4j.Slf4j;
import net.ijiangtao.tech.designpattern.pubsub.spring.generic.checkout.GenericSpringEventPublisherCheckout;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationEvent;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * Spring Events
 *
 * @author ijiangtao
 * @create 2019-05-02 12:53
 **/
@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class SpringEventsGenericTests {

    @Autowired
    private GenericSpringEventPublisherCheckout checkoutPubliser;


    @Test
    public void test1() {

        ApplicationEvent applicationEvent;
        checkoutPubliser.publish(101L, true);

        checkoutPubliser.publish(202L, false);
    }

}

複製程式碼

Wechat-westcall

相關連結

相關文章