今天去官网查看 spring boot
资料时,在特性中看见了 系统的事件及监听
章节。想想, spring
的事件应该是在 3.x
版本就发布的功能了,并越来越完善,其为 bean
和 bean
之间的消息通信提供了支持。比如,我们可以在 用户注册成功后,发送一份注册成功的邮件至用户邮箱或者发送短信 。使用事件其实最大作用,应该还是为了 业务解耦 ,毕竟用户注册成功后,注册服务的事情就做完了,只需要发布一个用户注册成功的事件,让其他监听了此事件的业务系统去做剩下的事件就好了。对于事件发布者而言,不需要关心谁监听了该事件,以此来解耦业务。今天,我们就来讲讲 spring boot
中事件的使用和发布。当然了,也可以使用像 guava
的 eventbus
或者异步框架 Reactor
来处理此类业务需求的。本文仅仅谈论 ApplicationEvent
以及 Listener
的使用。
示例前,我们来了解下相关知识点。
java中的事件机制一般包括3个部分: EventObject
, EventListener
和 Source
。
java.util.EventObject是事件状态对象的基类,它封装了事件源对象以及和事件相关的信息。所有java的事件类都需要继承该类。
java.util.EventListener是一个标记接口,就是说该接口内是没有任何方法的。所有事件监听器都需要实现该接口。事件监听器注册在事件源上,当事件源的属性或状态改变的时候,调用相应监听器内的回调方法。
事件源不需要实现或继承任何接口或类,它是事件最初发生的地方。因为事件源需要注册事件监听器,所以事件源内需要有相应的盛放事件监听器的容器。
java
的事件机制是一个观察者模式。大家可以根据这个模式,自己实现一个。可以看看这篇博文:《 java事件机制 》一个很简单的实例。
ApplicationEvent
以及 Listener
是 Spring
为我们提供的一个事件监听、订阅的实现,内部实现原理是观察者设计模式,设计初衷也是为了系统业务逻辑之间的解耦,提高可扩展性以及可维护性。
ApplicationEvent
就是 Spring
的事件接口 ApplicationListener
就是 Spring
的事件监听器接口,所有的监听器都实现该接口 ApplicationEventPublisher
是 Spring
的事件发布接口, ApplicationContext
实现了该接口 ApplicationEventMulticaster
就是 Spring
事件机制中的事件广播器,默认实现 SimpleApplicationEventMulticaster
在 Spring
中通常是 ApplicationContext
本身担任监听器注册表的角色,在其子类 AbstractApplicationContext
中就聚合了事件广播器 ApplicationEventMulticaster
和事件监听器 ApplicationListnener
,并且提供注册监听器的 addApplicationListnener
方法。
其执行的流程大致为:
当一个事件源产生事件时,它通过事件发布器 ApplicationEventPublisher
发布事件,然后事件广播器 ApplicationEventMulticaster
会去事件注册表 ApplicationContext
中找到事件监听器 ApplicationListnener
,并且逐个执行监听器的 onApplicationEvent
方法,从而完成事件监听器的逻辑。
Spring
中,使用注册监听接口,除了继承 ApplicationListener
接口外,还可以使用注解 @EventListener
来监听一个事件,同时该注解还支持 SpEL
表达式,来触发监听的条件,比如只接受编码为 001
的事件,从而实现一些个性化操作。下文示例中会简单举例下。 在 SpringBoot
的 1.5.x
中,提供了几种事件,供我们在开发过程中进行更加便捷的扩展及差异化操作。
ApplicationStartingEvent
:springboot启动开始的时候执行的事件
ApplicationEnvironmentPreparedEvent
: spring boot
对应 Enviroment 已经准备完毕,但此时上下文 context
还没有创建。在该监听中获取到 ConfigurableEnvironment
后可以对配置信息做操作,例如:修改默认的配置信息,增加额外的配置信息等等。
ApplicationPreparedEvent
: spring boot
上下文 context
创建完成,但此时 spring
中的 bean
是没有完全加载完成的。在获取完上下文后,可以将上下文传递出去做一些额外的操作。值得注意的是: 在该监听器中是无法获取自定义bean并进行操作的。
ApplicationReadyEvent
: springboot
加载完成时候执行的事件。
ApplicationFailedEvent
: spring boot
启动异常时执行事件。
从官网文档中,我们可以知道,由于一些事件实在上下文为加载完触发的,所以无法使用注册 bean
的方式来声明,文档中可以看出,可以通过 SpringApplication.addListeners(…)
或者 SpringApplicationBuilder.listeners(…)
来添加,或者添加 META-INF/spring.factories
文件z中添加监听类也是可以的,这样会自动加载。
org.springframework.context.ApplicationListener=com.example.project.MyListener
启动类中添加:
@SpringBootApplication public class Application { public static void main(String[] args){ SpringApplication app =new SpringApplication(Application.class); app.addListeners(new MyApplicationStartingEventListener());//加入自定义的监听类 app.run(args); } }
所以在需要的时候,可以通过适当的监听以上事件,来完成一些业务操作。
通过以上的介绍,我们来定义一个自定义事件的发布和监听。
0.加入POM依赖,这里为了演示加入了 web
依赖。事件相关类都在 spring-context
包下。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
1.自定义事件源和实体。
MessageEntity.java
/** * 消息实体类 * @author oKong * */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public class MessageEntity { String message; String code; }
CustomEvent.java
/** * 编写事件源 * @author oKong * */ @SuppressWarnings("serial") public class CustomEvent extends ApplicationEvent{ private MessageEntity messageEntity; public CustomEvent(Object source, MessageEntity messageEntity) { super(source); this.messageEntity = messageEntity; } public MessageEntity getMessageEntity() { return this.messageEntity; } }
2.编写监听类
使用 @EventListener
方式。
/** * 监听配置类 * * @author oKong * */ @Configuration @Slf4j public class EventListenerConfig { @EventListener public void handleEvent(Object event) { //监听所有事件 可以看看 系统各类时间 发布了哪些事件 //可根据 instanceof 监听想要监听的事件 // if(event instanceof CustomEvent) { // // } log.info("事件:{}", event); } @EventListener public void handleCustomEvent(CustomEvent customEvent) { //监听 CustomEvent事件 log.info("监听到CustomEvent事件,消息为:{}, 发布时间:{}", customEvent.getMessageEntity(), customEvent.getTimestamp()); } /** * 监听 code为oKong的事件 */ @EventListener(condition="#customEvent.messageEntity.code == 'oKong'") public void handleCustomEventByCondition(CustomEvent customEvent) { //监听 CustomEvent事件 log.info("监听到code为'oKong'的CustomEvent事件,消息为:{}, 发布时间:{}", customEvent.getMessageEntity(), customEvent.getTimestamp()); } @EventListener public void handleObjectEvent(MessageEntity messageEntity) { //这个和eventbus post方法一样了 log.info("监听到对象事件,消息为:{}", messageEntity); } }
** 注意: Spring
中,事件源不强迫继承 ApplicationEvent
接口的,也就是可以直接发布任意一个对象类。但内部其实是使用 PayloadApplicationEvent
类进行包装了一层。这点和 guava
的 eventBus
类似。**
@EventListener
的 condition
可以实现更加精细的事件监听, condition
支持 SpEL
表达式,可根据事件源的参数来判断是否监听。 使用 ApplicationListener
方式。
@Component @Slf4j public class EventListener implements ApplicationListener<CustomEvent>{ @Override public void onApplicationEvent(CustomEvent event) { //这里也可以监听所有事件 使用 ApplicationEvent 类即可 //这里仅仅监听自定义事件 CustomEvent log.info("ApplicationListener方式监听事件:{}", event); } }
3.编写控制类,示例发布事件。
/** * 模拟触发事件 * @author oKong * */ @RestController @RequestMapping("/push") @Slf4j public class DemoController { /** * 注入 事件发布类 */ @Autowired ApplicationEventPublisher eventPublisher; @GetMapping public String push(String code,String message) { log.info("发布applicationEvent事件:{},{}", code, message); eventPublisher.publishEvent(new CustomEvent(this, MessageEntity.builder().code(code).message(message).build())); return "事件发布成功!"; } @GetMapping("/obj") public String pushObject(String code,String message) { log.info("发布对象事件:{},{}", code, message); eventPublisher.publishEvent(MessageEntity.builder().code(code).message(message).build()); return "对象事件发布成功!"; } }
4.编写启动类。
/** * 事件监听 * * @author oKong * */ @SpringBootApplication @Slf4j public class EventAndListenerApplication { public static void main(String[] args) throws Exception { SpringApplication app =new SpringApplication(EventAndListenerApplication.class); app.addListeners(new MyApplicationStartingEventListener());//加入自定义的监听类 app.run(args); log.info("spring-boot-event-listener-chapter32启动!"); } }
这里,创建了个 ApplicationStartingEvent
事件监听类。
/** * 示例-启动事件 * @author oKong * */ public class MyApplicationStartingEventListener implements ApplicationListener<ApplicationStartingEvent>{ @Override public void onApplicationEvent(ApplicationStartingEvent event) { // TODO Auto-generated method stub //由于 log相关还未加载 使用了也输出不了的 // log.info("ApplicationStartingEvent事件发布:{}", event); System.out.println("ApplicationStartingEvent事件发布:" + event.getTimestamp()); } }
5.启动应用,控制台可以看出,在启动时,我们监听到了 ApplicationStartingEvent
事件
首先访问下: http://127.0.0.1:8080/push?code=lqdev&message=趔趄的猿
,可以看见事件已经被监听到了, 而监听了 code
为 oKong
的监听未触发。
然后访问下: http://127.0.0.1:8080/push?code=oKong&message=趔趄的猿
,可以看见此时 三个监听事件都接收到了事件了 。
此时,由于写了一个监听所有事件的方法,可以看见请求结束后,会发布一个事件 ServletRequestHandledEvent
,里面记录了请求的时间、请求url、请求方式等等信息。
事件:ServletRequestHandledEvent: url=[/push]; client=[127.0.0.1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]
默认情况下,监听事件都是同步执行的。在需要异步处理时,可以在方法上加上 @Async
进行异步化操作。此时,可以定义一个线程池,同时开启异步功能,加入 @EnableAsync
。
对于异步处理,可以查看之前发布的文章: 《第二十一章:异步开发之异步调用》 。里面有详细的介绍异步调用,这里就不阐述了。
异步简单示例:
/** * 监听 code为oKong的事件 */ @Async @EventListener(condition="#customEvent.messageEntity.code == 'oKong'") public void handleCustomEventByCondition(CustomEvent customEvent) { //监听 CustomEvent事件 log.info("监听到code为'oKong'的CustomEvent事件,消息为:{}, 发布时间:{}", customEvent.getMessageEntity(), customEvent.getTimestamp()); }
当一些场景下,比如在用户注册成功后,即数据库事务提交了,之后再异步发送邮件等,不然会发生数据库插入失败,但事件却发布了,也就是邮件发送成功了的情况。此时,我们可以使用 @TransactionalEventListener
注解或者 TransactionSynchronizationManager
类来解决此类问题,也就是:事务成功提交后,再发布事件。当然也可以利用返回上层(事务提交后)再发布事件的方式了,只是不够优雅而已罢了,其实能起作用就好了,是吧~
本例中未使用到数据库,就不示例了,都在 Spring-tx
包下。
具体可查看文章: Spring Event 事件中的事务控制
https://docs.spring.io/spring-boot/docs/1.5.15.RELEASE/reference/htmlsingle/#boot-features-application-events-and-listeners
https://blog.csdn.net/eos2009/article/details/77773551
https://www.cnblogs.com/senlinyang/p/8496099.html
本章节主要简单介绍了 spring
的事件机制。感兴趣的同学,可以编写一个监听所有事件的方法,然后看看系统运行各类请求或者相关操作时,系统会发布哪些事件,了解后可以在之后碰见一些特殊业务需求时,可以适当的监听相关的事件来完成特定的业务公共。同时对这种观察者模式,大家还可以看看 eventbus
和 reactor
了。后者没用过,有时间倒是可以看看。最近买了本 RxJava2
书籍,确实要好好补课下了。
目前互联网上很多大佬都有 SpringBoot
系列教程,如有雷同,请多多包涵了。 原创不易,码字不易 ,还希望大家多多支持。若文中有所错误之处,还望提出,谢谢。
499452441 lqdevOps
个人博客: http://blog.lqdev.cn
完整示例: https://github.com/xie19900123/spring-boot-learning/tree/master/chapter-32
原文地址: https://blog.lqdev.cn/2018/11/06/springboot/chapter-thirty-two/