轉:https://segmentfault.com/a/1190000011433514
:https://blog.csdn.net/hjing123/article/details/89203524
ApplicationEvent
以及Listener
是Spring為我們提供的一個事件監聽、訂閱的實現,內部實現原理是觀察者設計模式,設計初衷也是為了系統業務邏輯之間的解耦,提高可擴展性以及可維護性。事件發布者並不需要考慮誰去監聽,監聽具體的實現內容是什么,發布者的工作只是為了發布事件而已。
我們平時日常生活中也是經常會有這種情況存在,如:我們在平時拔河比賽中,裁判員給我們吹響了開始的信號,也就是給我們發布了一個開始的事件,而拔河雙方人員都在監聽着這個事件,一旦事件發布后雙方人員就開始往自己方使勁。而裁判並不關心你比賽的過程,只是給你發布事件你執行就可以了。
本章目標
我們本章在SpringBoot
平台上通過ApplicationEvents以及Listener來完成簡單的注冊事件流程。
構建項目
我們本章只是簡單的講解如何使用ApplicationEvent以及Listener來完成業務邏輯的解耦,不涉及到數據交互所以依賴需要引入的也比較少,項目pom.xml配置文件如下所示:
.....//省略 <dependencies> <!--web--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.16.16</version> </dependency> <!--test--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> .....//省略
其中lombok依賴大家有興趣可以去深研究下,這是一個很好的工具,它可以結合Idea開發工具完成對實體的動態添加構造函數、Getter/Setter方法、toString方法等。
創建UserRegisterEvent事件
我們先來創建一個事件,監聽都是圍繞着事件來掛起的。事件代碼如下所示:
package com.yuqiyu.chapter27.event; import com.yuqiyu.chapter27.bean.UserBean; import lombok.Getter; import org.springframework.context.ApplicationEvent; /** * ======================== * Created with IntelliJ IDEA. * User:恆宇少年 * Date:2017/7/21 * Time:10:08 * 碼雲:http://git.oschina.net/jnyqy * ======================== */ @Getter public class UserRegisterEvent extends ApplicationEvent { //注冊用戶對象 private UserBean user; /** * 重寫構造函數 * @param source 發生事件的對象 * @param user 注冊用戶對象 */ public UserRegisterEvent(Object source,UserBean user) { super(source); this.user = user; } }
我們自定義事件UserRegisterEvent繼承了ApplicationEvent,繼承后必須重載構造函數,構造函數的參數可以任意指定,其中source參數指的是發生事件的對象,一般我們在發布事件時使用的是this關鍵字代替本類對象,而user參數是我們自定義的注冊用戶對象,該對象可以在監聽內被獲取。
在Spring內部中有多種方式實現監聽如:@EventListener注解、實現ApplicationListener泛型接口、實現SmartApplicationListener接口等,我們下面來講解下這三種方式分別如何實現。
創建UserBean
我們簡單創建一個用戶實體,並添加兩個字段:用戶名、密碼。實體代碼如下所示:
package com.yuqiyu.chapter27.bean; import lombok.Data; /** * ======================== * Created with IntelliJ IDEA. * User:恆宇少年 * Date:2017/7/21 * Time:10:05 * 碼雲:http://git.oschina.net/jnyqy * ======================== */ @Data public class UserBean { //用戶名 private String name; //密碼 private String password; }
創建UserService
UserService內添加一個注冊方法,該方法只是實現注冊事件發布功能,代碼如下所示:
package com.yuqiyu.chapter27.service; import com.yuqiyu.chapter27.bean.UserBean; import com.yuqiyu.chapter27.event.UserRegisterEvent; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Service; /** * ======================== * Created with IntelliJ IDEA. * User:恆宇少年 * Date:2017/7/21 * Time:10:11 * 碼雲:http://git.oschina.net/jnyqy * ======================== */ @Service public class UserService { @Autowired ApplicationContext applicationContext; /** * 用戶注冊方法 * @param user */ public void register(UserBean user) { //../省略其他邏輯 //發布UserRegisterEvent事件 applicationContext.publishEvent(new UserRegisterEvent(this,user)); } }
事件發布是由ApplicationContext對象管控的,我們發布事件前需要注入ApplicationContext對象調用publishEvent方法完成事件發布。
創建UserController
創建一個@RestController控制器,對應添加一個注冊方法簡單實現,代碼如下所示:
package com.yuqiyu.chapter27.controller; import com.yuqiyu.chapter27.bean.UserBean; import com.yuqiyu.chapter27.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * 用戶控制器 * ======================== * Created with IntelliJ IDEA. * User:恆宇少年 * Date:2017/7/21 * Time:10:05 * 碼雲:http://git.oschina.net/jnyqy * ======================== */ @RestController public class UserController { //用戶業務邏輯實現 @Autowired private UserService userService; /** * 注冊控制方法 * @param user 用戶對象 * @return */ @RequestMapping(value = "/register") public String register ( UserBean user ) { //調用注冊業務邏輯 userService.register(user); return "注冊成功."; } }
@EventListener實現監聽
注解方式比較簡單,並不需要實現任何接口,具體代碼實現如下所示:
package com.yuqiyu.chapter27.listener; import com.yuqiyu.chapter27.bean.UserBean; import com.yuqiyu.chapter27.event.UserRegisterEvent; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; /** * 使用@EventListener方法實現注冊事件監聽 * ======================== * Created with IntelliJ IDEA. * User:恆宇少年 * Date:2017/7/21 * Time:10:50 * 碼雲:http://git.oschina.net/jnyqy * ======================== */ @Component public class AnnotationRegisterListener { /** * 注冊監聽實現方法 * @param userRegisterEvent 用戶注冊事件 */ @EventListener public void register(UserRegisterEvent userRegisterEvent) { //獲取注冊用戶對象 UserBean user = userRegisterEvent.getUser(); //../省略邏輯 //輸出注冊用戶信息 System.out.println("@EventListener注冊信息,用戶名:"+user.getName()+",密碼:"+user.getPassword()); } }
我們只需要讓我們的監聽類被Spring所管理即可,在我們用戶注冊監聽實現方法上添加@EventListener注解,該注解會根據方法內配置的事件完成監聽。下面我們啟動項目來測試下我們事件發布時是否被監聽者所感知。
測試事件監聽
使用SpringBootApplication方式啟動成功后,我們來訪問下地址:http://127.0.0.1:8080/register?name=admin&password=123456,界面輸出內容肯定是“注冊成功”,這個是沒有問題的,我們直接查看控制台輸出內容,如下所示:
2017-07-21 11:09:52.532 INFO 10460 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring FrameworkServlet 'dispatcherServlet' 2017-07-21 11:09:52.532 INFO 10460 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization started 2017-07-21 11:09:52.545 INFO 10460 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization completed in 13 ms @EventListener注冊信息,用戶名:admin,密碼:123456
可以看到我們使用@EventListener注解配置的監聽已經生效了,當我們在UserService內發布了注冊事件時,監聽方法自動被調用並且輸出內信息到控制台。
ApplicationListener實現監聽
這種方式也是Spring之前比較常用的監聽事件方式,在實現ApplicationListener接口時需要將監聽事件作為泛型傳遞,監聽實現代碼如下所示:
package com.yuqiyu.chapter27.listener; import com.yuqiyu.chapter27.bean.UserBean; import com.yuqiyu.chapter27.event.UserRegisterEvent; import org.springframework.context.ApplicationListener; import org.springframework.stereotype.Component; /** * 原始方式實現 * 用戶注冊監聽 * ======================== * Created with IntelliJ IDEA. * User:恆宇少年 * Date:2017/7/21 * Time:10:24 * 碼雲:http://git.oschina.net/jnyqy * ======================== */ @Component public class RegisterListener implements ApplicationListener<UserRegisterEvent> { /** * 實現監聽 * @param userRegisterEvent */ @Override public void onApplicationEvent(UserRegisterEvent userRegisterEvent) { //獲取注冊用戶對象 UserBean user = userRegisterEvent.getUser(); //../省略邏輯 //輸出注冊用戶信息 System.out.println("注冊信息,用戶名:"+user.getName()+",密碼:"+user.getPassword()); } }
我們實現接口后需要使用@Component注解來聲明該監聽需要被Spring注入管理,當有UserRegisterEvent事件發布時監聽程序會自動調用onApplicationEvent方法並且將UserRegisterEvent對象作為參數傳遞。
我們UserService內的發布事件不需要修改,我們重啟下項目再次訪問之前的地址查看控制台輸出的內容如下所示:
2017-07-21 13:03:35.399 INFO 4324 --- [nio-8080-exec-2] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring FrameworkServlet 'dispatcherServlet' 2017-07-21 13:03:35.399 INFO 4324 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization started 2017-07-21 13:03:35.411 INFO 4324 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization completed in 12 ms 注冊信息,用戶名:admin,密碼:123456
我們看到了控制台打印了我們監聽內輸出用戶信息,事件發布后就不會考慮具體哪個監聽去處理業務,甚至可以存在多個監聽同時需要處理業務邏輯。
我們在注冊時如果不僅僅是記錄注冊信息到數據庫,還需要發送郵件通知用戶,當然我們可以創建多個監聽同時監聽UserRegisterEvent事件,接下來我們先來實現這個需求。
郵件通知監聽
我們使用注解的方式來完成郵件發送監聽實現,代碼如下所示:
package com.yuqiyu.chapter27.listener; import com.yuqiyu.chapter27.event.UserRegisterEvent; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; /** * 注冊用戶事件發送郵件監聽 * ======================== * Created with IntelliJ IDEA. * User:恆宇少年 * Date:2017/7/21 * Time:13:08 * 碼雲:http://git.oschina.net/jnyqy * ======================== */ @Component public class RegisterUserEmailListener { /** * 發送郵件監聽實現 * @param userRegisterEvent 用戶注冊事件 */ @EventListener public void sendMail(UserRegisterEvent userRegisterEvent) { System.out.println("用戶注冊成功,發送郵件。"); } }
監聽編寫完成后,我們重啟項目,再次訪問注冊請求地址查看控制台輸出內容如下所示:
2017-07-21 13:09:20.671 INFO 7808 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring FrameworkServlet 'dispatcherServlet' 2017-07-21 13:09:20.671 INFO 7808 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization started 2017-07-21 13:09:20.685 INFO 7808 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization completed in 14 ms 用戶注冊成功,發送郵件。 注冊信息,用戶名:admin,密碼:123456
我們看到控制台輸出的內容感到比較疑惑,我注冊時用戶信息寫入數據庫應該在發送郵件前面,為什么沒有在第一步執行呢?
好了,證明了一點,事件監聽是無序的,監聽到的事件先后順序完全隨機出現的。我們接下來使用SmartApplicationListener實現監聽方式來實現該邏輯。
SmartApplicationListener實現有序監聽
我們對注冊用戶以及發送郵件的監聽重新編寫,注冊用戶寫入數據庫監聽代碼如下所示:
package com.yuqiyu.chapter27.listener; import com.yuqiyu.chapter27.bean.UserBean; import com.yuqiyu.chapter27.event.UserRegisterEvent; import com.yuqiyu.chapter27.service.UserService; import org.springframework.context.ApplicationEvent; import org.springframework.context.event.SmartApplicationListener; import org.springframework.stereotype.Component; /** * 用戶注冊>>>保存用戶信息監聽 * ======================== * Created with IntelliJ IDEA. * User:恆宇少年 * Date:2017/7/21 * Time:10:09 * 碼雲:http://git.oschina.net/jnyqy * ======================== */ @Component public class UserRegisterListener implements SmartApplicationListener { /** * 該方法返回true&supportsSourceType同樣返回true時,才會調用該監聽內的onApplicationEvent方法 * @param aClass 接收到的監聽事件類型 * @return */ @Override public boolean supportsEventType(Class<? extends ApplicationEvent> aClass) { //只有UserRegisterEvent監聽類型才會執行下面邏輯 return aClass == UserRegisterEvent.class; } /** * 該方法返回true&supportsEventType同樣返回true時,才會調用該監聽內的onApplicationEvent方法 * @param aClass * @return */ @Override public boolean supportsSourceType(Class<?> aClass) { //只有在UserService內發布的UserRegisterEvent事件時才會執行下面邏輯 return aClass == UserService.class; } /** * supportsEventType & supportsSourceType 兩個方法返回true時調用該方法執行業務邏輯 * @param applicationEvent 具體監聽實例,這里是UserRegisterEvent */ @Override public void onApplicationEvent(ApplicationEvent applicationEvent) { //轉換事件類型 UserRegisterEvent userRegisterEvent = (UserRegisterEvent) applicationEvent; //獲取注冊用戶對象信息 UserBean user = userRegisterEvent.getUser(); //.../完成注冊業務邏輯 System.out.println("注冊信息,用戶名:"+user.getName()+",密碼:"+user.getPassword()); } /** * 同步情況下監聽執行的順序 * @return */ @Override public int getOrder() { return 0; } }
SmartApplicationListener接口繼承了全局監聽ApplicationListener,並且泛型對象使用的ApplicationEvent來作為全局監聽,可以理解為使用SmartApplicationListener作為監聽父接口的實現,監聽所有事件發布。
既然是監聽所有的事件發布,那么SmartApplicationListener接口添加了兩個方法supportsEventType、supportsSourceType來作為區分是否是我們監聽的事件,只有這兩個方法同時返回true時才會執行onApplicationEvent方法。
可以看到除了上面的方法,還提供了一個getOrder方法,這個方法就可以解決執行監聽的順序問題,return的數值越小證明優先級越高,執行順序越靠前。
注冊成功發送郵件通知監聽代碼如下所示:
package com.yuqiyu.chapter27.listener.order; import com.yuqiyu.chapter27.bean.UserBean; import com.yuqiyu.chapter27.event.UserRegisterEvent; import com.yuqiyu.chapter27.service.UserService; import org.springframework.context.ApplicationEvent; import org.springframework.context.event.SmartApplicationListener; import org.springframework.stereotype.Component; /** * ======================== * Created with IntelliJ IDEA. * User:恆宇少年 * Date:2017/7/21 * Time:13:38 * 碼雲:http://git.oschina.net/jnyqy * ======================== */ @Component public class UserRegisterSendMailListener implements SmartApplicationListener { /** * 該方法返回true&supportsSourceType同樣返回true時,才會調用該監聽內的onApplicationEvent方法 * @param aClass 接收到的監聽事件類型 * @return */ @Override public boolean supportsEventType(Class<? extends ApplicationEvent> aClass) { //只有UserRegisterEvent監聽類型才會執行下面邏輯 return aClass == UserRegisterEvent.class; } /** * 該方法返回true&supportsEventType同樣返回true時,才會調用該監聽內的onApplicationEvent方法 * @param aClass * @return */ @Override public boolean supportsSourceType(Class<?> aClass) { //只有在UserService內發布的UserRegisterEvent事件時才會執行下面邏輯 return aClass == UserService.class; } /** * supportsEventType & supportsSourceType 兩個方法返回true時調用該方法執行業務邏輯 * @param applicationEvent 具體監聽實例,這里是UserRegisterEvent */ @Override public void onApplicationEvent(ApplicationEvent applicationEvent) { //轉換事件類型 UserRegisterEvent userRegisterEvent = (UserRegisterEvent) applicationEvent; //獲取注冊用戶對象信息 UserBean user = userRegisterEvent.getUser(); System.out.println("用戶:"+user.getName()+",注冊成功,發送郵件通知。"); } /** * 同步情況下監聽執行的順序 * @return */ @Override public int getOrder() { return 1; } }
在getOrder方法內我們返回的數值為“1”,這就證明了需要在保存注冊用戶信息監聽后執行,下面我們重啟項目訪問注冊地址查看控制台輸出內容如下所示:
2017-07-21 13:40:43.104 INFO 10128 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring FrameworkServlet 'dispatcherServlet' 2017-07-21 13:40:43.104 INFO 10128 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization started 2017-07-21 13:40:43.119 INFO 10128 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization completed in 15 ms 注冊信息,用戶名:admin,密碼:123456 用戶:admin,注冊成功,發送郵件通知。
這次我們看到了輸出的順序就是正確的了,先保存信息然后再發送郵件通知。
如果說我們不希望在執行監聽時等待監聽業務邏輯耗時,發布監聽后立即要對接口或者界面做出反映,我們該怎么做呢?
使用@Async實現異步監聽
@Aysnc其實是Spring內的一個組件,可以完成對類內單個或者多個方法實現異步調用,這樣可以大大的節省等待耗時。內部實現機制是線程池任務ThreadPoolTaskExecutor,通過線程池來對配置@Async的方法或者類做出執行動作。
線程任務池配置
我們創建一個ListenerAsyncConfiguration,並且使用@EnableAsync注解開啟支持異步處理,具體代碼如下所示:
package com.yuqiyu.chapter27; import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.AsyncConfigurer; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.Executor; /** * 異步監聽配置 * ======================== * Created with IntelliJ IDEA. * User:恆宇少年 * Date:2017/7/21 * Time:14:04 * 碼雲:http://git.oschina.net/jnyqy * ======================== */ @Configuration @EnableAsync public class ListenerAsyncConfiguration implements AsyncConfigurer { /** * 獲取異步線程池執行對象 * @return */ @Override public Executor getAsyncExecutor() { //使用Spring內置線程池任務對象 ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); //設置線程池參數 taskExecutor.setCorePoolSize(5); taskExecutor.setMaxPoolSize(10); taskExecutor.setQueueCapacity(25); taskExecutor.initialize(); return taskExecutor; } @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return null; } } 我們自定義的監聽
異步配置類實現了AsyncConfigurer接口並且實現內getAsyncExecutor方法以提供線程任務池對象的獲取。
我們只需要在異步方法上添加@Async注解就可以實現方法的異步調用,為了證明這一點,我們在發送郵件onApplicationEvent方法內添加線程阻塞3秒,修改后的代碼如下所示:
/** * supportsEventType & supportsSourceType 兩個方法返回true時調用該方法執行業務邏輯 * @param applicationEvent 具體監聽實例,這里是UserRegisterEvent */ @Override @Async public void onApplicationEvent(ApplicationEvent applicationEvent) { try { Thread.sleep(3000);//靜靜的沉睡3秒鍾 }catch (Exception e) { e.printStackTrace(); } //轉換事件類型 UserRegisterEvent userRegisterEvent = (UserRegisterEvent) applicationEvent; //獲取注冊用戶對象信息 UserBean user = userRegisterEvent.getUser(); System.out.println("用戶:"+user.getName()+",注冊成功,發送郵件通知。"); }
下面我們重啟下項目,訪問注冊地址,查看界面反映是否也有延遲。
我們測試發現訪問界面時反映速度要不之前還要快一些,我們去查看控制台時,可以看到注冊信息輸出后等待3秒后再才輸出郵件發送通知,而在這之前界面已經做出了反映。
注意:如果存在多個監聽同一個事件時,並且存在異步與同步同時存在時則不存在執行順序。
總結
我們在傳統項目中往往各個業務邏輯之間耦合性較強,因為我們在service都是直接引用的關聯service或者jpa來作為協作處理邏輯,然而這種方式在后期更新、維護性難度都是大大提高了。然而我們采用事件通知、事件監聽形式來處理邏輯時耦合性則是可以降到最小。