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也就沒有意義了。