SpringBoot使用ApplicationEvent&Listener完成業務解耦
ApplicationEvent
以及Listener
是Spring為我們提供的一個事件監聽、訂閱的實現,內部實現原理是觀察者設計模式,設計初衷也是為了系統業務邏輯之間的解耦,提高可擴充套件性以及可維護性。事件釋出者並不需要考慮誰去監聽,監聽具體的實現內容是什麼,釋出者的工作只是為了釋出事件而已。
我們平時日常生活中也是經常會有這種情況存在,如:我們在平時拔河比賽中,裁判員給我們吹響了開始的訊號,也就是給我們釋出了一個開始的事件,而拔河雙方人員都在監聽著這個事件,一旦事件釋出後雙方人員就開始往自己方使勁。而裁判並不關心你比賽的過程,只是給你釋出事件你執行就可以了。
本章目標
我們本章在SpringBoot
構建專案
我們本章只是簡單的講解如何使用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註解,該註解會根據方法內配置的事件完成監聽。下面我們啟動專案來測試下我們事件釋出時是否被監聽者所感知。
測試事件監聽
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