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 MyAppEnvPreparedListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {
privatestatic Logger logger = LoggerFactory.getLogger(MyAppEnvPreparedListener.class);
@Override
public void onApplicationEvent(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 ApplicationReadyListener implements ApplicationListener<ApplicationReadyEvent> {
privatestatic Logger logger = LoggerFactory.getLogger(ApplicationReadyListener.class);
@Override
public void onApplicationEvent(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 {
public static void main(String[] args) {
SpringApplication springApplication = new SpringApplication(BootDemoApplication.class);
MyAppEnvPreparedListener myAppEnvPreparedListener = new MyAppEnvPreparedListener();
ApplicationReadyListener applicationReadyListener = new ApplicationReadyListener();
//利用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 repositories in DEFAULT mode.
2020-07-05 21:34:25.264 INFO 14524 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 106ms. Found 2 repository interfaces.
2020-07-05 21:34:26.037 INFO 14524 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type [org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration$$EnhancerBySpringCGLIB$$234a6beb] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for 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 : Starting service [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 completed in 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 BootDemoApplication in 8.461 seconds (JVM running for 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 initialization in 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 ApplicationEvent extends EventObject {
privatestaticfinallong serialVersionUID = 7099057708183571937L;
privatefinallong timestamp = System.currentTimeMillis();
public ApplicationEvent(Object source) {
super(source);
}
public final long getTimestamp() {
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;
public class UserRegisterEvent extends ApplicationEvent { //定义私有变量 private UserInfo userInfo; //够造函数中传入事件源用户服务类 public UserRegisterEvent(UserService userService, UserInfo userInfo) {
super(userService);
this.userInfo = userInfo;
}
public UserInfo getUserInfo() {
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 UserRegisterListener implements ApplicationListener<UserRegisterEvent> {
Logger logger = LoggerFactory.getLogger(UserRegisterListener.class);
@Override
public void onApplicationEvent(UserRegisterEvent userRegisterEvent) {
logger.info("用户已注册,完成给新注册用户发邮件业务逻辑......");
//打赢用户信息
logger.info("userInfo={}", JSON.toJSON(userRegisterEvent.getUserInfo()));
}
}
注意:监听非SpringBoot
应用事件的监听器可以以组件的形式注册到Spring
容器中去
5.4 在事件源类中利用Spring容器中发布事件
UserServiceImpl.java
@Transactional
@Override
public UserInfo save(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 = new UserRegisterEvent(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 = new ServiceResponse<>();
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架构师的方向前进