【微服务解耦之事件启动】Spring Boot 解耦之事件驱动

news2024/12/28 3:37:22

一、前言

简介: 在项目实际开发过程中,我们有很多这样的业务场景:一个事务中处理完一个业务逻辑后需要跟着处理另外一个业务逻辑,伪码大致如下:

@Service
public class ProductServiceImpl {
  ...
    public void saveProduct(Product product) {
        productMapper.saveOrder(product);
        notifyService.notify(product);
    }
  ...
}

很简单并且很常见的一段业务逻辑:首先将产品先保存数据库,然后发送通知。
某一天你们可能需要把新增的产品存到Es中,这时候也需要代码可能变成这样:

@Service
public class ProductServiceImpl {
  ...
    public void saveProduct(Product product) {
        productMapper.saveProduct(product);
        esService.saveProduct(product)
        notifyService.notify(product);
    }
  ...
}:

随着业务需求的变化,代码也需要跟着一遍遍的修改。而且还会存在另外一个问题,如果通知系统挂了,那就不能再新增产品了。
对于上面这种情况非常适合引入消息中间件(消息队列)来对业务进行解耦,但并非所有的业务系统都会引入消息中间件(引入会第三方架构组件会带来很大的运维成本)。
Spring提供了事件驱动机制可以帮助我们实现这一需求。

Spring事件驱动

spring事件驱动由3个部分组成

ApplicationEvent:表示事件本身,自定义事件需要继承该类,用来定义事件
ApplicationEventPublisher:事件发送器,主要用来发布事件
ApplicationListener:事件监听器接口,监听类实现ApplicationListener 里onApplicationEvent方法即可,也可以在方法上增加@EventListener以实现事件监听。

实现Spring事件驱动一般只需要三步:
自定义需要发布的事件类,需要继承ApplicationEvent类
使用ApplicationEventPublisher来发布自定义事件
使用@EventListener来监听事件
这里需要特别注意一点,默认情况下事件是同步的。即事件被publish后会等待Listener的处理。如果发布事件处的业务存在事务,监听器处理也会在相同的事务中。如果需要异步处理事件,可以onApplicationEvent方法上加@Aync支持异步或在有@EventListener的注解方法上加上@Aync。

源码实战

• 创建事件

public class ProductEvent extends ApplicationEvent {
    public ProductEvent(Product product) {
        super(product);
    }
}

• 发布事件

@Service
public class ProductServiceImpl implements IproductService {
    ...
    @Autowired
    private ApplicationEventPublisher publisher;
    
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void saveProduct(Product product) {
        productMapper.saveProduct(product); 
        //事件发布
        publisher.publishEvent(product);
    }
    ...
}

• 事件监听

@Slf4j
@AllArgsConstructor
public class ProductListener {
    private final NotifyService notifyServcie;
    @Async
    @Order
    @EventListener(ProductEvent.class)
    public void notify(ProductEvent event) {
        Product product = (Product) event.getSource();
        notifyServcie.notify(product, "product");
    }
}

• 在SpringBoot启动类上增加@EnableAsync 注解
@Slf4j
@EnableSwagger2
@SpringBootApplication
@EnableAsync
public class ApplicationBootstrap {
...
}

• 使用了Async后会使用默认的线程池SimpleAsyncTaskExecutor,一般我们会在项目中自定义一个线程池。

@Configuration
public class ExecutorConfig {
    /** 核心线程数 */
    private int corePoolSize = 10;
    /** 最大线程数  */
    private int maxPoolSize = 50;
    /** 队列大小  */
    private int queueCapacity = 10;
    /** 线程最大空闲时间   */
    private int keepAliveSeconds = 150;
    @Bean("customExecutor")
    public Executor myExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(corePoolSize);
        executor.setMaxPoolSize(maxPoolSize);
        executor.setQueueCapacity(queueCapacity);
        executor.setThreadNamePrefix("customExecutor-");
        executor.setKeepAliveSeconds(keepAliveSeconds);
        // rejection-policy:当pool已经达到max size的时候,如何处理新任务
        // CALLER_RUNS:不在新线程中执行任务,而是由调用者所在的线程来执行
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }

SpringBoot 的事件是基于Spring的事件,所以我们先介绍Spring事件原理。
明白了使用,我们再来看看原理:

1.Spring 事件原理

核心类:
ApplicationEventMulticaster,事件派发器。
ApplicationListener,事件监听类。
ApplicationEvent,事件类。
事件派发器派发事件,事件监听类监听派发的事件。

1.ApplicationEventMulticaster事件派发器的注册时机,何时被注册到Spring容器内部的?

我们找到Spring容器创建Bean的流程中,AbstractApplicationContext#refresh这个方法里:

// Initialize message source for this context.
	initMessageSource();

	// 在这一步骤中,初始化了事件派发器
	// Initialize event multicaster for this context.
	initApplicationEventMulticaster();

	// Initialize other special beans in specific context subclasses.
	onRefresh();

initApplicationEventMulticaster方法:

	protected void initApplicationEventMulticaster() {
		ConfigurableListableBeanFactory beanFactory = getBeanFactory();
		...
		// 这一步骤创建了派发器
		this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
		// 注册到单例池里面
		beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster);
		...
	}

2.当我们调用了publishEvent(ApplicationEvent event);监听类怎么就会执行了呢?
我们点进该方法里面AbstractApplicationContext#publishEvent():

// 会调用ApplicationEventMulticaster的multicastEvent()方法
getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);

ApplicationEventMulticaster#multicastEvent():

	public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
		ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
		// 从派发器里面获取线程池对象
		Executor executor = getTaskExecutor();
		// getApplicationListeners(event, type) 或获取该事件对象类型的所有监听器
		for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
			if (executor != null) {
				// 线程池异步调用
				executor.execute(() -> invokeListener(listener, event));
			}
			else {
				// 直接调用
				invokeListener(listener, event);
			}
		}
	}

invokeListener方法:

protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
		ErrorHandler errorHandler = getErrorHandler();
		if (errorHandler != null) {
			try {
				// 这个直接调用 listener.onApplicationEvent(event); 方法
				doInvokeListener(listener, event);
			}
			catch (Throwable err) {
				errorHandler.handleError(err);
			}
		}
		else {
			doInvokeListener(listener, event);
		}
	}

总结:当我们调用 applicationContext.publishEvent(xxx);方法时,Spring内部会拿到关于此事件对象的所有监听器类对象,直接调用执行。

SpringBoot 启动事件流程

SpringBoot在启动时,会默认发布一些事件,我们可以自定义监听类实现监听该事件,做一些初始化等等操作,一些框架整合就是通过事件监听的方式进行内部的初始化。
值得注意的是SpringBoot对于有些事件的监听是只能通过读取配置里面配置的监听类才能生效,直接注解的方式是无法监听到的!
为什么会这样呢?
因为我们直接加注解的话,是必须要经过Spring容器的洗礼,有些事件的发布是在容器refresh之前做的,所以注解的方式是没办法生效的!

SpringBoot提供了配置文件的方式去配置监听器,那么配置文件中的实现类是何时获取到的呢?
在这里插入图片描述
在构造器里面获取的,SPI机制加载实现类。
我们要监听一些容器刷新之前的内部事件只能在spring.factories中指定。

启动时会发布的事件顺序:

  • ApplicationStartingEvent: 准备启动SpringBoot环境之前。
  • ApplicationEnvironmentPreparedEvent: 环境变量初始化完成,应用启动之前。
  • ApplicationContextInitializedEvent:应用初始化器执行后发布
  • ApplicationPreparedEvent:应用准备就绪,容器初始化之前发布。
  • ApplicationStartedEvent:容器初始化完成之后发布。
  • ApplicationReadyEvent:容器已经初始化完成并且已经可以接收Web请求之后发布。
  • ApplicationFailedEvent:容器启动失败。
    除此之外,在ApplicationPreparedEvent之后和ApplicationStartedEvent之前还将发布以下事件:
  • ContextRefreshedEvent:容器刷新完成发布。 WebServerInitializedEvent
    :在WebServer准备就绪后发送。ServletWebServerInitializedEvent和ReactiveWebServerInitializedEvent分别是servlet和reactive变体。

以上的事件对象都继承 SpringApplicationEvent 类,SpringApplicationEvent 又继承 ApplicationEvent。

public abstract class SpringApplicationEvent extends ApplicationEvent {

	private final String[] args;

	public SpringApplicationEvent(SpringApplication application, String[] args) {
		super(application);
		this.args = args;
	}

	public SpringApplication getSpringApplication() {
		return (SpringApplication) getSource();
	}

	public final String[] getArgs() {
		return this.args;
	}

}

以上事件的发布类是:EventPublishingRunListener。

EventPublishingRunListener 类是SpringBoot类似通过SPI机制加载进来的:

public ConfigurableApplicationContext run(String... args) {
		StopWatch stopWatch = new StopWatch();
		...
		// 这一步加载进来的
		SpringApplicationRunListeners listeners = getRunListeners(args);
		listeners.starting();
}		

在这里插入图片描述
EventPublishingRunListener 实现了 SpringApplicationRunListener接口,该接口里面规范了SpringBoot启动时机的事件发布流程。

public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {
	private final SpringApplication application;
	// 事件派发器对象
	private final SimpleApplicationEventMulticaster initialMulticaster;
  ....
}

参考文献:
1、https://blog.csdn.net/qq_33549942/article/details/122992865
2、https://developer.aliyun.com/article/829271

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

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

相关文章

【FreeSwitch开发实践】外呼线路电话收不到回铃音问题的解决

✨ 博客主页&#xff1a;小小马车夫的主页 ✨ 所属专栏&#xff1a;FreeSwitch开发实践 ✨ 专栏介绍&#xff1a;主要介绍博主在实际项目中使用FreeSwitch开发外呼类项目的一些经验心得&#xff0c;主要涉及FreeSwitch的基本安装编译、基本配置、ESL、WSS、录音、自定义模块、m…

从URL输入到页面展现到底发生什么?

从开发&运维角度方面来看&#xff0c;总体来说分为以下几个过程&#xff1a; DNS 解析:将域名解析成 IP 地址TCP 连接&#xff1a;TCP 三次握手发送 HTTP 请求服务器处理请求并返回 HTTP 报文浏览器解析渲染页面断开连接&#xff1a;TCP 四次挥手 一、什么是URL&#xff…

[附源码]java毕业设计书店网站论文

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

如何从0到1实现一个基于bitcask的kv存储引擎

愿景 ​ 今年大部分业余时间都在nutsdb的开源贡献上&#xff0c;nutsdb是基于bitcask模型实现的持久化存储引擎&#xff0c;提供了诸如list&#xff0c;set等多种丰富的数据结构。近来很多小伙伴&#xff0c;其中也有一些我的好朋友陆陆续续加入到这个项目上来。为了帮助小伙伴…

pytorch中Dataset和Dataloader的使用

1.datasets下载数据集 root :代表着路径&#xff0c;表示现存或者准备存储的地方。 train :代表是否下载训练数据集&#xff0c;如果否的话就下载测试数据集 transform: 如果想对数据集进行什么变化&#xff0c;在这里进行操作 target_transform:跟上面的一样 download:如果是T…

ORB-SLAM2 ---- Tracking::CreateInitialMapMonocular函数

目录 1.函数作用 2.函数解析 2.1 调用函数解析 2.2 Tracking::CreateInitialMapMonocular函数总体思路 2.2.1 代码 2.2.2 总体思路解析 2.3 MapPoint::ComputeDistinctiveDescriptors函数解析 2.3.1 函数作用 2.3.2 代码 2.3.3 函数解析 2.4 MapPoint::UpdateNor…

idea使用谷歌翻译

项目场景&#xff1a; idea google翻译 问题描述 由于某些原因&#xff0c;现在谷歌翻译一直不能正常使用… 解决方案&#xff1a; 使用 pigcha 工具 设置也超级简单&#xff0c;每个月也就三十多块钱&#xff0c;可正常使用国内外网络。 电脑网络代理设置如下&#xff1a;…

XSS平台与cookie获取

今天继续给大家介绍渗透测试相关知识&#xff0c;本文主要内容是XSS平台与cookie获取。 免责声明&#xff1a; 本文所介绍的内容仅做学习交流使用&#xff0c;严禁利用文中技术进行非法行为&#xff0c;否则造成一切严重后果自负&#xff01; 再次强调&#xff1a;严禁对未授权…

[附源码]SSM计算机毕业设计大学生心理咨询网站JAVA

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

【ElasticSearch学习笔记】一、ES下载、安装、目录结构、root用户权限问题、kibana下载安装

下载和安装一、下载二、安装2.1 JDK的安装2.2 ElasticSearch的安装2.3 启动ES2.4 多节点启动三、Kibana的安装一、下载 以下载7.10.0为例&#xff1a; https://www.elastic.co/cn/downloads/elasticsearch 选择对应的操作系统&#xff0c;我是为了安装在CentOS上面&#xff0c…

微信小程序 | 做一个小程序端的扫雷游戏

&#x1f4cc;个人主页&#xff1a;个人主页 ​&#x1f9c0; 推荐专栏&#xff1a;小程序开发成神之路 --【这是一个为想要入门和进阶小程序开发专门开启的精品专栏&#xff01;从个人到商业的全套开发教程&#xff0c;实打实的干货分享&#xff0c;确定不来看看&#xff1f; …

【第三部分 | 移动端开发】3:Flex布局

目录 | Flex布局简介 | Flex父元素属性 设置主轴的方向 flex-direction 设置主轴上的子元素排列方式 justify-content 设置子元素是否换行 flex-wrap 设置侧轴上的子元素排列方式&#xff08;单行&#xff09; align-items 设置侧轴上的子元素的排列方式&#xff08;多行…

HIve数仓新零售项目DWD层的构建

HIve数仓新零售项目 注&#xff1a;大家觉得博客好的话&#xff0c;别忘了点赞收藏呀&#xff0c;本人每周都会更新关于人工智能和大数据相关的内容&#xff0c;内容多为原创&#xff0c;Python Java Scala SQL 代码&#xff0c;CV NLP 推荐系统等&#xff0c;Spark Flink Kaf…

网络是怎样链接的--向DNS服务器查询Web服务器的IP地址

文章目录2.1 IP地址的基本知识2.2 域名和IP地址共用理由2.3 DNS本质是什么2.4 浏览器如何获取IP2.5 DNS解析器内部工作原理2.1 IP地址的基本知识 浏览器能够解析网址并生成HTTP消息&#xff0c;但并不具备将消息发送到网络中的功能&#xff0c;因此这一功能需要委托操作系统来…

DataObjectImpl

DataObjectImpl目录概述需求&#xff1a;设计思路实现思路分析1.DataObjectImpl2.DeadLetterJobQueryImpl3.DeploymentQueryImpl4.Direction5.DynamicBpmnServiceImpl参考资料和推荐阅读Survive by day and develop by night. talk for import biz , show your perfect code,fu…

IDEA2022版本创建maven web项目(两种方式)

目录 一、使用骨架的方式 二、maven中添加 web方式 总结&#xff1a; 前言&#xff1a; 创建maven web项目有两种方式&#xff0c;一种是使用骨架方式&#xff0c;一种是不使用骨架的方式 一、使用骨架的方式 1.打开idea&#xff0c;按照步骤创建一个新的项目 2.点击Mave…

【高级篇】Java JVM实战 之 内存调优

文章目录一、通过Jprofiler调式Dump文件错误⛅ 什么是Jprofiler&#xff1f;⚡使用Jprofiler调试Dump文件二、堆内存调优三、 GC垃圾回收器四、GC常用算法❄️引用计数法⛄复制算法♨️标记清除算法⛽标记压缩⚠️标记清除压缩五、JMM⛵小结一、通过Jprofiler调式Dump文件错误 …

Spark 3.0 - 4.Pipeline 管道的工作流程

目录 一.引言 二.基本组件 三.Pipeline 基本流程 1.训练 Pipeline - Estimator 2.预测 Pipeline - Transformer 四.Pipeline 分解与构造 1.DataFrame 2.Transformer1 - Tokenizer 3.Transformer2 - HashingTF 4.Estimator - LR 5.Pipeline With ParamMap - Estimat…

SpringCloud微服务(一)——Consul服务注册中心

Consul服务注册中心 SpringCloud 中文官网&#xff1a;https://www.springcloud.cc/spring-cloud-consul.html Consul是一套开源的分布式服务发现和配置管理系统&#xff0c;Go语言开发。 Consul是一个服务网格&#xff08;微服务间的 TCP/IP&#xff0c;负责服务之间的网络…

SharedPreferences存储

文章目录 前言 听说SharedPreferences存储技术快过时了&#xff0c;不过如果是单纯的使用的话&#xff0c;不费什么时间成本。 本文的Demo摘录自《第一行代码》。 一.什么是SharedPreferences SharedPreferences&#xff0c;一种通过使用键值对的方式来存储数据的技术。 二…