springboot自定义事件监听器


事件监听的优势

事件监听实际就是观察者模式的实现,主要为了降低代码耦合,实现通知处理机制。


事件监听的使用场景

示例如下:
公司电商平台新用户注册场景,为了增强用户体验和营销,设计师要求给新用户短信形式推送券包;
实际开发过程如下:
员工A负责开发注册登录模块,涉及到了使用第三方(微信,支付宝,抖音,微博等等)授权注册以及手机验证码注册,本身逻辑已经相对复杂;
员工B负责券包业务处理(包含新手券包,购物返券,平台活动送券,商家独立送券,用户自己领券等等);
单元测试:A,B单独测试都通过了,完美!
对接测试:A,B单独测试都通过了,完美!

根据设计师要求,员工B在注册功能的代码最下方添加了推送新手礼券的功能,感觉简简单单,没啥毛病,联调提测,等待反馈;

过了一会,测试人员给员工A提了个bug工单,内容是“登录出现异常,老用户无法正常登录,新用户可以正常注册登录,请你在提测前先进行内部测试!!!!再有下次我就直接往部门提单子了。”
员工A懵逼了,我tmd测试都通过了,为啥突然就不行了?你还敢威胁我往部门提单子,直接起身去找测试人员评理,来,你tmd给我演示一遍,报啥错,一顿操作,员工A…凸(⊙▂⊙✖ ),差点给测试跪下了,赶紧回去看git,发现员工B的操作记录,什么鬼,写错地方了?故意搞我?一问才知道,产品让加的新需求,裂开,员工A的注册登录是一个流程,新老用户通用,怎么办,只能硬着头皮改,改完后,感觉自己的代码第二天自己就不认识了,木得办法啊。

好了,通过员工A和员工B的通力合作,代码改完了,内测也通过了,联调提测,这次测试通过了,员工A去找测试说明情况,你看这次的bug不是我引起的,我这个月的bug配额快满了,肯请大哥下次注册登录再报错,别提给我,提给员工B可不可行,测试毫不留情地拒绝,doc上写的注册登录就是你的功能,为啥提给别人?此时员工A内心有一句mmp不知当讲不当讲,测试咱惹不起,只能回去弱弱地和员工B说,大哥,下次再有改动,提测前,你先内部跑一遍吧。

大功告成,项目终于怀胎九月,上线了,前期营销做的好,用户量蹭蹭往上涨,转眼间过去半个月,产品和运营又开始敲黑板了:根据反馈,咱们营销短信被拦截的概率挺高,要想个其他的办法,这次咱们来个大活,直接三连(短信,手机邮箱,极光推送),多管齐下,用户必定能收到,这样销量还不翻个倍?参与这次开发的人员都留下,加个班,辛苦一下,公司给大家点外卖。

员工A认为这和自己无关,好容易挤上地铁,好心情就被一通电话无情地摧毁,只好高高兴兴(骂骂咧咧)地扒拉着公司点的鸡腿饭;
员工C(邮件业务),员工D(极光推送业务)开始加班对接第三方,加班蹂躏员工A的注册代码,员工A只能在角落里瑟瑟发抖。
由于不是核心业务,测试一遍通过,感觉没啥问题,直接上线了,员工A的心理是没一点底气,回到家,游戏准备上高地了,运维打电话过来😨,注册功能时好时坏,报了一大堆错,快回来看看bug平台,老板也在往公司赶;这个时候,员工A想死的心都有了,一边是理想(快要拿下的焦灼局),一边是现实,突然就想起了张玮玮的米店:你一手拿着苹果,一手拿着命运,穿着拖鞋打个车,就往公司跑,经过一番折腾,发现是员工C的邮件接口连接超时,员工D的推送接口异常返回提示超限,老板劈里啪啦一顿骂,还不是你的注册接口不健壮,怎么能让其他业务影响了核心注册功能,吧啦吧啦…,员工A只能( ¯▽¯;),老板说得对,我现在就改,只能使用try-cache大法,一顿操作,齐活,线上的注册功能终于恢复了,其他的就等着员工C,D慢慢改自己的业务;

又过了半个月,敲黑板的又来了:哎呀,这段时间,咱们运营做得好,用户体量上来了,接到部分用户反馈,咱们的注册太烦了,一大堆通知,又是短信又是邮件又是推送的,为了优化用户体验,咱们把这些东西都去掉,统一改成新用户登录成功,直接在首页的广告屏展示发送的券包,老用户中近期没参与活动的,顺便也给发个平台抵用券,登录后区分首次展示,这次的任务不着急,技术部门想办法两天之内解决就行。

员工A预感到又有一场似曾相识的遭遇即将来临,不想再当背锅侠的他,在部门会议上勇敢地站出来,请大家别再蹂躏我的注册功能了,咱们使用事件监听,你们要什么核心数据,我注册的时候给你们提供,发布个事件,剩下的,你们哪个业务有需要,就自己监听,不需要了,就把监听器注释掉,互不干涉吧啦吧啦…,这样自定义事件监听器就来了。


springboot事件监听器的核心接口和流程

事件监听流程

ApplicationEvent:事件抽象类,自定义事件必须继承该类,表示某一类事件,并显示定义构造方法,可以自定义一些参数,供监听器获取并开展业务处理;
ApplicationEventPublisher:事件发布者,业务层可以使用applicationEventPublisher.publishEvent(event)发布对应的事件;
ApplicationEventMulticaster:是一个多播器,用于管理事件监听者(监听器),给其通知广播事件,如果不自定义bean,那么会默认创建SimpleApplicationEventMulticaster,SimpleApplicationEventMulticaster是同步的广播,如果 listener 过多,会使得应用阻塞,如果传入 Executor 就会使用异步广播;
ApplicationListener:事件监听器接口,用于监听事件并处理。


springboot自定义事件、监听器

talk is cheap show me the code

1. 自定义事件核心参数实体类

@Data
public class RegisterEventDTO {
    /**
     * 用户id
     */
    private Integer id;
    /**
     * 用户名称
     */
    private String name;

    /**
     * 是否是新用户,ture/false
     */
    private Boolean newUser;
    /**
     * 手机号
     */
	private String phone;
}

2. 自定义事件

@Setter
@Getter
public class RegisterEvent extends ApplicationEvent {

    private RegisterEventDTO registerEventDTO;
    public RegisterEvent(Object source,RegisterEventDTO registerEventDTO) {
        super(source);
        this.registerEventDTO = registerEventDTO;
    }
}

3.自定义监听器:(同步执行)

3.1 方式一:实现ApplicationListener接口,重写onApplicationEvent方法

/**
 * <p>Description: 用户注册登录监听器:发送营销短信简单示例 </p>
 */
@Component
@RequiredArgsConstructor
public class RegisterSmsListener implements ApplicationListener<RegisterEvent> {
    private final SmsService smsService;
    @Value("new.coupon")
    private Integer coupon;
    
    @Override
    public void onApplicationEvent(RegisterEvent event) {
        System.out.println("-------start sms");
        SmsCouponVO smsCoupon = new SmsCouponVO();
        smsCoupon.setCoupon(coupon);
        smsCoupon.setUserName(event.getRegisterEventDTO().getName());
        smsCoupon.setPhone(event.getRegisterEventDTO().getPhone());
        smsService.sendNewUserCode(smsCoupon);
        System.out.println("------end sms");
    }
}

3.2 方式二:@EventListener注解

/**
 * <p>Description: 用户注册登录监听器:新用户发放券包简单示例 </p>
 */
@Component
@RequiredArgsConstructor
public class RegisterCouponListener {
    private final CouponUserService couponUserService;
    @Value("new.coupon")
    private Integer coupon;

    @EventListener
    public void couponListener(RegisterEvent registerEvent){
        System.out.println("---------start coupon db");
        CouponUser couponUser= new CouponUser();
        couponUser.setUserId(registerEvent.getRegisterEventDTO().getId());
        couponUser.setBizId(xx);
        couponUser.setBizType(xx);
       	...
        couponUserService.sendCoupon(couponUser);
        System.out.println("---------end coupon db");
    }
}

4.发布事件

@Service
@RequiredArgsConstructor
public class RegisterServiceImpl implements RegisterService {
    private final ApplicationEventPublisher applicationEventPublisher;
    
    @Override
    public void register(UserInfo userInfo){
    	System.out.println("-----------begin register");
        ...
        //执行用户注册核心业务操作,并完善RegisterEventDTO 所需参数
        to do something
        ...
        RegisterEventDTO registerEventDTO = new RegisterEventDTO();
        registerEventDTO.setName(userInfo.getName());
        registerEventDTO.setId(userInfo.getId());
        registerEventDTO.setPhone(userInfo.getPhone());
        registerEventDTO.set(userInfo.getId());
        registerEventDTO.setNewUser(true);
        //发布事件
        applicationEventPublisher.publishEvent(new RegisterEvent(this,registerEventDTO));
        System.out.println("-----------end register");
    }
}

5.单元测试输出

@SpringBootTest(classes = WebappApplication.class)
@RunWith(SpringRunner.class)
class WebappApplicationTests {
	@Autowired
    RegisterService registerService;
    
    @Test
    void testEvent(){
    	UserInfo userInfo = new UserInfo();
    	userInfo.setPhone(xxx);
    	userInfo.setName(xxx);
    	// ...
        registerService.register(userInfo);
    }
}

输出结果如下:
-----------begin register
---------start coupon db
---------end coupon db
-------start sms
-------end sms
-----------end register

细心的同学,这里就会发现,事件监听器之间是同步的,只能一个接一个执行,事件监听器和核心业务也是同步执行的,核心业务还需要等待监听器的处理逻辑走完,才能返回,而且多个监听器没有固定执行顺序(添加@Order()来控制顺序)

6.实现异步监听

6.1 自定义线程池

@Configuration
@EnableAsync
public class ThreadPoolTaskConfig extends AsyncConfigurerSupport {
    /** 线程池核心池的大小 */
    private int corePoolSize = 5;
    /** 线程池的最大线程数 */
    private int maxPoolSize = 9;
    /** 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间s */
    private int keepAliveTime = 10;
    /**队列数量 */
    private int queueCapacity = 100;
    /**等待时长 */
    private int awaitTerminationSeconds = 60;

    /**
     * 注册线程池
     * @return
     */
    @Bean("asyncThreadPoolTaskExecutor")
    public ThreadPoolTaskExecutor createAsyncThreadPoolTaskExecutor(){
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        threadPoolTaskExecutor.setCorePoolSize(corePoolSize);
        threadPoolTaskExecutor.setMaxPoolSize(maxPoolSize);
        threadPoolTaskExecutor.setKeepAliveSeconds(keepAliveTime);
        //调度器shutdown被调用时等待当前被调度的任务完成
        threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);
        threadPoolTaskExecutor.setAwaitTerminationSeconds(awaitTerminationSeconds);
        threadPoolTaskExecutor.setQueueCapacity(queueCapacity);
        threadPoolTaskExecutor.setThreadNamePrefix("TaskExecutorProduct-");
        // 线程池对拒绝任务(无线程可用)的处理策略
        threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return threadPoolTaskExecutor;
    }

    @Override
    public Executor getAsyncExecutor() {
        return createAsyncThreadPoolTaskExecutor();
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (ex, method, params) -> System.out.println(String.format("taskConfig-'%s'", method, ex));
    }
}

6.2 实现异步监听

/**
 * <p>Description: 用户注册登录监听器:新用户发放券包简单示例 </p>
 */
@Component
@RequiredArgsConstructor
public class RegisterCouponListener {
    private final CouponUserService couponUserService;
    @Value("new.coupon")
    private Integer coupon;

    @EventListener
    @Async(value = "asyncThreadPoolTaskExecutor")
    public void couponListener(RegisterEvent registerEvent){
        System.out.println("---------start coupon db");
        CouponUser couponUser= new CouponUser();
        couponUser.setUserId(registerEvent.getRegisterEventDTO().getId());
        couponUser.setBizId(xx);
        couponUser.setBizType(xx);
       	...
        couponUserService.sendCoupon(couponUser);
        System.out.println("---------end coupon db");
    }
}
/**
 * <p>Description: 用户注册登录监听器:发送营销短信简单示例 </p>
 */
@Component
@RequiredArgsConstructor
public class RegisterSmsListener implements ApplicationListener<RegisterEvent> {
    private final SmsService smsService;
    @Value("new.coupon")
    private Integer coupon;
    
    @Override
    @Async(value = "asyncThreadPoolTaskExecutor")
    public void onApplicationEvent(RegisterEvent event) {
        System.out.println("-------start sms");
        SmsCouponVO smsCoupon = new SmsCouponVO();
        smsCoupon.setCoupon(coupon);
        smsCoupon.setUserName(event.getRegisterEventDTO().getName());
        smsCoupon.setPhone(event.getRegisterEventDTO().getPhone());
        smsService.sendNewUserCode(smsCoupon);
        System.out.println("------end sms");
    }
}

总结

如果生产业务中,核心业务和附属业务没有强一致性要求,例如:用户注册完成后,发送短信,邮件,推送这类型子业务,建议采用异步监听,主线程无需等待监听器执行结果,发布完事件后,就能直接返回;如果有强一致要求,最好是选择同步执行,并添加事务@Transactional。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

人生大事

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值