深入学习SpringBoot中的应用事件和监听器

2022-07-25 07:57:17
1 引言

在项SpringBoot目中我们经常需要一些监听器,通过监听一些事件做出相应的反应,并确保组件在生命周期内正常运行。比如上下文加载监听器。

ContextLoaderListener监听ServletContextEvent用于确定何时初始化和销毁应用上下文,ContextRefreshListener监听应用上下文刷新事件ContextRefreshedEvent。有时根据业务需求,作为开发人员我们也需要定义自己的事件,比如自定义用户注册、更新和删除事件,并做出相应的处理。

例如分布式组件zookeeper里面就大量用到了注册监听器,以监听节点的添加、更新、删除及其子节点的添加、删除和更新事件,在每个事件发生时做出相应的处理。因此对于Java中高级程序员来说,掌握Spring中的常用监听器以及自定义事件和监听器是一项必不可少的技能。

2 SpringBoot中与应用上下文加载过程相关的6大事件

SpringBoot项目中,启动应用后除了触发常规的spring框架事件,例如ContextRefreshedEvent外,SpringApplication还将触发一些额外的应用事件。而与SpringBoot应用相关的事件主要有以下6种:

  • 01 ApplicationStartingEvent事件:在应用刚启动时触发,发生在除了监听器注册和初始化线程之外的其他线程之前;
  • 02 ApplicationEnvironmentPreparedEvent事件:介于应用上下文中的Environment 已经准备好之后和在应用上下文创建之前的时间段触发,此时通过Spring容器可以拿到一些环境和系统变量,但还无法拿到任何bean的实例;
  • 03 ApplicationPreparedEvent 准备事件:介于spring应用上下文中的bean定义加载之后和应用上下文刷新之前的时间段触发;
  • 04 ApplicationStartedEvent 事件:介于在应用上下文刷新之后和任意application(应用)和command-line runners(命令行任务)调用之前的时间段触发;
  • 05 ApplicationReadyEvent : 在application和command-line 调用之后促发,此时表明应已经准备好服务于web请求;
  • 06 ApplicationFailedEvent: 在应用的启动过程中发生异常时触发

SpringBoot应用启动后,Spring应用中的事件将以上事件类型先后触发。

3 SpringBoot应用事件的两种注册方式
  • SpringApplication实例方法注册注册: 及通过new出来的SpringApplication实例的addListeners(ApplicationListener... listeners)API方法注册

  • spring.factories文件中键值对自动注册:在SpringBoot项目的resources目录下创建META-INF文件夹,然后在该目录下添加spring.factories文件,通过使用org.springframework.context.ApplicationListener作为键值以下面这种方式引用你自定义的监听器,示例如下:

org.springframework.context.ApplicationListener=com.example.project.MyListener

这种方式可以实现在应用启动时自动注册监听器。

注意: 一些事件会在Spring应用上下文初始话之前就触发,所以以组件的形式注册监听器对于这样的事件会失效,而通过以上两种方式注册监听器则正好避免了这种尴尬。

4 SpringBoot中事件和监听器器的用法

SpringBoot项目中所有监听以上6中监听事件的监听器都要实现ApplicationListener接口,并实现onApplicationEvent方法;并且需要将监听器注册到Spring应用中去。

4.1定义监听器

MyAppEnvPreparedListener.java

package com.example.bootdemo.listeners;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;import org.springframework.context.ApplicationListener;import org.springframework.core.env.ConfigurableEnvironment;import java.util.Map;import java.util.Set;

publicclass MyAppEnvPreparedListenerimplementsApplicationListener<ApplicationEnvironmentPreparedEvent>{

    privatestatic Logger logger= LoggerFactory.getLogger(MyAppEnvPreparedListener.class);@OverridepublicvoidonApplicationEvent(ApplicationEnvironmentPreparedEvent applicationEnvironmentPreparedEvent){
        logger.info("监听到ApplicationEnvironmentPreparedEvent事件......");//获取并打印事件源全类名
        String className= applicationEnvironmentPreparedEvent.getSource().getClass().getName();

        logger.info("className={}",className);//获取并打印环境变量
        ConfigurableEnvironment environment= applicationEnvironmentPreparedEvent.getEnvironment();
        
        String[] activeProfiles= environment.getActiveProfiles();for(int i=0;i<activeProfiles.length;i++){
            logger.info("activeProfiles["+i+"]="+activeProfiles[i]);}

        Map<String, Object> systemProperties= environment.getSystemProperties();

        Set<String> keySet= systemProperties.keySet();for(String key: keySet){
            logger.info(key+"="+systemProperties.get(key));}}}

ApplicationReadyListener.java

package com.example.bootdemo.listeners;import com.example.bootdemo.service.impl.UserServiceImpl;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.boot.context.event.ApplicationReadyEvent;import org.springframework.context.ApplicationListener;import org.springframework.context.ConfigurableApplicationContext;

publicclass ApplicationReadyListenerimplementsApplicationListener<ApplicationReadyEvent>{

    privatestatic Logger logger= LoggerFactory.getLogger(ApplicationReadyListener.class);@OverridepublicvoidonApplicationEvent(ApplicationReadyEvent applicationReadyEvent){

        logger.info("监听到ApplicationReadyEvent事件......");//从事件中拿到应用上下文
        ConfigurableApplicationContext applicationContext= applicationReadyEvent.getApplicationContext();//使用应用上下文获取单例bean
        Object bean= applicationContext.getBean(UserServiceImpl.class);

        String className= bean.getClass().getName();//打印单例bean的类名
        logger.info("className={}",className);}}
4.2 在启动类中注册监听器
import com.example.bootdemo.listeners.*;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
publicclass BootDemoApplication{publicstaticvoidmain(String[] args){

		SpringApplication springApplication=newSpringApplication(BootDemoApplication.class);

		MyAppEnvPreparedListener myAppEnvPreparedListener=newMyAppEnvPreparedListener();

		ApplicationReadyListener applicationReadyListener=newApplicationReadyListener();//利用SpringApplication实例方法注册监听器
  springApplication.addListeners(myAppEnvPreparedListener,applicationReadyListener);//启动应用
  springApplication.run(args);}}

通过以上代码可以看出:监听以上6种应用事件的监听器必须实现带泛型事件的应用监听器接口ApplicationListener<?extends SpringApplicationEvent>,并实现其onApplicationEvent方法

4.3 测试

启动SpringBoot应用后可以看到控制台打印出了如下日志信息:

//与MyAppEnvPreparedListener监听器相关的日志
2020-07-05 21:34:23.303  INFO 14524 ---[           main] c.e.b.l.MyAppEnvPreparedListener: 监听到ApplicationEnvironmentPreparedEvent事件......
2020-07-05 21:34:23.307  INFO 14524 ---[           main] c.e.b.l.MyAppEnvPreparedListener: className=org.springframework.boot.SpringApplication
2020-07-05 21:34:23.310  INFO 14524 ---[           main] c.e.b.l.MyAppEnvPreparedListener: activeProfiles[0]=dev
2020-07-05 21:34:23.310  INFO 14524 ---[           main] c.e.b.l.MyAppEnvPreparedListener: user.dir=D:\SpringBootProject\boot-demo
2020-07-05 21:34:23.312  INFO 14524 ---[           main] c.e.b.l.MyAppEnvPreparedListener: intellij.debug.agent=true
2020-07-05 21:34:23.312  INFO 14524 ---[           main] c.e.b.l.MyAppEnvPreparedListener: java.runtime.version=1.8.0_131-b11
//其余与MyAppEnvPreparedListener相关的信息不再贴上
2020-07-05 21:34:23.486  INFO 14524 ---[           main] c.example.bootdemo.BootDemoApplication: The following profiles are active: dev
2020-07-05 21:34:25.146  INFO 14524 ---[           main] .s.d.r.c.RepositoryConfigurationDelegate: Bootstrapping Spring Data repositoriesin DEFAULT mode.
2020-07-05 21:34:25.264  INFO 14524 ---[           main] .s.d.r.c.RepositoryConfigurationDelegate: Finished Spring Data repository scanningin 106ms. Found 2 repository interfaces.
2020-07-05 21:34:26.037  INFO 14524 ---[           main] trationDelegate$BeanPostProcessorChecker: Bean'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' oftype[org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration$$EnhancerBySpringCGLIB$$234a6beb] is not eligiblefor getting processed by all BeanPostProcessors(for example: not eligiblefor auto-proxying)
2020-07-05 21:34:26.852  INFO 14524 ---[           main]
o.s.b.w.embedded.tomcat.TomcatWebServer: Tomcat initialized with port(s): 8088(http)
2020-07-05 21:34:26.900  INFO 14524 ---[           main] o.apache.catalina.core.StandardService: Startingservice[Tomcat]
2020-07-05 21:34:26.901  INFO 14524 ---[           main] org.apache.catalina.core.StandardEngine: Starting Servlet engine:[Apache Tomcat/9.0.17]
//tomcat中间件中默认使用了一个生命周期监听器AprLifecycleListener
2020-07-05 21:34:26.913  INFO 14524 ---[           main] o.a.catalina.core.AprLifecycleListener: Loaded APR based Apache Tomcat Native library[1.2.21] using APR version[1.6.5].
2020-07-05 21:34:26.913  INFO 14524 ---[           main] o.a.catalina.core.AprLifecycleListener: APR capabilities: IPv6[true], sendfile[true], accept filters[false], random[true].
2020-07-05 21:34:26.913  INFO 14524 ---[           main] o.a.catalina.core.AprLifecycleListener: APR/OpenSSL configuration: useAprConnector[false], useOpenSSL[true]
2020-07-05 21:34:26.921  INFO 14524 ---[           main] o.a.catalina.core.AprLifecycleListener: OpenSSL successfully initialized[OpenSSL 1.1.1a  20 Nov 2018]
2020-07-05 21:34:27.284  INFO 14524 ---[           main] o.a.c.c.C.[.[localhost].[/apiBoot]: Initializing Spring embedded WebApplicationContext
2020-07-05 21:34:27.285  INFO 14524 ---[           main] o.s.web.context.ContextLoader: Root WebApplicationContext: initialization completedin 3680 ms
o.s.b.w.embedded.tomcat.TomcatWebServer: Tomcat started on port(s): 8088(http) with context path'/apiBoot'
2020-07-05 21:34:31.290  INFO 14524 ---[           main] c.example.bootdemo.BootDemoApplication: Started BootDemoApplicationin 8.461 seconds(JVM runningfor 11.682)
                                                                  
//与ApplicationReadyListener监听器相关的日志
c.e.b.l.ApplicationReadyListener: 监听到ApplicationReadyEvent事件......
2020-07-05 21:34:31.295  INFO 14524 ---[           main] c.e.b.l.ApplicationReadyListener: className=com.example.bootdemo.service.impl.UserServiceImpl$$EnhancerBySpringCGLIB$$6ce06578
2020-07-06 00:05:47.531  INFO 14524 ---[nio-8088-exec-1] o.a.c.c.C.[.[localhost].[/apiBoot]: Initializing Spring DispatcherServlet'dispatcherServlet'
2020-07-06 00:05:47.590  INFO 14524 ---[nio-8088-exec-1] o.s.web.servlet.DispatcherServlet: Initializing Servlet'dispatcherServlet'
2020-07-06 00:05:48.620  INFO 14524 ---[nio-8088-exec-1] o.s.web.servlet.DispatcherServlet: Completed initializationin 1030 ms
---至此项目启动完成 ---
2020-07-06 00:05:50.107  INFO 14524 ---[nio-8088-exec-1]

从日志信息中可以看到: 实现接口的服务类bean是经过了SpringCGLIB增强后的类

5 自定义事件并监听
5.1 事件与监听器原理浅析

SpringBoot中的事件与监听器使用了设计模式中的观察者模式,其原理是基于订阅和发布机制。

通过查看以上6种事件的类继承关系发现他们都继承自SpringApplicationEvent类,事件源均为SpringApplication实例;而SpringApplicationEvent类又继承自ApplicationEvent,该类的构造函数中需传一个Object类型的事件源对象

package org.springframework.context;import java.util.EventObject;

publicabstractclass ApplicationEventextendsEventObject{
    privatestaticfinallong serialVersionUID=7099057708183571937L;
    privatefinallong timestamp= System.currentTimeMillis();publicApplicationEvent(Object source){super(source);}publicfinallonggetTimestamp(){
        returnthis.timestamp;}}
5.2 自定义用户注册事件
package com.example.bootdemo.events;import com.example.bootdemo.pojo.UserInfo;import com.example.bootdemo.service.UserService;import org.springframework.context.ApplicationEvent;publicclassUserRegisterEventextendsApplicationEvent{//定义私有变量 private UserInfo userInfo; //够造函数中传入事件源用户服务类 public UserRegisterEvent(UserService userService, UserInfo userInfo) {super(userService);this.userInfo= userInfo;}public UserInfogetUserInfo(){return userInfo;}}
5.3 定义监听用户注册事件的监听器
package com.example.bootdemo.listeners;import com.alibaba.fastjson.JSON;import com.example.bootdemo.events.UserRegisterEvent;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.context.ApplicationListener;import org.springframework.stereotype.Component;@Component
publicclass UserRegisterListenerimplementsApplicationListener<UserRegisterEvent>{

    Logger logger= LoggerFactory.getLogger(UserRegisterListener.class);@OverridepublicvoidonApplicationEvent(UserRegisterEvent userRegisterEvent){

        logger.info("用户已注册,完成给新注册用户发邮件业务逻辑......");//打赢用户信息
        logger.info("userInfo={}", JSON.toJSON(userRegisterEvent.getUserInfo()));}}

注意:监听非SpringBoot应用事件的监听器可以以组件的形式注册到Spring容器中去

5.4 在事件源类中利用Spring容器中发布事件

UserServiceImpl.java

@Transactional@Overridepublic UserInfosave(UserInfo userInfo){try{//数据入库前对密码进行加密
            String password= EncryptionUtil.md5Encrypt(userInfo.getPassword());
            userInfo.setPassword(password);}catch(Exception e){
            log.error("encrypt password failed",e);
            returnnull;}
        UserInfo userInfo1= userRepository.save(userInfo);//构造和发布用户注册事件
        UserRegisterEvent userRegisterEvent=newUserRegisterEvent(this,userInfo1);
        applicationContext.publishEvent(userRegisterEvent);return userInfo1;}

UserInfoController.java

@PostMapping("/info")public ServiceResponse<UserInfo>saveUserInfo(@RequestBody UserInfo userInfo){
       log.info("userInfo={}", JSON.toJSON(userInfo));
        ServiceResponse<UserInfo> response=newServiceResponse<>();
        UserInfo userInfo1= userInfoService.save(userInfo);
        response.setData(userInfo1);return response;}
5.5 测试效果

在postman上发送一个post请求:
http://localhost:8088/apiBoot/user/info
请求体RequestBody参数如下:

{"userName":"LeiFeng","password":"leifeng2020","userSex":"M","userRole":"Admin","telNum":15200001316,"email":"leifeng163.com","regDate":"2020-07-05","birthDay":"1990-12-01","userNameCn":"雷锋","createdBy":"x_heshengfu","createdTime":"2020-07-06 00:02:00","lastUpdatedBy":"x_heshengfu","lastUpdatedTime":"2020-07-06 00:02:00"}

调用接口后可以看到服务器控制台打印出如下日志信息:

2020-07-06 00:05:50.989  INFO 14524 ---[nio-8088-exec-1] c.e.b.listeners.UserRegisterListener: 用户已注册,完成给新注册用户发邮件业务逻辑......
2020-07-06 00:05:50.989  INFO 14524 ---[nio-8088-exec-1] c.e.b.listeners.UserRegisterListener: userInfo={"birthDay":"1990-12-01","lastUpdatedBy":"x_heshengfu","userSex":"M","regDate":"2020-07-05","userName":"LeiFeng","userId":29,"userNameCn":"雷锋","password":"3d92f2a6b7fa372948dfaf8ceee78891","createdBy":"x_heshengfu","telNum":15200001316,"createdTime":"2020-07-06 00:02:00","lastUpdatedTime":"2020-07-06 00:02:00","userRole":"Admin","email":"leifeng163.com"}
6 小结

1) 应用程序事件是通过·Spring Framework·的事件发布机制发送的。这部分机制确保发布到子上下文中监听器的事件也被发布到任何祖先上下文中的监听器。因此,如果应用程序使用层次结构对于SpringApplication实例,监听器可以接收相同类型的多个实例应用程序事件。

2) 为了让侦听器区分·Spring·应用上下文的事件及其下级的上下文事件,要求注入它的应用程序上下文,然后进行比较注入的上下文与事件的上下文。上下文可以通过实现applicationcontextAware接口注入,如果侦听器是一个bean,则可以使用@Autowired注解实现注入

参考文章
《Spring Boot Reference Guide》(2.1.3.RELEASE版本SprigBoot开发指南)

本文完成代码已上传到gitee,克隆地址: https://gitee.com/heshengfu1211/boot-demo.git
需要查看完整代码的读可通过git自行clone下来
欢迎读者朋友们使用微信扫描下方微信二维码,关作者的微信公众号,与作者一同成长,
向着Java架构师的方向前进

  • 作者:heshengfu1211
  • 原文链接:https://blog.csdn.net/heshengfu1211/article/details/107420725
    更新时间:2022-07-25 07:57:17