SpringBoot 使用注解实现消息广播功能

背景


在开发工作中,会遇到一种场景,做完某一件事情以后,需要广播一些消息或者通知,告诉其他的模块进行一些事件处理,一般来说,可以一个一个发送请求去通知,但是有一种更好的方式,那就是事件监听,事件监听也是设计模式中发布-订阅模式、观察者模式的一种实现。

观察者模式:简单的来讲就是你在做事情的时候身边有人在盯着你,当你做的某一件事情是旁边观察的人感兴趣的事情的时候,他会根据这个事情做一些其他的事,但是盯着你看的人必须要到你这里来登记,否则你无法通知到他(或者说他没有资格来盯着你做事情)。

对于Spring容器的一些事件,可以监听并且触发相应的方法。通常的方法有 2 种,ApplicationListener 接口和@EventListener 注解。

简介


要想顺利的创建监听器,并起作用,这个过程中需要这样几个角色:

  • 事件(event)可以封装和传递监听器中要处理的参数,如对象或字符串,并作为监听器中监听的目标。

  • 监听器(listener)具体根据事件发生的业务处理模块,这里可以接收处理事件中封装的对象或字符串。

  • 事件发布者(publisher)事件发生的触发者。

ApplicationListener 接口


ApplicationListener接口的定义如下:

d49b727674fcf7e920747c92fa025f91.png

它是一个泛型接口,泛型的类型必须是ApplicationEvent 及其子类,只要实现了这个接口,那么当容器有相应的事件触发时,就能触发 onApplicationEvent 方法。ApplicationEvent 类的子类有很多,Spring 框架自带的如下几个。

4c22a5e99af0eaa34c2cab13ff10f690.png

▐  简单使用

使用方法很简单,就是实现一个 ApplicationListener 接口,并且将加入到容器中就行。

@Component
public class DefinitionApplicationListener implements ApplicationListener<ApplicationEvent> {// ----------- 简单使用-实现ApplicationListener 接口@Overridepublic void onApplicationEvent(ApplicationEvent event) {System.out.println("事件触发:" + event.getClass().getName());}
}

启动项目

@SpringBootApplication
public class EngineSupportApplication {public static void main(String[] args) {SpringApplication.run(EngineSupportApplication.class);}
}

查看日志

783c9255980e7db58d3f6db9864b484b.png

2021-11-12 21:04:16.114  INFO 83691 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
事件触发:org.springframework.context.event.ContextRefreshedEvent
2021-11-12 21:04:16.263  INFO 83691 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
事件触发:org.springframework.boot.web.servlet.context.ServletWebServerInitializedEvent
2021-11-12 21:04:16.266  INFO 83691 --- [           main] com.alibaba.EngineSupportApplication     : Started EngineSupportApplication in 1.975 seconds (JVM running for 3.504)
事件触发:org.springframework.boot.context.event.ApplicationStartedEvent
事件触发:org.springframework.boot.context.event.ApplicationReadyEvent

自定义事件以及监听


▐ 定义事件

public class DefinitionEvent extends ApplicationEvent {public boolean enable;/*** Create a new ApplicationEvent.** @param source the object on which the event initially occurred (never {@code null})* @param enable*/public DefinitionEvent(Object source, boolean enable) {super(source);this.enable = enable;}

▐ 定义监听器

@Component
public class DefinitionApplicationListener implements ApplicationListener<ApplicationEvent> {// ----------- 简单使用-实现ApplicationListener 接口@Overridepublic void onApplicationEvent(ApplicationEvent event) {System.out.println("事件触发:" + event.getClass().getName());}// ----------- 自定义事件以及监听@Autowiredprivate ApplicationEventPublisher eventPublisher;/*** 事件发布方法*/public void pushListener(String msg) {eventPublisher.publishEvent(new DefinitionEvent(this, false));}
}

@EventListener 注解


▐ 简单使用

除了通过实现接口,还可以使用@EventListener 注解,实现对任意的方法都能监听事件。

在任意方法上标注@EventListener 注解,指定 classes,即需要处理的事件类型,一般就是 ApplicationEvent及其子类,可以设置多项。

@Component
public class DefinitionAnnotationEventListener {@EventListener(classes = {DefinitionEvent.class})public void listen(DefinitionEvent event) {System.out.println("注解监听器:" + event.getClass().getName());}}

此时,就可以有一个发布,两个监听器监听到发布的消息了,一个是注解方式,一个是非注解方式

结果:

注解监听器:com.alibaba.spring.context.event.DefinitionEvent
事件触发:com.alibaba.spring.context.event.DefinitionEvent

原理


其实上面添加@EventListener注解的方法被包装成了ApplicationListener对象,上面的类似于下面这种写法,这个应该比较好理解。

@Component
public class DefinitionAnnotationEventListener implements ApplicationListener<DefinitionEvent> {@Overridepublic void onApplicationEvent(DefinitionEvent event) {System.out.println("注解监听器:" + event.getMsg());}
}

查看SpringBoot的源码,找到下面的代码,因为我是Tomcat环境,这里创建的ApplicationContext是org.springframework.bootweb.servlet.context.AnnotationConfigServletWebServerApplicationContext

protected ConfigurableApplicationContext createApplicationContext() {Class<?> contextClass = this.applicationContextClass;if (contextClass == null) {try {switch (this.webApplicationType) {case SERVLET:contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);break;case REACTIVE:contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);break;default:contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);}}catch (ClassNotFoundException ex) {throw new IllegalStateException("Unable create a default ApplicationContext, " + "please specify an ApplicationContextClass",ex);}}return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);}

构造方法如下

public AnnotationConfigApplicationContext() {this.reader = new AnnotatedBeanDefinitionReader(this);this.scanner = new ClassPathBeanDefinitionScanner(this);
}

进入AnnotatedBeanDefinitionReader里面

/*** Create a new {@code AnnotatedBeanDefinitionReader} for the given registry,* using the given {@link Environment}.* @param registry the {@code BeanFactory} to load bean definitions into,* in the form of a {@code BeanDefinitionRegistry}* @param environment the {@code Environment} to use when evaluating bean definition* profiles.* @since 3.1*/public AnnotatedBeanDefinitionReader(BeanDefinitionRegistry registry, Environment environment) {Assert.notNull(registry, "BeanDefinitionRegistry must not be null");Assert.notNull(environment, "Environment must not be null");this.registry = registry;this.conditionEvaluator = new ConditionEvaluator(registry, environment, null);AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);}

再进到AnnotationConfigUtils的方法里面,省略了一部分代码,可以看到他注册了一个EventListenerMethodProcessor类到工厂了。这是一个BeanFactory的后置处理器。

public static Set<BeanDefinitionHolder> registerAnnotationConfigProcessors(BeanDefinitionRegistry registry, @Nullable Object source) {DefaultListableBeanFactory beanFactory = unwrapDefaultListableBeanFactory(registry);.................    if (!registry.containsBeanDefinition(EVENT_LISTENER_PROCESSOR_BEAN_NAME)) {RootBeanDefinition def = new RootBeanDefinition(EventListenerMethodProcessor.class);def.setSource(source);beanDefs.add(registerPostProcessor(registry, def, EVENT_LISTENER_PROCESSOR_BEAN_NAME));}............return beanDefs;}

查看这个BeanFactory的后置处理器EventListenerMethodProcessor,下面方法,他会遍历所有bean,找到其中带有@EventListener的方法,将它包装成ApplicationListenerMethodAdapter,注册到工厂里,这样就成功注册到Spring的监听系统里了。

@Overridepublic void afterSingletonsInstantiated() {ConfigurableListableBeanFactory beanFactory = this.beanFactory;Assert.state(this.beanFactory != null, "No ConfigurableListableBeanFactory set");String[] beanNames = beanFactory.getBeanNamesForType(Object.class);for (String beanName : beanNames) {if (!ScopedProxyUtils.isScopedTarget(beanName)) {Class<?> type = null;try {type = AutoProxyUtils.determineTargetClass(beanFactory, beanName);}catch (Throwable ex) {// An unresolvable bean type, probably from a lazy bean - let's ignore it.if (logger.isDebugEnabled()) {logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex);}}if (type != null) {if (ScopedObject.class.isAssignableFrom(type)) {try {Class<?> targetClass = AutoProxyUtils.determineTargetClass(beanFactory, ScopedProxyUtils.getTargetBeanName(beanName));if (targetClass != null) {type = targetClass;}}catch (Throwable ex) {// An invalid scoped proxy arrangement - let's ignore it.if (logger.isDebugEnabled()) {logger.debug("Could not resolve target bean for scoped proxy '" + beanName + "'", ex);}}}try {processBean(beanName, type);}catch (Throwable ex) {throw new BeanInitializationException("Failed to process @EventListener " +"annotation on bean with name '" + beanName + "'", ex);}}}}}private void processBean(final String beanName, final Class<?> targetType) {if (!this.nonAnnotatedClasses.contains(targetType) &&!targetType.getName().startsWith("java") &&!isSpringContainerClass(targetType)) {Map<Method, EventListener> annotatedMethods = null;try {annotatedMethods = MethodIntrospector.selectMethods(targetType,(MethodIntrospector.MetadataLookup<EventListener>) method ->AnnotatedElementUtils.findMergedAnnotation(method, EventListener.class));}catch (Throwable ex) {// An unresolvable type in a method signature, probably from a lazy bean - let's ignore it.if (logger.isDebugEnabled()) {logger.debug("Could not resolve methods for bean with name '" + beanName + "'", ex);}}if (CollectionUtils.isEmpty(annotatedMethods)) {this.nonAnnotatedClasses.add(targetType);if (logger.isTraceEnabled()) {logger.trace("No @EventListener annotations found on bean class: " + targetType.getName());}}else {// Non-empty set of methodsConfigurableApplicationContext context = this.applicationContext;Assert.state(context != null, "No ApplicationContext set");List<EventListenerFactory> factories = this.eventListenerFactories;Assert.state(factories != null, "EventListenerFactory List not initialized");for (Method method : annotatedMethods.keySet()) {for (EventListenerFactory factory : factories) {if (factory.supportsMethod(method)) {Method methodToUse = AopUtils.selectInvocableMethod(method, context.getType(beanName));ApplicationListener<?> applicationListener =factory.createApplicationListener(beanName, targetType, methodToUse);if (applicationListener instanceof ApplicationListenerMethodAdapter) {((ApplicationListenerMethodAdapter) applicationListener).init(context, this.evaluator);}context.addApplicationListener(applicationListener);break;}}}if (logger.isDebugEnabled()) {logger.debug(annotatedMethods.size() + " @EventListener methods processed on bean '" +beanName + "': " + annotatedMethods);}}}}

由方法生成Listener的逻辑由EventListenerFactory完成的,这又分为两种,一种是普通的@EventLintener另一种是@TransactionalEventListener,是由两个工厂处理的。

总结


上面介绍了@EventListener的原理,其实上面方法里还有一个@TransactionalEventListener注解,其实原理是一模一样的,只是这个监听者可以选择在事务完成后才会被执行,事务执行失败就不会被执行。

这两个注解的逻辑是一模一样的,并且@TransactionalEventListener本身就被标记有@EventListener,只是最后生成监听器时所用的工厂不一样而已。

515dd374948b8fdcdd891d11d3111416.gif

往期推荐

c3208287903c31d05fafb3657871699d.png

MyBatis 中为什么不建议使用 where 1=1?


11d5b519aa7b2a7a4575ff300e9f4640.png

MyBatis原生批量插入的坑与解决方案!


cd8ae4f228b782b998337c707ef0b418.png

聊聊sql优化的15个小技巧



本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/544471.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Matlab】模式识别——聚类算法集锦

文章目录0.聚类分析简介0.1.简单的聚类样本生成器1.静态聚类算法1.1.最近邻聚类算法1.1.1.算法原理1.1.2.参考代码1.1.3.参数选择及运行结果1.2.最大最小距离法1.2.1.算法原理1.2.2.参考代码1.2.3.参数选择及运行结果2.动态聚类算法2.1.C均值聚类算法2.1.1.算法原理2.1.2.参考代…

【MATLAB】混合粒子群算法原理、代码及详解

目录1.算法1.1.原理1.2.性能比较1.3.步骤2.代码2.1.源码及注释2.2.执行与效果1.算法 1.1.原理 \qquad建议没接触过粒子群算法的朋友先看较为基础的全局粒子群算法原理及介绍&#xff0c;以下博文链接有详细的讲解、代码及其应用举例&#xff1a; 【Simulink】粒子群算法&#…

MVC HtmlHelper用法大全

HtmlHelper用来在视图中呈现 HTML 控件。 以下列表显示了当前可用的一些 HTML 帮助器。 本主题演示所列出的带有星号 (*) 的帮助器。 ActionLink - 链接到操作方法。 BeginForm * - 标记窗体的开头并链接到呈现该窗体的操作方法。 CheckBox * - 呈现复选框。 DropDownList *…

基于 MyBatis 手撸一个分表插件

背景事情是酱紫的&#xff0c;上级leader负责记录信息的业务&#xff0c;每日预估数据量是15万左右&#xff0c;所以引入sharding-jdbc做分表。上级leader完成业务的开发后&#xff0c;走了一波自测&#xff0c;git push后&#xff0c;就忙其他的事情去了。项目的框架是SpringB…

密码学哈希函数_哈希函数在密码学中的应用

密码学哈希函数A Hash Function is a mathematical function that converts a numerical value into another compressed numeric value. The input value for the hash functions can be of arbitrary length, but the output text that it will produce will always be of fi…

C语言图形化界面——含图形、按钮、鼠标、进度条等部件制作(带详细代码、讲解及注释)

目录0.引言1.素材准备2.编程2.1.创建你的界面2.2.创建按钮2.3.鼠标操作2.3.1.单击特效2.3.2.光标感应2.3.3.进度条3.完整代码及效果0.引言 \qquad看了CSDN上很多关于C程序图形化界面的介绍&#xff0c;有的代码繁琐难解&#xff0c;不方便调试修改&#xff1b;有的不够详细。本…

【MATLAB】无人驾驶车辆的模型预测控制技术(精简讲解和代码)【运动学轨迹规划】

文章目录<font color#19C>0.友情链接<font color#19C>1.引言<font color#19C>2.预测模型<font color#19C>3.滚动优化<font color#08CF>3.1.线性化3.2.UrU_rUr​的求取<font color#08CF>3.3.离散化与序列化<font color#08CF>3.4.实现…

顶级Javaer,常用的 14 个类库

作者&#xff1a;小姐姐味道&#xff08;微信公众号ID&#xff1a;xjjdog&#xff09;昨天下载下来Java16尝尝鲜。一看&#xff0c;好家伙&#xff0c;足足有176MB大。即使把jmc和jvisualvm给搞了出去&#xff0c;依然还是这么大&#xff0c;真的是让人震惊不已。但即使JDK足够…

单层神经网络线性回归_单层神经网络| 使用Python的线性代数

单层神经网络线性回归A neural network is a powerful tool often utilized in Machine Learning because neural networks are fundamentally very mathematical. We will use our basics of Linear Algebra and NumPy to understand the foundation of Machine Learning usin…

面试官:说一下 final 和 final 的 4 种用法?

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;重要说明&#xff1a;本篇为博主《面试题精选-基础篇》系列中的一篇&#xff0c;查看系列面试文章请关注我。Gitee 开源地址…

面试官:int和Integer有什么区别?为什么要有包装类?

作者 | 磊哥来源 | Java面试真题解析&#xff08;ID&#xff1a;aimianshi666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;重要说明&#xff1a;本篇为博主《面试题精选-基础篇》系列中的一篇&#xff0c;查看系列面试文章请关注我。Gitee 开…

innodb是如何存数据的?yyds

前言如果你使用过mysql数据库&#xff0c;对它的存储引擎&#xff1a;innodb&#xff0c;一定不会感到陌生。众所周知&#xff0c;在mysql5以前&#xff0c;默认的存储引擎是&#xff1a;myslam。但mysql5之后&#xff0c;默认的存储引擎已经变成了&#xff1a;innodb&#xff…

【MATLAB】卡尔曼滤波器的原理及仿真(初学者专用)

文章目录0.引言1.场景预设2.卡尔曼滤波器3.仿真及效果0.引言 \qquad本文参考了Matlab对卡尔曼滤波器的官方教程及帮助文档&#xff08;Kalman Filter&#xff09;。官方教程的B站链接如下&#xff0c;在此对分享资源的Up主表示感谢。(如不能正常播放或需要看中文字幕&#xff0…

Go实现查找目录下(包括子目录)替换文件内容

为什么80%的码农都做不了架构师&#xff1f;>>> 【功能】 按指定的目录查找出文件&#xff0c;如果有子目录&#xff0c;子目录也将进行搜索&#xff0c;将其中的文件内容进行替换。 【缺陷】 1. 没有过滤出文本文件 2. 当文件过大时&#xff0c;效率不高 【代码】…

卡诺模板_无关条件的卡诺地图

卡诺模板Till now, the Boolean expressions which have been discussed by us were completely specified, i.e., for each combination of input variable we have specified a minterm by representing them as 1 in the K-Map. But, there may arise a case when for a giv…

面试官:final、finally、finalize 有什么区别?

作者 | 磊哥来源 | Java面试真题解析&#xff08;ID&#xff1a;aimianshi666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;重要说明&#xff1a;本篇为博主《面试题精选-基础篇》系列中的一篇&#xff0c;查看系列面试文章请关注我。Gitee 开…

【Matlab】扩展卡尔曼滤波器原理及仿真(初学者入门专用)

文章目录0.引言及友情链接1.场景预设2.扩展卡尔曼滤波器3.仿真及效果0.引言及友情链接 \qquad卡尔曼滤波器&#xff08;Kalman Filter, KF&#xff09;是传感器融合&#xff08;Sensor Fusion&#xff09;的基础&#xff0c;虽然知乎、CSDN、GitHub等平台已有大量的学习资料&am…

Windows 8.1 升级到专业版

本例将一台 Windows 8.1 平板升级到专业版。升级前&#xff1a;升级的原因&#xff0c;是因为用户发现这台平板不能启用远程桌面管理。查看计算机属性&#xff0c;显示如下&#xff1a;从上面的信息可以看出&#xff0c;目前这台平板安装的不是专业版。具体是什么版本呢&#x…

【MATLAB】求点到多边形的最短距离

文章目录0.引言1.原理2.代码及实用教程0.引言 \qquad点与多边形的关系无非三种——内部、上、外部。本文定义点在多边形内部距离为负&#xff0c;点在多边形边上距离为0&#xff0c;到多边形外部距离为正。 1.原理 计算点到多边形的距离分为3个步骤&#xff1a; 判断点与多边…

【Python】mmSegmentation语义分割框架教程(自定义数据集、训练设定、数据增强)

文章目录0.mmSegmentation介绍1.mmSegmentation基本框架1.1.mmSegmentation的model设置1.2.mmSegmentation的dataset设置1.2.1.Dataset Class文件配置1.2.2.Dataset Config文件配置1.2.3.Total Config文件配置2.运行代码 3.展示效果图和预测X.附录X.1.mmSegmentation框架解释 X…