摘要:實體代碼如下所示恒宇少年碼云用戶名密碼創建內添加一個注冊方法,該方法只是實現注冊事件發布功能,代碼如下所示恒宇少年碼云用戶注冊方法省略其他邏輯發布事件事件發布是由對象管控的,我們發布事件前需要注入對象調用方法完成事件發布。
ApplicationEvent以及Listener是Spring為我們提供的一個事件監聽、訂閱的實現,內部實現原理是觀察者設計模式,設計初衷也是為了系統業務邏輯之間的解耦,提高可擴展性以及可維護性。事件發布者并不需要考慮誰去監聽,監聽具體的實現內容是什么,發布者的工作只是為了發布事件而已。
本章目標我們平時日常生活中也是經常會有這種情況存在,如:我們在平時拔河比賽中,裁判員給我們吹響了開始的信號,也就是給我們發布了一個開始的事件,而拔河雙方人員都在監聽著這個事件,一旦事件發布后雙方人員就開始往自己方使勁。而裁判并不關心你比賽的過程,只是給你發布事件你執行就可以了。
我們本章在SpringBoot平臺上通過ApplicationEvents以及Listener來完成簡單的注冊事件流程。
構建項目我們本章只是簡單的講解如何使用ApplicationEvent以及Listener來完成業務邏輯的解耦,不涉及到數據交互所以依賴需要引入的也比較少,項目pom.xml配置文件如下所示:
.....//省略.....//省略 org.springframework.boot spring-boot-starter-web org.projectlombok lombok 1.16.16 org.springframework.boot spring-boot-starter-test test
其中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參數是我們自定義的注冊用戶對象,該對象可以在監聽內被獲取。
創建UserBean在Spring內部中有多種方式實現監聽如:@EventListener注解、實現ApplicationListener泛型接口、實現SmartApplicationListener接口等,我們下面來講解下這三種方式分別如何實現。
我們簡單創建一個用戶實體,并添加兩個字段:用戶名、密碼。實體代碼如下所示:
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{ /** * 實現監聽 * @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實現監聽方式來實現該邏輯。
我們對注冊用戶以及發送郵件的監聽重新編寫,注冊用戶寫入數據庫監聽代碼如下所示:
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來作為協作處理邏輯,然而這種方式在后期更新、維護性難度都是大大提高了。然而我們采用事件通知、事件監聽形式來處理邏輯時耦合性則是可以降到最小。
本章代碼已經上傳到碼云:
網頁地址:http://git.oschina.net/jnyqy/lessons
Git地址:https://git.oschina.net/jnyqy/lessons.git
SpringBoot相關系列文章請訪問:目錄:SpringBoot學習目錄
QueryDSL相關系列文章請訪問:QueryDSL通用查詢框架學習目錄
感謝閱讀!
歡迎加入QQ技術交流群,共同進步。
QQ技術交流群
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/67677.html
摘要:準備好了我們可以開始向中發布,當發布后,所有在中的都會收到對應的。將類注入到的中。測試和通過方法將發布到應用上下文中,同時這個動作會觸發收到事件。深入剖析發布與監聽的過程在使用方法發布的時候,最終會調用到中的類的如下的一段代碼。 本篇主要來聊一聊spring中ApplicationListener接口和ApplicationEvent類。 從命名上可以很容易的看出來一個是listene...
摘要:基于版本基于版本。由于中英行文差異,完全的逐字逐句翻譯會很冗余啰嗦。譯者在翻譯中同時參考了谷歌百度有道翻譯的譯文以及編程思想第四版中文版的部分內容對其翻譯死板,生造名詞,語言精煉度差問題進行規避和改正。 來源:LingCoder/OnJava8 主譯: LingCoder 參譯: LortSir 校對:nickChenyx E-mail: 本書原作者為 [美] Bru...
摘要:第四章安全管理制度發布第十條安全管理制度必須以正式文件的形式發布施行。第十一條安全管理制度由信息安全管理小組制訂,信息安全領導小組審批發布。第二十條安全管理制度的修改與廢止須經信息安全領導組織審批確認,信息安全管理部門備案。 字數 3610閱讀 760評論 0贊 3《xxxx安全管理制度匯編》****制度管理辦法****文...
摘要:寫一個正則表達式來測試變量中是否包含字符串。用函數給出不使用字符,但和等價的正則表達式。第十四課標志全局匹配標志第二個常用的標志是全局匹配標志,用字母表示。寫出一個正則表達式來檢驗合法性。非捕獲組的主要用途是給一個組賦予量詞。 TRY REGEX 是一個交互式的正則表達式學習項目項目地址:https://github.com/callumacra...在線地址:http://tryre...
摘要:貢獻者飛龍版本最近總是有人問我,把這些資料看完一遍要用多長時間,如果你一本書一本書看的話,的確要用很長時間。為了方便大家,我就把每本書的章節拆開,再按照知識點合并,手動整理了這個知識樹。 Special Sponsors showImg(https://segmentfault.com/img/remote/1460000018907426?w=1760&h=200); 貢獻者:飛龍版...
閱讀 2069·2021-11-16 11:45
閱讀 569·2021-11-04 16:12
閱讀 1369·2021-10-08 10:22
閱讀 840·2021-09-23 11:52
閱讀 4128·2021-09-22 15:47
閱讀 3513·2021-09-22 15:07
閱讀 485·2021-09-03 10:28
閱讀 1730·2021-09-02 15:21