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

