SpringBoot使用ApplicationEvent&Listener完成業務解耦(轉)
轉:https://segmentfault.com/a/1190000011433514
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來作為協作處理邏輯,然而這種方式在後期更新、維護性難度都是大大提高了。然而我們採用事件通知、事件監聽形式來處理邏輯時耦合性則是可以降到最小。