前言

今天去官网查看spring boot资料时,在特性中看见了系统的事件及监听章节。想想,spring的事件应该是在3.x版本就发布的功能了,并越来越完善,其为beanbean之间的消息通信提供了支持。比如,我们可以在用户注册成功后,发送一份注册成功的邮件至用户邮箱或者发送短信。使用事件其实最大作用,应该还是为了业务解耦,毕竟用户注册成功后,注册服务的事情就做完了,只需要发布一个用户注册成功的事件,让其他监听了此事件的业务系统去做剩下的事件就好了。对于事件发布者而言,不需要关心谁监听了该事件,以此来解耦业务。今天,我们就来讲讲spring boot中事件的使用和发布。当然了,也可以使用像guavaeventbus或者异步框架Reactor来处理此类业务需求的。本文仅仅谈论ApplicationEvent以及Listener的使用。

一点知识

示例前,我们来了解下相关知识点。

Java的事件机制

java中的事件机制一般包括3个部分:EventObjectEventListenerSource

EventObject

java.util.EventObject是事件状态对象的基类,它封装了事件源对象以及和事件相关的信息。所有java的事件类都需要继承该类。

EventListener

java.util.EventListener是一个标记接口,就是说该接口内是没有任何方法的。所有事件监听器都需要实现该接口。事件监听器注册在事件源上,当事件源的属性或状态改变的时候,调用相应监听器内的回调方法。

Source

事件源不需要实现或继承任何接口或类,它是事件最初发生的地方。因为事件源需要注册事件监听器,所以事件源内需要有相应的盛放事件监听器的容器。

java的事件机制是一个观察者模式。大家可以根据这个模式,自己实现一个。可以看看这篇博文:《java事件机制》一个很简单的实例。

Spring的事件

ApplicationEvent以及ListenerSpring为我们提供的一个事件监听、订阅的实现,内部实现原理是观察者设计模式,设计初衷也是为了系统业务逻辑之间的解耦,提高可扩展性以及可维护性。

  • ApplicationEvent就是Spring的事件接口
  • ApplicationListener就是Spring的事件监听器接口,所有的监听器都实现该接口
  • ApplicationEventPublisherSpring的事件发布接口,ApplicationContext实现了该接口
  • ApplicationEventMulticaster就是Spring事件机制中的事件广播器,默认实现SimpleApplicationEventMulticaster

Spring中通常是ApplicationContext本身担任监听器注册表的角色,在其子类AbstractApplicationContext中就聚合了事件广播器ApplicationEventMulticaster和事件监听器ApplicationListnener,并且提供注册监听器的addApplicationListnener方法。

其执行的流程大致为:

当一个事件源产生事件时,它通过事件发布器ApplicationEventPublisher发布事件,然后事件广播器ApplicationEventMulticaster会去事件注册表ApplicationContext中找到事件监听器ApplicationListnener,并且逐个执行监听器的onApplicationEvent方法,从而完成事件监听器的逻辑。

Spring中,使用注册监听接口,除了继承ApplicationListener接口外,还可以使用注解@EventListener来监听一个事件,同时该注解还支持SpEL表达式,来触发监听的条件,比如只接受编码为001的事件,从而实现一些个性化操作。下文示例中会简单举例下。

简单来说,在Java中,通过java.util. EventObject来描述事件,通过java.util. EventListener来描述事件监听器,在众多的框架和组件中,建立一套事件机制通常是基于这两个接口来进行扩展。

SpringBoot的默认启动事件

SpringBoot1.5.x中,提供了几种事件,供我们在开发过程中进行更加便捷的扩展及差异化操作。

  • ApplicationStartingEvent:springboot启动开始的时候执行的事件

  • ApplicationEnvironmentPreparedEventspring boot对应Enviroment已经准备完毕,但此时上下文context还没有创建。在该监听中获取到ConfigurableEnvironment后可以对配置信息做操作,例如:修改默认的配置信息,增加额外的配置信息等等。

  • ApplicationPreparedEventspring boot上下文context创建完成,但此时spring中的bean是没有完全加载完成的。在获取完上下文后,可以将上下文传递出去做一些额外的操作。值得注意的是:在该监听器中是无法获取自定义bean并进行操作的。

  • ApplicationReadyEventspringboot加载完成时候执行的事件。

  • ApplicationFailedEventspring 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类进行包装了一层。这点和guavaeventBus类似。**

而且,使用@EventListenercondition可以实现更加精细的事件监听,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=趔趄的猿,可以看见事件已经被监听到了,而监听了codeoKong的监听未触发。

然后访问下: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 事件中的事务控制

参考资料

  1. https://docs.spring.io/spring-boot/docs/1.5.15.RELEASE/reference/htmlsingle/#boot-features-application-events-and-listeners

  2. https://blog.csdn.net/eos2009/article/details/77773551

  3. https://www.cnblogs.com/senlinyang/p/8496099.html

总结

本章节主要简单介绍了spring的事件机制。感兴趣的同学,可以编写一个监听所有事件的方法,然后看看系统运行各类请求或者相关操作时,系统会发布哪些事件,了解后可以在之后碰见一些特殊业务需求时,可以适当的监听相关的事件来完成特定的业务公共。同时对这种观察者模式,大家还可以看看eventbusreactor了。后者没用过,有时间倒是可以看看。最近买了本RxJava2书籍,确实要好好补课下了。

最后

目前互联网上很多大佬都有SpringBoot系列教程,如有雷同,请多多包涵了。原创不易,码字不易,还希望大家多多支持。若文中有所错误之处,还望提出,谢谢。

老生常谈

  • 个人QQ: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/

SpringBoot | 第三十二章:事件的发布和监听的更多相关文章

  1. Gradle 1.12用户指南翻译——第三十二章. JDepend 插件

    本文由CSDN博客万一博主翻译,其他章节的翻译请参见: http://blog.csdn.net/column/details/gradle-translation.html 翻译项目请关注Githu ...

  2. “全栈2019”Java多线程第三十二章:显式锁Lock等待唤醒机制详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...

  3. “全栈2019”Java第三十二章:增强for循环Foreach语法

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...

  4. SpringBoot | 第三十五章:Mybatis的集成和使用

    前言 最近收到公众号留言说,单纯的Mybatis的集成和使用.前面在第九章:Mybatis-plus的集成和使用介绍了基于mybatis-plus的集成和使用.后者也只是对mybatis进行了功能增强 ...

  5. 【WPF学习】第三十二章 执行命令

    前面章节已经对命令进行了深入分析,分析了基类和接口以及WPF提供的命令库.但尚未例举任何使用这些命令的例子. 如前所述,RoutedUICommand类没有任何硬编码的功能,而是只表达命令,为触发命令 ...

  6. 第三十二章、使用splitDockWidget和tabifyDockWidget嵌套布局QDockWidget的PyQt人机对话案例

    专栏:Python基础教程目录 专栏:使用PyQt开发图形界面Python应用 专栏:PyQt入门学习 老猿Python博文目录 一.引言 在第<第三十一章.containers容器类部件QDo ...

  7. SpringBoot | 第三十六章:集成多CacheManager

    前言 今天有网友咨询了一个问题:如何在一个工程中使用多种缓存进行差异化缓存,即实现多个cacheManager灵活切换.原来没有遇见这种场景,今天下班抽空试了下,以下就把如何实现的简单记录下. 一点知 ...

  8. 第三十二章 Linux常规练习题(一)

    一.练习题一 1.超级用户(管理员用户)提示符是____,普通用户提示符是____.2.linux关机重启的命令有哪些 ?3.bash是什么?4.bash特性, 常见的bash特性有哪些?5.网卡的配 ...

  9. 《FPGA全程进阶---实战演练》第三十二章 Signal Tap II 应用实例

    还有几天就要交文章终稿了,三年的研究生生活也快要结束了,时间飞快,岁月如梭,但学习技术的热情仍然不能松懈,不懂的东西太多,需要实时保持奋斗!!过些天会继续更新<FPGA全程进阶---实战演练&g ...

随机推荐

  1. Castle Windsor

    让我们从Web API的集成点开始,它们是IDependencyResolver和IDependencyScope接口.IDependencyResolver和其他接口的名称可能与MVC中的接口相同, ...

  2. SQL Server乱码处理(ASCII)

    CREATE FUNCTION [dbo].[RegexReplace] ( @string VARCHAR(MAX), --被替换的字符串 @pattern VARCHAR(255), --替换模板 ...

  3. C语言编程基础

    主流C语言编译器介绍 关于GNU规范的语法扩展 用C语言构建一个可执行程序的流程 宏定义(无参宏定义和带参宏定义),C语言宏定义详解 条件编译,C语言条件编译详解 原码.反码.补码及位操作符,C语言位 ...

  4. conda install 安装太慢怎么办?

    小编我在安装tensorflow和keras的过程中,安装进程太慢,小木棍一直在转圈...抓狂... 如何解决??? 使用清华提供的anaconda镜像,使用以后真的很快! Anaconda 镜像使用 ...

  5. dota有哪些经典的典故或笑话?

    ----------------------------------------------------dota有哪些经典的典故或笑话?虽然现在玩游戏也没什么热情了, 但是看到这些还是笑尿,笑点低 = ...

  6. VMware虚拟机下安装CentOS系统超详细教程

    链接:https://jingyan.baidu.com/article/fdffd1f8736173f3e98ca1e3.html 1.步骤一.工具准备 1.物理计算机一台 配置要求: 操作系统:w ...

  7. Unity---动画系统学习(3)---使用状态机来实现走、跑、转弯等的动画切换

    1. 初始设置 用动画学习笔记(2)中方法,把动画全都切割好. 拖进状态机并设置箭头.并设置具体箭头触发的事件. 在状态机左侧中添加参数,Float和Int类型参数只能从-1~1之间变化 Float: ...

  8. kibana安装汉化包

    kibana安装汉化包其实很简单!但要找到汉化包可能就很麻烦了.我这里提供了6.2的版本的汉化包!至于能不能在其他版本用,我就没试过了.但6.2的kibana本人亲测.没问题!!!! 下载——解压.这 ...

  9. rabbitMq使用学习笔记

    rabbitmq的工作原理: MQ全称为Message Queue,即消息队列, RabbitMQ是由erlang语言开发,基于AMQP(Advanced MessageQueue 高级消息队列协议) ...

  10. angluarJs与后台交互小案例

    .myService.html: <!DOCTYPE HTML> <html ng-app="app"> <head> <title> ...