springboot自定义事件监听器

文章目录

  • 事件监听的优势
  • 事件监听的使用场景
  • springboot事件监听器的核心接口和流程
  • springboot自定义事件、监听器
    • 1. 自定义事件核心参数实体类
    • 2. 自定义事件
    • 3.自定义监听器:(同步执行)
      • 3.1 方式一:实现ApplicationListener接口,重写onApplicationEvent方法
      • 3.2 方式二:@EventListener注解
    • 4.发布事件
    • 5.单元测试输出
    • 6.实现异步监听
      • 6.1 自定义线程池
      • 6.2 实现异步监听
  • 总结


事件监听的优势

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


事件监听的使用场景

示例如下:
公司电商平台新用户注册场景,为了增强用户体验和营销,设计师要求给新用户短信形式推送券包;
实际开发过程如下:
员工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事件监听器的核心接口和流程

springboot自定义事件监听器_第1张图片

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方法

/**
 * 

Description: 用户注册登录监听器:发送营销短信简单示例

*/
@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注解

/**
 * 

Description: 用户注册登录监听器:新用户发放券包简单示例

*/
@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 实现异步监听

/**
 * 

Description: 用户注册登录监听器:新用户发放券包简单示例

*/
@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"); } }
/**
 * 

Description: 用户注册登录监听器:发送营销短信简单示例

*/
@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。

你可能感兴趣的:(#,spring,boot技术进阶,spring,boot,java,后端)