1. 程式人生 > >spring event的事件驅動模型的最佳實踐@EventListene

spring event的事件驅動模型的最佳實踐@EventListene


我們知道觀察者模式可以實現程式碼的解耦,而spring的event模型就是這種設計模式的極佳體現。一個事件包含:事件釋出、監聽、和事件源。在spring中我們可以通過ApplicationContext的publishEvent方法去釋出事件;通過實現ApplicationListener介面來自定義自己的監聽器;繼承ApplicationEvent類來實現事件源。下面以一個例項來說明:

1.spring下使用event模型

1.1 定義event

/**
 * event的基類
 *
 * @author 94977
 * @create 2018/7/22
 */
public abstract class BaseEvent extends ApplicationEvent {
    public BaseEvent(Object source) {
        super(source);
    }
}
public class FaceEvent extends BaseEvent {

    /**
    * @author 94977
    * @time 2018/7/22 15:50
    * @param * @param null
    * @return
    * @description  必須要實現的構造方法
    */
    public FaceEvent(User user) {
        super(user);
    }
}

1.2 event的監聽處理類。監聽類實現ApplicationListener 裡onApplicationEvent方法即可

@Component
public class FaceEventListener implements ApplicationListener {

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        if(event instanceof FaceEvent){
            User user = (User) event.getSource();
            LOGGER.info("===> 收到人臉事件:  {}",user);
            // .....
            System.out.println("人臉事件處理結束。。。");
        }
    }
}

當然通過event instanceof FaceEvent判斷事件源來處理的方式不是很優雅。有更好的方式,介面ApplicationListener支援泛型,可以通過泛型來判斷處理的事件源。如下只處理FaceEvent源。

@Component
public class FaceEventListener extends BaseEventListener implements ApplicationListener<FaceEvent> {

    @Override
    public void onApplicationEvent(FaceEvent event) {
        User user = (User) event.getSource();
        LOGGER.info("===> 收到人臉事件:  {}",user);
        // .....
        System.out.println("人臉事件處理結束。。。");

    }
}

如果要實現有序的監聽,實現SmartApplicationListener 介面即可

1.3 釋出事件

@Service
public class FaceHandler {

    @Autowired
    private ApplicationContext applicationContext;

    public void handle(){
        User user = new User();
        user.setAge(34);
        user.setUsername("人臉事件");
        user.setHobby("抓拍");
        //釋出事件
        applicationContext.publishEvent(new FaceEvent(user));
		//進行其他業務處理
    }

以上即可。

2.evnet模型的注意點

  • 事件沒要處理的監聽器,就會被拋棄。
  • 一個事件可以同時被多個監聽處理類監聽處理。
  • 以上處理事件都是同步的,如果釋出事件處的業務存在事務,監聽器處理也會在相同的事務中。這個一定要注意!如果對於事件的處理不想受到影響,可以onApplicationEvent方法上加@Aync支援非同步(參考taskExecutor的使用)。
  • 原理部分可以參考 部落格 事件體系

3. 一種更優雅的方式

3.1 釋出事件

我們可以通過工具類釋出來避免在程式碼耦合注入ApplicationContext,工具類實現ApplicationEventPublisherAware 介面,具體可參考spring的aware學習
這裡有一個小細節,如果通過注入ApplicationContext的方式來發布事件,idea在程式碼左邊會有一個類似耳機的小圖示,點選可以跳到監聽此釋出事件的監聽者位置,用工具類釋出事件就沒有此提示了。
在這裡插入圖片描述

3.2 定義事件源

public abstract class BaseEvent<T> extends ApplicationEvent {

    private static final long serialVersionUID = 895628808370649881L;

    protected T eventData;

    public BaseEvent(Object source, T eventData){
        super(source);
        this.eventData = eventData;
    }

    public BaseEvent(T eventData){
        super(eventData);
    }
    
    public T getEventData() {
        return eventData;
    }
    public void setEventData(T eventData) {
        this.eventData = eventData;
    }
}

需要釋出的事件繼承此BaseEvent

public class FaceEvent extends BaseEvent<User> {
    
    public FaceEvent(User user) {
        super(user);
    }

    public FaceEvent(Object source, User user){
        super(source,user);
    }

}

如果程式碼結構較複雜,多處釋出相同的事件,建議釋出事件時將this作為source傳遞,便於通過分析日誌確定釋出源。

3.3 監聽事件@EventListener

在spring4.2中我們可以以更加簡潔的方式來監聽event的釋出,監聽事件我們不必再實現ApplicationListener介面了,只要在方法上添加註解@EventListener即可:

	@EventListener
    public void onApplicationEvent(FaceEvent event) {
        User user = (User) event.getSource();
        String name = Thread.currentThread().getName();
        LOGGER.info("===> 收到人臉事件:  {},執行緒名為: {}",user,name);
    }

會根據方法引數型別來自動監聽相應事件的釋出。
如果要監聽多個事件型別的釋出,可以在@EventListener(classes = {FaceEvent.class,ArmEvent.class})指定,spring會多次呼叫此方法來處理多個事件。但是注意此時,方法引數不能有多個,否則會發生轉換異常,可以將使用多個事件的父類作為唯一的方法引數來接收處理事件,但除非必要否則並不推薦監聽多個事件的釋出。

  • 如果有多個監聽器監聽同一事件,我們可以在方法上使用spring的@order註解來定義多個監聽器的順序,如:

	@EventListener
    @Order(4)
    public void onApplicationEvent(FaceEvent event) {
        User user = (User) event.getSource();
        LOGGER.info("===> A 收到人臉事件:  {}",user);
    }


    @EventListener({FaceEvent.class,ArmEvent.class})
    @Order(3)
    public void onApplicationEvent3(Object event) {

        if(event instanceof FaceEvent){
            LOGGER.info("===> B 收到人臉事件:  {}",((FaceEvent) event).getEventData());
        }else if(event instanceof ArmEvent){
            ArmEvent armEvent = (ArmEvent) event;
            LOGGER.info("===> B 收到臂膀事件:  {}",armEvent.getEventData());
        }
    }

這真的是很方便。

  • @EventListener還有一個屬性,condition()裡可以使用SPEL表示式來過濾監聽到事件,即只有符合某種條件的才進行接收處理。暫時還用不到。

3.4 監聽事件時的事務隔離

  • @TransactionalEventListener和@EventListener都可以監聽事件,但前者可以對釋出事件和監聽事件進行一些事務上的隔離。@TransactionalEventListenerr指不和釋出事件的方法在同一個事務內,釋出事件的方法事務結束後才會執行本監聽方法,監聽邏輯內發生異常不會回滾釋出事件方法的事務。

    @Transactional(rollbackFor = Exception.class)
    public void handle(){
        User user = new User();
        user.setAge(34);
        user.setUsername("人臉事件");
        user.setHobby("抓拍");

        //處理完上面的邏輯後,釋出事件
        EventPublisherUtil.publishEvent(new FaceEvent(user));
 
        //資料庫新增操作
        Integer integer = deviceAlarmService.addDevice();
    }

可以看到釋出事件的方法處在事務控制中,我們使用@TransactionalEventListener來監聽事件:


	@TransactionalEventListener(fallbackExecution = true)
    public void onApplicationEvent(FaceEvent event) {
        User user = event.getEventData();
        LOGGER.info("===> A 收到人臉事件:  {}}",user);

        //@TransactionalEventListener指不和釋出事件的在同一個事務內,釋出事件的方法事務結束後才會執行本方法,本方法發生異常不會回滾釋出事件的事務,
        throw new  RuntimeException("監聽事件丟擲異常");
    }

執行結果,addDevice正常在資料庫插入資料,但是修改為@EventListener監聽則插入資料失敗。

  • @TransactionalEventListener有一個屬性為fallbackExecution,預設為false,指釋出事件的方法沒有事務控制時,監聽器不進行監聽事件,此為預設情況! fallbackExecution=true,則指釋出事件的方法沒有事務控制時,監聽方法仍可以監聽事件進行處理。
	/**
	 * Whether the event should be processed if no transaction is running.
	 */
	boolean fallbackExecution() default false;
  • 剛才我們說到使用@TransactionalEventListener會在釋出事件的方法事務結束後執行監聽方法,但其實我們還可以進行細化的控制。它有一個屬性為TransactionPhase,預設為TransactionPhase.AFTER_COMMIT,即事務提交後。還可以根據需要選擇AFTER_COMPLETION、BEFORE_COMMIT、AFTER_ROLLBACK。
    但仍需注意,如果fallbackExecution=false,且釋出事件的方法沒有事務控制時,監聽器根本不會監聽到事件,此處的TransactionPhase也就沒有意義了。