Spring的事件通知機制是一項很有用的功能,使用事件機制可將相互耦合的代碼解耦,從而方便功能的開發。
1.入門案例
1.1環境准備
新建一個SpringBoot的項目,導入web的依賴,編寫一個controller接口:
package com.zys.springboottestexample.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Map; @RestController @RequestMapping("/user") public class UserController { @PostMapping("/save") public void save(Map map) { System.out.println(map); } }
1.2使用Spring Event
1)使用說明
使用用事件需要以下的幾個步驟:
第一:定義事件,繼承ApplicationEvent
第二:定義監聽,實現ApplicationListener接口或添加注解@EventListener
第三:發布事件,調用ApplicationEventPublisher.publishEvent()或ApplicationContext.publishEvent()
2)定義事件
package com.zys.springboottestexample.event; import org.springframework.context.ApplicationEvent; // 定義一個事件 public class EventDemo extends ApplicationEvent { private String message; public EventDemo(Object source, String message) { super(source); this.message = message; } public String getMessage() { return message; } }
3)定義監聽
package com.zys.springboottestexample.listener; import com.zys.springboottestexample.entity.EventDemo; import org.springframework.context.ApplicationListener; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; // 定義一個事件監聽者 @Component public class EventDemoListener implements ApplicationListener<EventDemo> { @Override public void onApplicationEvent(EventDemo event) { System.out.println("當前線程:" + Thread.currentThread().getId()); System.out.println("receiver " + event.getMessage()); } }
4)發布事件
package com.zys.springboottestexample.service; import com.zys.springboottestexample.entity.EventDemo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Component; // 事件發布的方法 @Component public class EventPublishService { @Autowired private ApplicationEventPublisher applicationEventPublisher; public void publish(String message) { EventDemo demo = new EventDemo(this, message); applicationEventPublisher.publishEvent(demo); } }
5)觸發事件
當事件發布后,在需要的地方就可以觸發事件了。在上述UserController中觸發:
@Autowired private EventPublishService eventPublishService; @PostMapping("/save") public void save(Map map) { System.out.println(map); System.out.println("當前線程:" + Thread.currentThread().getId()); eventPublishService.publish("添加成功"); }
6)測試。訪問localhost:8080/user/save,在控制台打印了信息
7)異步處理
上述看到打印結果分析,處理接口的線程和事件監聽使用的是同一個線程。一般會使用異步的方式。
第一步:首先在事件監聽的方法上添加異步的注解@Async
第二步:然后在啟動類上啟用異步@EnableAsync
第三步:重啟項目,再次訪問上述接口,控制台打印的線程id就不一樣了
2.實際應用
2.1應用場景
當然在實際應用中,Spring Event因解耦的特性也顯得格外重要。比如現有一個請假申請的方法,在申請保存到數據庫的同時需要給上級領導發送系統和郵件通知。當然可以在保存申請的代碼后面假設這些操作,但是這樣的代碼違反了設計模式的多項原則:單一職責原則、迪米特法則、開閉原則。也就是說,比如將來評論添加成功之后還需要有發送短信通知,這時又要去修改存申請代碼才能符合需求。若使用了事件通知機制,則無需修改原有功能,只需在發布通知功能中調用短信發送功能即可。
2.2自定義日志收集的starter
本章節通過自定義一個starter,名為log-spring-boot-starter,用來攔截用戶請求並收集操作日志信息,收集的信息會通過監聽器返回給使用者,使用者再獲取。
源碼:https://github.com/zhongyushi-git/zxh-starter-collection.git
2.2.1開發步驟
具體步驟如下:
1)定義日志對象LogDTO
2)定義日志操作事件類LogEvent
3)定義@Log注解
4)定義切面類LogAspect
5)在切面類LogAspect中定義切點,攔截Controller中添加@Log注解的方法
6)在切面類LogAspect中定義前置通知,在前置通知方法中收集操作日志相關信息封裝為LogDTO對象並保存到ThreadLocal中
7)在切面類LogAspect中定義后置通知,在后置通知方法中通過ThreadLocal獲取LogDTO並繼續設置其他的操作信息到LogDTO
8)在切面類LogAspect的后置通知方法中發布事件LogEvent
9)定義監聽器LogListener,監聽日志發布事件LogEvent
10)定義配置類LogAutoConfiguration,用於自動配置切面LogAspect對象。在配置類有一個屬性sys.log.enabled
,表示是否啟用日志收集,值true啟用,值false不啟用,若不配置時也生效。
11)定義starter所需的META-INF/spring.factories文件,配置自動配置類
2.2.2說明
1)使用@Log注解時,參數value是接口的描述,type是日志的類型,1表示操作日志,2表示登錄日志。
2)代碼中的自動配置類的關鍵注解說明:
注解名稱 | 描述 |
@ConditionalOnWebApplication |
只有當前項目是Web項目的條件下生效 |
@ConditionalOnProperty |
指定的屬性是否有指定的值,通過havingValue與配置文件中的值對比,返回為true則配置類生效,反之失效 |
@ConditionalOnMissingBean |
用來修飾bean,當注冊此bean時,會檢查是否已經注冊過此Bean,若注冊過就不會再次注冊,若沒有注冊過則進行注冊,保證此bean只有一個。 |
2.3使用日志收集的starter
1)新建SpringBoot的項目,導入web和此starter依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.zxh.boot</groupId>
<artifactId>log-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
2)定義LogService類,用於保存日志信息
package com.zys.springboottestexample.service; import com.zxh.boot.log.entity.LogDTO; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @Service @Slf4j public class LogService { public void saveLog(LogDTO logDTO){ log.info(logDTO.toString()); //保存到數據庫 } }
這里只是打印,實際中將對象按需求存入數據庫即可
3)定義配置類LogConfiguration,用於初始化監聽
package com.zys.springboottestexample.config; import com.zxh.boot.log.listener.LogListener; import com.zys.springboottestexample.service.LogService; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * 日志配置類 */ @Configuration public class LogConfiguration { /** * 初始化監聽器, * @param logService * @return */ @Bean @ConditionalOnMissingBean public LogListener logListener(LogService logService){ //函數式接口 return new LogListener(logDTO -> logService.saveLog(logDTO)); } }
當日志事件發布后,會在Log監聽器中進行監聽,並調用consumer.accept()方法,而在初始化監聽器中函數式接口又指明了調用的方法,則最終會自動調用logService.saveLog()方法。
4)創建UserController類,定義一個接口,添加日志注解
package com.zys.springboottestexample.controller; import com.zxh.boot.log.annotation.Log; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/user") public class UserController { @GetMapping("/save") @Log("用戶添加") public String save(String name) { int t=10/0; return "你好啊"; } }
5)啟動項目,訪問localhost:8080/user/save?name=123即可在控制台看到打印的日志對象
從控制台可以看出,雖然在代碼中故意制造了異常,但仍然有日志信息,那么這些日志被記錄到數據庫,就可查詢錯誤的信息,這就體現了日志的重要性。
6)若不再使用此starter,除了刪除其依賴外,還可以直接在配置文件中配置:
sys.log.enabled=false
那么即使加了@Log注解,也不會生效。
7)獲取登錄用戶信息
可以通過設置請求頭header讓日志獲取登錄用戶信息,需要設置的有兩個,分別是userId和userName,例如: