SpringBoot實現(xiàn)異步事件Event詳解
SpringBoot實現(xiàn)異步事件
為什么需要用到Spring Event?
我簡單說一個場景,大家都能明白: 你在公司內部,寫好了一個用戶注冊的功能
然后產品經理根據(jù)公司情況,新增以下需求
- 注冊新用戶,給新用戶發(fā)郵件
- 發(fā)放新用戶優(yōu)惠券
public void registerUser(AddUserRequest request){ //插入用戶 userService.insertUser(request); }
實現(xiàn)需求后:
public void registerUser(AddUserRequest request){ //插入用戶 User user = convertToUser(request) userService.insertUser(user); //發(fā)郵件 sendEmail(user); //發(fā)放優(yōu)惠券 sendCouponToUser(user); }
這樣正常寫的話,會有以下缺點:
- 發(fā)郵件方法里面,如果郵件服務出現(xiàn)問題,就會影響到注冊用戶的核心業(yè)務,無論發(fā)郵件成不成功,都不應影響注冊用戶
- 發(fā)放優(yōu)惠券,產品經理會根據(jù)市場需求要求你反復去掉刪除,要是沒有一些措施,很容易被產品經理"耍猴",而且反復改代碼會導致功能不穩(wěn)定。
更理論的話來說,就是把一些次要的功能耦合到核心功能里面,且經常調整,會導致核心功能不穩(wěn)定
解決方案: 將發(fā)放優(yōu)惠券,發(fā)送郵件做成單獨的服務A和B。 注冊業(yè)務在注冊用戶成功后,發(fā)布一個"注冊成功"的消息。
服務A和服務B相當于一個監(jiān)聽者,都監(jiān)聽**"注冊成功"的消息**,監(jiān)聽到后,服務A和B就各自做自己的事情了。 服務A和服務B不需要關心到底是誰,哪個地方發(fā)出了這個消息,它只需要監(jiān)聽此消息并做出反應。
這種方式的好處是:
- 如果不想要發(fā)放優(yōu)惠券的功能,直接把服務A的代碼去掉就好了,而且由于跟注冊用戶解耦,可以不用擔心影響到注冊功能。
- 如果想要做更多的次要業(yè)務,例如注冊時發(fā)短信通知,可以增加一個服務C監(jiān)聽**"注冊成功"的消息**,然后服務C進行自己的服務就行。不需要更改注冊用戶的代碼。
上面這種模式就是事件模式。
Spring Event 的使用
注解方式實現(xiàn)
我用注解的方式去實現(xiàn)Spring Event的使用 事件對象:
@Data public class RegisterUserEvent { /** * 用戶id */ private Integer userId; /** * 用戶名 */ private String userName; }
接口:
@RestController @Api(tags="測試前端控制器") @RequiredArgsConstructor public class TestController { private final TestService testService; @ApiOperation(value="模擬注冊用戶功能的發(fā)送事件", notes="\n 開發(fā)者:") @PostMapping("/sendEvent") public JsonResult sendEvent(){ testService.sendEvent(); return JsonResult.success(); } }
注冊功能:
/** * @author zhengbingyuan * @date 2023/2/6 */ @Slf4j @Service @RequiredArgsConstructor public class TestService { private final ApplicationEventPublisher eventPublisher; /** * 模擬一個注冊用戶的功能 */ @Transactional(rollbackFor = Exception.class) public void sendEvent() { log.info("開始注冊用戶...."); UserDto dto = saveUser(); RegisterUserEvent userEvent = new RegisterUserEvent(); userEvent.setUserId(dto.getId()); userEvent.setUserName(dto.getUserName()); eventPublisher.publishEvent(userEvent); } private UserDto saveUser() { int id = 1; String userName = "超人"; log.info("保存用戶id: {},name:{}",id,userName); UserDto dto = new UserDto(); dto.setId(id); dto.setUserName(userName); return dto; } }
次要業(yè)務的事件監(jiān)聽:
/** * @author zhengbingyuan * @date 2023/2/6 */ @Slf4j @Component public class RegisterUserEventListener { @EventListener public void processSendCouponToUser(RegisterUserEvent event){ log.info("發(fā)放優(yōu)惠券給用戶:{}",event.getUserName()); } @EventListener public void processSendEmailToUser(RegisterUserEvent event){ log.info("發(fā)放郵件給用戶:{}",event.getUserName()); } }
結果:
2023-02-06 16:47:30,228:INFO http-nio-8083-exec-2 [] (TestService.java:28) - 開始注冊用戶....
2023-02-06 16:47:30,229:INFO http-nio-8083-exec-2 [] (TestService.java:40) - 保存用戶id: 1,name:超人
2023-02-06 16:47:30,232:INFO http-nio-8083-exec-2 [] (RegisterUserEventListener.java:17) - 發(fā)放優(yōu)惠券給用戶:超人
2023-02-06 16:47:30,232:INFO http-nio-8083-exec-2 [] (RegisterUserEventListener.java:23) - 發(fā)放郵件給用戶:超人
小結
上面將注冊的主要邏輯(用戶信息落庫)和次要的業(yè)務邏輯(發(fā)送郵件)通過事件的方式解耦了。次要的業(yè)務做成了可插拔的方式,比如不想發(fā)送郵件了,只需要將郵件監(jiān)聽器上面的@Component注釋就可以了,非常方便擴展。
Spring Event異步模式
對于上面的程序,如果發(fā)送郵件出現(xiàn)異常的話,根據(jù)實踐,整個注冊功能會受到影響,也就是上面的程序僅只實現(xiàn)了代碼可拔插的效果。 如果將發(fā)送郵件這一個功能完全解耦出來,還需要做成異步事件模式。
先看看事件監(jiān)聽器是怎么實現(xiàn)的 在注解方式的publishEvent方法底層,會通過getApplicationEventMulticaster().multicastEvent(event)來派發(fā)事件。這個getApplicationEventMulticaster()獲得的對象是SimpleApplicationEventMulticaster。
SimpleApplicationEventMulticaster 里面有一個taskExecutor 的線程池,如果這個線程池不是null,那么將會使用這個線程池去消費事件消息。
@Override public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) { ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event)); Executor executor = getTaskExecutor(); for (ApplicationListener<?> listener : getApplicationListeners(event, type)) { if (executor != null) { //線程池調用 executor.execute(() -> invokeListener(listener, event)); } else { //直接調用 invokeListener(listener, event); } } }
所以,只要讓executor 不為null,就能使用異步事件了。但是默認情況下executor是空的,此時需要我們來給其設置一個值。
怎么設置這個值,這需要看回去ApplicationEventMulticaster是怎么初始化的,這個對象是在AbstractApplication.refresh()中的initApplicationEventMulticaster()方法執(zhí)行。
protected void initApplicationEventMulticaster() { ConfigurableListableBeanFactory beanFactory = getBeanFactory(); if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) { this.applicationEventMulticaster = beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class); if (logger.isTraceEnabled()) { logger.trace("Using ApplicationEventMulticaster [" + this.applicationEventMulticaster + "]"); } } else { this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory); beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster); if (logger.isTraceEnabled()) { logger.trace("No '" + APPLICATION_EVENT_MULTICASTER_BEAN_NAME + "' bean, using " + "[" + this.applicationEventMulticaster.getClass().getSimpleName() + "]"); } } }
通過初始化方法,可以得知,只要存在name = "applicationEventMulticaster" 的bean,那么就不會創(chuàng)建SimpleApplicationEventMulticaster 實例。 換句話說,只要開發(fā)者在配置類,提供一個設置好taskExecutor的SimpleApplicationEventMulticaster 就可以使用異步事件了。
/** * @author zhengbingyuan * @date 2023/2/6 */ @Configuration @RequiredArgsConstructor public class AsyncEventConfiguration { @Bean public SimpleApplicationEventMulticaster applicationEventMulticaster(BeanFactory beanFactory) { SimpleApplicationEventMulticaster applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory); //設置線程池 applicationEventMulticaster.setTaskExecutor(eventExecutor()); return applicationEventMulticaster; } @Bean public TaskExecutor eventExecutor() { ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor(); //核心線程數(shù) int corePoolSize = 5; threadPoolTaskExecutor.setCorePoolSize(corePoolSize); //最大線程數(shù) int maxPoolSize = 10; threadPoolTaskExecutor.setMaxPoolSize(maxPoolSize); //隊列容量 int queueCapacity = 10; threadPoolTaskExecutor.setQueueCapacity(queueCapacity); //拒絕策略 threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy()); //線程名前綴 String threadNamePrefix = "eventExecutor-"; threadPoolTaskExecutor.setThreadNamePrefix(threadNamePrefix); threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true); // 使用自定義的跨線程的請求級別線程工廠類19 int awaitTerminationSeconds = 5; threadPoolTaskExecutor.setAwaitTerminationSeconds(awaitTerminationSeconds); threadPoolTaskExecutor.initialize(); return threadPoolTaskExecutor; } }
繼續(xù)使用上面所說的例子,由于我log日志有加線程前綴,這里就不用加線程阻塞手段去測試了。
結果:可以看出,次要業(yè)務和核心業(yè)務已經是發(fā)生在不同的線程上了
2023-02-06 18:22:19,865:INFO http-nio-8083-exec-2 [] (TestService.java:28) - 開始注冊用戶....
2023-02-06 18:22:19,866:INFO http-nio-8083-exec-2 [] (TestService.java:41) - 保存用戶id: 1,name:超人
2023-02-06 18:22:19,866:INFO http-nio-8083-exec-2 [] (TestService.java:35) - 注冊用戶完成
2023-02-06 18:22:19,866:INFO eventExecutor-3 [] (RegisterUserEventListener.java:17) - 發(fā)放優(yōu)惠券給用戶:超人
2023-02-06 18:22:19,866:INFO eventExecutor-7 [] (RegisterUserEventListener.java:23) - 發(fā)放郵件給用戶:超人
小結: 異步線程的使用,在次要業(yè)務代碼可拔插的情況下,進一步解耦,即使次要業(yè)務出問題,也不影響核心業(yè)務。
事件使用建議
異步事件的模式,通常將一些非主要的業(yè)務放在監(jiān)聽器中執(zhí)行,因為監(jiān)聽器中存在失敗的風險,所以使用的時候需要注意。
如果只是為了解耦,但是被解耦的次要業(yè)務也是必須要成功的,可以使用消息中間件的方式(落地+重試機制)來解決這些問題。
到此這篇關于SpringBoot實現(xiàn)異步事件Event詳解的文章就介紹到這了,更多相關SpringBoot實現(xiàn)異步事件內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
解決JDK異常處理No appropriate protocol問題
這篇文章主要介紹了解決JDK異常處理No appropriate protocol問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-06-06Servlet和Filter之間的區(qū)別與聯(lián)系
這篇文章主要介紹了Servlet和Filter之間的區(qū)別與聯(lián)系的相關資料,需要的朋友可以參考下2016-05-05IntelliJ IDEA中出現(xiàn)"PSI and index do not match"錯誤的解決辦法
今天小編就為大家分享一篇關于IntelliJ IDEA中出現(xiàn)"PSI and index do not match"錯誤的解決辦法,小編覺得內容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2018-10-10如何使用SpringBootCondition更自由地定義條件化配置
這篇文章主要介紹了如何使用SpringBootCondition更自由地定義條件化配置,幫助大家更好的理解和學習使用springboot框架,感興趣的朋友可以了解下2021-04-04Matplotlib可視化之自定義顏色繪制精美統(tǒng)計圖
matplotlib提供的所有繪圖都帶有默認樣式.雖然這可以進行快速繪圖,但有時可能需要自定義繪圖的顏色和樣式,以對繪制更加精美、符合審美要求的圖像.matplotlib的設計考慮到了此需求靈活性,很容易調整matplotlib圖形的樣式,需要的朋友可以參考下2021-06-06SpringBoot如何使用mail實現(xiàn)登錄郵箱驗證
在實際的開發(fā)當中,不少的場景中需要我們使用更加安全的認證方式,同時也為了防止一些用戶惡意注冊,我們可能會需要用戶使用一些可以證明個人身份的注冊方式,如短信驗證、郵箱驗證等,這篇文章主要介紹了SpringBoot如何使用mail實現(xiàn)登錄郵箱驗證,需要的朋友可以參考下2024-06-06