spring aop实现原理_Spring 异步实现原理与实战分享

最近因为全链路压测项目需要对用户自定义线程池 Bean 进行适配工作,我们知道全链路压测的核心思想是对流量压测进行标记,因此我们需要给压测的流量请求进行打标,并在链路中进行传递,那么问题来了,如果项目中使用了多线程处理业务,就会造成父子线程间无法传递压测打标数据,不过可以利用阿里开源的 ttl 解决这个问题。

全链路压测项目的宗旨就是不让用户感知这个项目的存在,因此我们不可能让用户去对其线程池进行改造的,我们需要主动去适配用户自定义的线程池。

在适配过程的过程中无非就是将线程池替换成 ttl 去解决,可通过代理或者替换 Bean 的方式实现,这方面不是本文的内容,本文主要是深入 Spring 异步实现的原理,让大家对 Spring 异步编程不再陌生!

运行原理分析

过一遍源码分析,才能知道其中的一些细节原理,这也是不可避免的过程,虽然我也不想在文章中贴过多的源码,但如果不从源码中得出原因,很可能你会知其然不知其所以然。下面就尽量跟着源码走一遍它的运行机制是怎么样的,我把我自己的理解也会尽量详细地描述出来,在这里我会将其关联的源码贴出来分析,这些源码都有其相互关联性,可能你看到后面还会回来再看一遍。

注册通知器过程

开启 Spring 异步编程之需要一个注解即可:

@EnableAsync

Springboot 中有非常多 @Enable* 的注解,其目的是显式开启某一个功能特性,这也是一个非常典型的编程模型。

@EnableAsync 注解注入了一个 AsyncConfigurationSelector 类,这个类目的就是为了注入 ProxyAsyncConfiguration 自动配置类,它的父类 AbstractAsyncConfiguration 做了件事情:

org.springframework.scheduling.annotation.AbstractAsyncConfiguration#setConfigurers

1de14e9c584aa93b561726424e35ad27.png

我们可以实现 AsyncConfigurer 接口的方式去自定义一个线程池 Bean,这个后面会会讲到,源码所示,这里目的是为了这个 bean,并将其定义的线程池对象和异常处理对象保存到 AsyncConfiguration 中,用于创建 AsyncAnnotationBeanPostProcessor 。

f8173175dd65aafe03d9a46c7c09b150.png

这两个对象后面源码分析会再次遇上。

而这个配置类就是为了注册一个名为 AsyncAnnotationBeanPostProcessor 的 bean,如其名,它是一个 BeanPostProcessor 处理器,它的类继承结构如下所示:

68bab8f864bac85602dd8bfa2243b2fc.png

从类继承结构可以看出,AsyncAnnotationBeanPostProcessor 实现了 BeanPostProcessor 和 BeanFactoryAware,因此 AsyncAnnotationBeanPostProcessor 会在 setBeanFactory 方法中做了 Spring 异步编程中最为重要的一步,创建一个针对 @Async 注解的通知器 AsyncAnnotationAdvisor(叫做切面貌似也可以),这个通知器主要用于拦截被 @Async 注解的方法。同时,bean 实例初始化过程会被  AsyncAnnotationBeanPostProcessor 拦截处理,处理过程会将符合条件的 bean 注册 AsyncAnnotationAdvisor :

org.springframework.aop.framework.AbstractAdvisingBeanPostProcessor#postProcessAfterInitialization

1fa0e02e2756b95166ae3f2464fa0aad.png

创建通知器过程

接下来我们就分析 AsyncAnnotationAdvisor 是如何创建的。

4386604b8e8958304cad1f99788db2ed.png

AsyncAnnotationAdvisor 实现了 PointcutAdvisor 接口,因此需要同时实现 getPointcut 和 getAdvice 方法,而这两个方法的实际内容有以上红框创建实现。

到这里我们已经知道,Spring 的异步实现原理,是利用 Spring AOP 切面编程实现的,通过 BeanPostProcessor 拦截处理符合条件的 bean,并将切面织入,实现切面增强处理。

Spring AOP 编程核心概念:

  1. Advice:通知,切面的一种实现,可以完成简单的织入功能。通知定义了增强代码切入到目标代码的时间点,是目标方法执行之前执行,还是执行之后执行等。切入点定义切入的位置,通知定义切入的时间;
  2. Pointcut:切点,切入点指切面具体织入的方法;
  3. Advisor:切面的另一种实现,能够将通知以更为复杂的方式织入到目标对象中,是将通知包装为更复杂切面的装配器。

因此我们需要创建一个切面和切入点:

  • buildAdvice:
728dcf72ffa883ba4dda94c4127cc0fe.png

buildAdvice 方法可知,切面是一个 AnnotationAsyncExecutionInterceptor 类,该类实现了 MethodInterceptor 接口,其 invoke 方法即为拦截处理的核心源码,后面会进行详细分析。

  • buildPointcut:

从 AsyncAnnotationAdvisor 构造器中可以看出,buildPointcut 方法目的就是为了创建 @Async 注解的切入点。

通知器拦截处理过程

前面我们已经知道,拦截切面是一个 AnnotationAsyncExecutionInterceptor 类,我们直接定位到 invoke 方法一探究竟:

org.springframework.aop.interceptor.AsyncExecutionInterceptor#invoke

2a16d38c7f8ad1d0a198d96ba23c97a9.png

拦截处理的核心逻辑就是这么简单,也没啥好分析的,无非就是匹配方法指定的线程池,接着构建执行单元 Callable,最后调用 doSubmit 方法执行。

如何匹配线程池?

重点在于如何匹配线程池,这也是后面实战分析的重点内容,因此我们需要在这里详细分析匹配线程池的一些策略细节。

org.springframework.aop.interceptor.AsyncExecutionAspectSupport#determineAsyncExecutor

6ecc9fa62fe799816766696ff7670261.png

getExecutorQualifier 方法目的是获取 @Async 注解上的 value 值,value 值即线程池 Bean 的名称,如果获取到的 targetExecutor 不是 Spring 类型的线程池,则使用 TaskExecutorAdapter 进行适配,这也是为什么我们直接创建 Executor 类型的线程池 Spring 也是支持的原因。

从以上源码逻辑可看出如果我们使用 @Async 注解时 value 值为空,Spring 就会使用 defaultExecutor ,defaultExecutor 是什么时候赋值的呢?上面内容已经有提及,在 buildAdvice 方法创建 AnnotationAsyncExecutionInterceptor 时 调用了其 configure 方法,如下:

org.springframework.aop.interceptor.AsyncExecutionAspectSupport#configure

18e2625d68c98c06780c28c3f653a6f1.png

原来当 defaultExecutor 和 exceptionHandler 是当初从 ProxyAsyncConfiguration 中获取用户自定义的 AsyncConfigurer 实现类而来的,那么如果 defaultExecutor 不存在怎么办?从源码可看出,defaultExecutor 其实是一个 SingletonSupplier 类型,如果调用 get 方法不存在,则使用默认值,默认值为:

() -> getDefaultExecutor(this.beanFactory);

org.springframework.aop.interceptor.AsyncExecutionAspectSupport#getDefaultExecutor

90a168d1ffa92325e4f5e9160976ec54.png

注意第一个红框的注释,此时 Spring 寻找默认的线程池 Bean 为指定 Spring 的 TaskExecutor 类型,并非 Executor 类型,如果 Bean 容器中没有找到  TaskExecutor 类型的 Bean,则继续寻找默认为以下名称的 Bean:

public static final String DEFAULT_TASK_EXECUTOR_BEAN_NAME = "taskExecutor";

那么如果都没有找到怎么办呢?在这个方法直接返回 null 了,AsyncExecutionInterceptor 类覆写了 这个方法:

org.springframework.aop.interceptor.AsyncExecutionInterceptor#getDefaultExecutor607e5c9689abbd2e33410942893682ee.png

如果没有找到,则直接创建一个 SimpleAsyncTaskExecutor 类作为 @Async 注解底层使用的线程池。

从匹配线程池源码得知,如果你创建的线程池 Bean 非TaskExecutor 类型并且没有使用实现 AsyncConfigurer 接口方式创建线程池,就需要主动指定线程池 Bean 名称,否则 Spring 会使用默认策略。

总结

利用 BeanPostProcessor 机制在 Bean 初始化过程中创建一个 AsyncAnnotationAdvisor 切面,并且符合条件的 Bean 生成代理对象并将 AsyncAnnotationAdvisor 切面添加到代理中。

可以看出 Spring 的很多功能都是围绕着 Spring IOC 和 AOP 实现的。

Spring 默认线程池策略分析

有时候为了方便,我们不自定义创建线程池 bean 时,Spring 默认会为我们提供什么样的线程池呢?

我们先来看下结果:

b281f2075ca40484c9260db75f3fafad.png

很奇怪,明明我们都没有在项目中自定义线程池 Bean,按照以上源码的分析结果来看,此时 Spring 选择的是 SimpleAsyncTaskExecutor 才对,莫非是 super#getDefaultExecutor 方法找到了线程池 Bean?

从以上截图确实是找到了,而且类型还是 ThreadPoolTaskExecutor 类型的,那可以推断出 Spring 一定是在某个地方创建了一个 ThreadPoolTaskExecutor 类型的 Bean。

果然,在 spring-boot-autoconfigure 2.1.3.RELEASE 中,会在 org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration 中自动创建一个默认的 ThreadPoolTaskExecutor bean,getDefaultExecutor 方法会在容器中找到这个bean,并将其作为默认的 @Async 注解的执行线程池。

这里我为什么要标注版本呢?因为某些低版本的 spring-boot-autoconfigure,是没有 TaskExecutionAutoConfiguration 的,此时 Spring 就会选择 SimpleAsyncTaskExecutor。

org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration

9ea32c89d839d40c1e64534d72721270.png

从以上源码可以看出,默认的线程池的参数还可以手动在 properties 中配置,这意味着不需要主动创建线程池的情况下,也可以通过 properties 配置文件更改线程池相关参数。

创建线程池 Bean 的几种方式

1、直接创建一个 Bean 的方式,这貌似是最多人使用的方式,可以创建多个线程池 Bean,使用时指定线程池 Bean 名称:

@Bean("myTaskExecutor_1")
public Executor getThreadPoolTaskExecutor1() {
  final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
  // set ...
  return executor;
}

@Bean("myTaskExecutor_2")
public Executor getThreadPoolTaskExecutor2() {
  final ThreadPoolExecutor executor = new ThreadPoolExecutor();
  // set ...
  return executor;
}

2、实现 AsyncConfigurer 接口方式:

@Component
public class AsyncConfigurerTest implements AsyncConfigurer {

  private static final Logger LOGGER = LoggerFactory.getLogger(AsyncConfigurerTest.class);

  @Override
  public Executor getAsyncExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    // set ...
    return executor;
  }

  @Override
  public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
    return (ex, method, params) -> {
      LOGGER.info("Exception message:{}", ex.getMessage(), ex);
      LOGGER.info("Method name:{}", method.getName());
      for (Object param : params) {
        LOGGER.info("Parameter value:{}", param);
      }
    };
  }
}

这种方式可以方便定义异常处理的逻辑,不过从源码分析可以看出,项目中只能存在一个 AsyncConfigurer 的配置,意味着我们只能通过 AsyncConfigurer 配置一个自定义的线程池 Bean。

2e2ba7718d317101a63b3840882fb8f1.png

3、利用 spring-boot-autoconfigure 在 properties 配置线程池参数:

前面讲到了 Spring 默认线程池策略,这里利用 spring-boot-autoconfigure 默认创建一个 ThreadPoolTaskExecutor,通过  properties 自定义线程池相关参数。

这个方式的缺点就是类型固定为 ThreadPoolTaskExecutor,且只能有一个线程池。

注:以上所有原理分析与实战结果都是基于 Spring 5.1.5.RELEASE 版本。

原创不易,你的「在看」和「转发」,是对我最大的认可!

近期热文

从源码和日志文件结构中分析 Kafka 重启失败事件

记一次 Kafka 重启失败问题排查

图解:Kafka 水印备份机制

记一次 Kafka 集群线上扩容

Kafka重平衡机制

Seata 配置中心实现原理

Seata AT 模式启动源码分析

分布式事务中间件 Seata 的设计原理

我对支付平台架构设计的一些思考

聊聊 Tomcat 的架构设计

关于 Kafka 的一些面试题目

基于Jenkins Pipeline自动化部署

RocketMQ消息发送的高可用设计

深度解析RocketMQ Topic的创建机制

mybatis-plus 源码分析之sql注入器

Mybatis源码分析之Mapper注册与绑定

从源码的角度解析线程池运行原理

关于线程池你不得不知道的一些设置

你都理解创建线程池的参数吗?

Java并发之AQS源码分析(二)

Java并发之AQS源码分析(一)

bb296a36db1d3ad5cf3a837e814a9d3d.png

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

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

相关文章

基于Springboot外卖系统07:员工分页查询+ 分页插件配置+分页代码实现

1. 员工分页查询 1.1 需求分析 在分页查询页面中, 以分页的方式来展示列表数据,以及查询条件 "员工姓名"。 请求参数 搜索条件: 员工姓名(模糊查询) 分页条件: 每页展示条数 , 页码 响应数据 总记录数 结果列表 1…

1045-Access denied for user 'root'@'localhost'(using password:YES)

解决: 1. 开始 --> cmd --> net stop mysql (停用MySQL服务 没启动的可以省略) 2. 找到安装路径 MySQL Server 5.1下的my.ini 3. 打开 my.ini 找到 [mysqld] 然后在下面加上 这句: skip_grant_tables (意思好像是 启动MySQL服务…

arial字体可以商用吗_【工作总结】莫让字体版权引火上身

前段一条微软雅黑字体引发的巨额罚款新闻,引起国内多个TW大群小地震,人人自危。我也赶紧检查自家文档、商用出版物、网站的字体,以免给公司带来法务后患。把这两天收集的信息,采取的行动记录一下。哪些中文字体免费?事…

基于Springboot外卖系统08:员工账号状态管理功能+对象转换器+扩展Spring mvc的消息转换器

1. 员工账号状态管理 1.1 需求分析 在员工管理列表页面,可以对某个员工账号进行启用或者禁用操作。账号禁用的员工不能登录系统,启用后的员工可以正常登录。如果某个员工账号状态为正常,则按钮显示为 "禁用",如果员工…

基于Springboot外卖系统09:员工信息编辑+员工信息保存

1 编辑员工信息功能 1.1 需求分析 在员工管理列表页面点击 "编辑" 按钮,跳转到编辑页面,在编辑页面回显员工信息并进行修改,最后点击 "保存" 按钮完成编辑操作。 那么从上述的分析中,当前实现的编辑功能需…

bcp 不能调用where 子句_MySQL中IS NULL、IS NOT NULL、!=不能用索引?胡扯!

不知道从什么时候开始,网上流传着这么一个说法:MySQL的WHERE子句中包含 IS NULL、IS NOT NULL、! 这些条件时便不能使用索引查询,只能使用全表扫描。这种说法愈演愈烈,甚至被很多同学奉为真理。咱啥话也不说,举个例子。…

基于Springboot外卖系统10:公共字段填充功能+ThreadLocal模块改进

1. 公共字段自动填充 1.1 问题分析 在新增员工时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工时需要设置修改时间、修改人等字段。这些字段属于公共字段,也就是也就是在系统中很多表中都会有这些字段,如下: 而…

腾讯云挂在和格式化数据盘

新购买了数据盘时,需要格式化才可使用。未购买数据盘的用户可以跳过此步骤。也可以根据需要进行多分区操作。 这里以Windows 2012R2为例进行格式化说明。 1) 通过步骤四介绍的方法登录Windows云服务器。 2) 点击【开始】(Start)-【服务器管理…

基于Springboot外卖系统11:菜品新增类别+类别信息分页查询

1. 新增分类 1.1 需求分析 后台系统中可以管理分类信息,分类包括两种类型,分别是 菜品分类 和 套餐分类 。当我们在后台系统中添加菜品时需要选择一个菜品分类,在后台系统中添加一个套餐时需要选择一个套餐分类,在移动端也会按照…

基于Springboot外卖系统12:删除菜品套餐类别+修改套餐类别信息

1. 删除分类 1.1 需求分析 在分类管理列表页面,可以对某个分类进行删除操作。需要注意的是当分类关联了菜品或者套餐时,此分类不允许删除。 1.2 前端页面分析 在前端页面中,点击 "删除" 按钮,就会触发定义的方法&…

html5手机移动端三级联动城市选择器

//我的地址 var area1 new LArea(); area1.init({ trigger: #demo1, //触发选择控件的文本框,同时选择完毕后name属性输出到该位置 valueTo: #value1, //选择完毕后id属性输出到该位置 keys: { id: id, name: name }, //绑定数据源相关字段 id对应valueTo的value属…

我的师傅是风清扬

1 珍惜那些处处帮助你的人 国庆假期就要过去了,国庆这几天一直在考虑一个问题(先保留是什么问题),也咨询了几个比较信任的朋友,都没有得到肯定的答案。 回家路上跟了一个大哥哥(陈哥)&#xf…

要多大内存才满足_佛龛的尺寸要多大?

佛龛是用于供奉佛像或者牌位的小阁子,大多数是木制家具。佛龛在一般是仿中国古代的房子等工程建筑制做而成,在其纹样层面有很高的要求,有关纹样今日姑且先不谈。除此之外佛龛的尺寸大小多少才算吉祥也是很有讲究的,那麼这个问题跟…

【YOLOV5-6.x讲解】数据配置文件 data/XXX.yaml

主干目录: 【YOLOV5-6.x 版本讲解】整体项目代码注释导航现在YOLOV5已经更新到6.X版本,现在网上很多还停留在5.X的源码注释上,因此特开一贴传承开源精神!5.X版本的可以看其他大佬的帖子本文章主要从6.X版本出发,主要解…

python print 输出到txt_(Python基础教程之七)Python字符串操作

Python基础教程在SublimeEditor中配置Python环境Python代码中添加注释Python中的变量的使用Python中的数据类型Python中的关键字Python字符串操作Python中的list操作Python中的Tuple操作Pythonmax()和min()–在列表或数组中查找最大值和最小值Python找到最大的N个(前N个)或最小…

【YOLOV5-6.x讲解】常用工具类 models/common.py

主干目录: 【YOLOV5-6.x 版本讲解】整体项目代码注释导航现在YOLOV5已经更新到6.X版本,现在网上很多还停留在5.X的源码注释上,因此特开一贴传承开源精神!5.X版本的可以看其他大佬的帖子本文章主要从6.X版本出发,主要解…

【YOLOV5-6.x讲解】DIY实验文件 models/experimental.py

主干目录: 【YOLOV5-6.x 版本讲解】整体项目代码注释导航现在YOLOV5已经更新到6.X版本,现在网上很多还停留在5.X的源码注释上,因此特开一贴传承开源精神!5.X版本的可以看其他大佬的帖子本文章主要从6.X版本出发,主要解…

mysql 触发器_MySQL入门之触发器

触发器作用当操作了某张表时,希望同时触发一些动作/行为,可以使用触发器完成!!例如: 当向员工表插入一条记录时,希望同时往日志表插入数据。首先创建日志表-- 日志表CREATE TABLE test_log(id INT PRIMARY …

【YOLOV5-6.x讲解】模型搭建模块 models/yolo.py

主干目录: 【YOLOV5-6.x 版本讲解】整体项目代码注释导航现在YOLOV5已经更新到6.X版本,现在网上很多还停留在5.X的源码注释上,因此特开一贴传承开源精神!5.X版本的可以看其他大佬的帖子本文章主要从6.X版本出发,主要解…

C++primer拾遗(第八章:IO库)

第八章内容不多,不过包含比较实用的文件读写操作。 总结不易,转载注明出处,谢谢。 http://www.cnblogs.com/linhaowei0389/ 转载于:https://www.cnblogs.com/linhaowei0389/p/6628471.html