spring aop实现原理_Spring 异步实现原理与实战分享
最近因为全链路压测项目需要对用户自定义线程池 Bean 进行适配工作,我们知道全链路压测的核心思想是对流量压测进行标记,因此我们需要给压测的流量请求进行打标,并在链路中进行传递,那么问题来了,如果项目中使用了多线程处理业务,就会造成父子线程间无法传递压测打标数据,不过可以利用阿里开源的 ttl 解决这个问题。
全链路压测项目的宗旨就是不让用户感知这个项目的存在,因此我们不可能让用户去对其线程池进行改造的,我们需要主动去适配用户自定义的线程池。
在适配过程的过程中无非就是将线程池替换成 ttl 去解决,可通过代理或者替换 Bean 的方式实现,这方面不是本文的内容,本文主要是深入 Spring 异步实现的原理,让大家对 Spring 异步编程不再陌生!
运行原理分析
过一遍源码分析,才能知道其中的一些细节原理,这也是不可避免的过程,虽然我也不想在文章中贴过多的源码,但如果不从源码中得出原因,很可能你会知其然不知其所以然。下面就尽量跟着源码走一遍它的运行机制是怎么样的,我把我自己的理解也会尽量详细地描述出来,在这里我会将其关联的源码贴出来分析,这些源码都有其相互关联性,可能你看到后面还会回来再看一遍。
注册通知器过程
开启 Spring 异步编程之需要一个注解即可:
@EnableAsync
Springboot 中有非常多 @Enable* 的注解,其目的是显式开启某一个功能特性,这也是一个非常典型的编程模型。
@EnableAsync 注解注入了一个 AsyncConfigurationSelector 类,这个类目的就是为了注入 ProxyAsyncConfiguration 自动配置类,它的父类 AbstractAsyncConfiguration 做了件事情:
org.springframework.scheduling.annotation.AbstractAsyncConfiguration#setConfigurers
我们可以实现 AsyncConfigurer 接口的方式去自定义一个线程池 Bean,这个后面会会讲到,源码所示,这里目的是为了这个 bean,并将其定义的线程池对象和异常处理对象保存到 AsyncConfiguration 中,用于创建 AsyncAnnotationBeanPostProcessor 。
这两个对象后面源码分析会再次遇上。
而这个配置类就是为了注册一个名为 AsyncAnnotationBeanPostProcessor 的 bean,如其名,它是一个 BeanPostProcessor 处理器,它的类继承结构如下所示:
从类继承结构可以看出,AsyncAnnotationBeanPostProcessor 实现了 BeanPostProcessor 和 BeanFactoryAware,因此 AsyncAnnotationBeanPostProcessor 会在 setBeanFactory 方法中做了 Spring 异步编程中最为重要的一步,创建一个针对 @Async 注解的通知器 AsyncAnnotationAdvisor(叫做切面貌似也可以),这个通知器主要用于拦截被 @Async 注解的方法。同时,bean 实例初始化过程会被 AsyncAnnotationBeanPostProcessor 拦截处理,处理过程会将符合条件的 bean 注册 AsyncAnnotationAdvisor :
org.springframework.aop.framework.AbstractAdvisingBeanPostProcessor#postProcessAfterInitialization
创建通知器过程
接下来我们就分析 AsyncAnnotationAdvisor 是如何创建的。
AsyncAnnotationAdvisor 实现了 PointcutAdvisor 接口,因此需要同时实现 getPointcut 和 getAdvice 方法,而这两个方法的实际内容有以上红框创建实现。
到这里我们已经知道,Spring 的异步实现原理,是利用 Spring AOP 切面编程实现的,通过 BeanPostProcessor 拦截处理符合条件的 bean,并将切面织入,实现切面增强处理。
Spring AOP 编程核心概念:
- Advice:通知,切面的一种实现,可以完成简单的织入功能。通知定义了增强代码切入到目标代码的时间点,是目标方法执行之前执行,还是执行之后执行等。切入点定义切入的位置,通知定义切入的时间;
- Pointcut:切点,切入点指切面具体织入的方法;
- Advisor:切面的另一种实现,能够将通知以更为复杂的方式织入到目标对象中,是将通知包装为更复杂切面的装配器。
因此我们需要创建一个切面和切入点:
- buildAdvice:
buildAdvice 方法可知,切面是一个 AnnotationAsyncExecutionInterceptor 类,该类实现了 MethodInterceptor 接口,其 invoke 方法即为拦截处理的核心源码,后面会进行详细分析。
- buildPointcut:
从 AsyncAnnotationAdvisor 构造器中可以看出,buildPointcut 方法目的就是为了创建 @Async 注解的切入点。
通知器拦截处理过程
前面我们已经知道,拦截切面是一个 AnnotationAsyncExecutionInterceptor 类,我们直接定位到 invoke 方法一探究竟:
org.springframework.aop.interceptor.AsyncExecutionInterceptor#invoke
拦截处理的核心逻辑就是这么简单,也没啥好分析的,无非就是匹配方法指定的线程池,接着构建执行单元 Callable,最后调用 doSubmit 方法执行。
如何匹配线程池?
重点在于如何匹配线程池,这也是后面实战分析的重点内容,因此我们需要在这里详细分析匹配线程池的一些策略细节。
org.springframework.aop.interceptor.AsyncExecutionAspectSupport#determineAsyncExecutor
getExecutorQualifier 方法目的是获取 @Async 注解上的 value 值,value 值即线程池 Bean 的名称,如果获取到的 targetExecutor 不是 Spring 类型的线程池,则使用 TaskExecutorAdapter 进行适配,这也是为什么我们直接创建 Executor 类型的线程池 Spring 也是支持的原因。
从以上源码逻辑可看出如果我们使用 @Async 注解时 value 值为空,Spring 就会使用 defaultExecutor ,defaultExecutor 是什么时候赋值的呢?上面内容已经有提及,在 buildAdvice 方法创建 AnnotationAsyncExecutionInterceptor 时 调用了其 configure 方法,如下:
org.springframework.aop.interceptor.AsyncExecutionAspectSupport#configure
原来当 defaultExecutor 和 exceptionHandler 是当初从 ProxyAsyncConfiguration 中获取用户自定义的 AsyncConfigurer 实现类而来的,那么如果 defaultExecutor 不存在怎么办?从源码可看出,defaultExecutor 其实是一个 SingletonSupplier 类型,如果调用 get 方法不存在,则使用默认值,默认值为:
() -> getDefaultExecutor(this.beanFactory);
org.springframework.aop.interceptor.AsyncExecutionAspectSupport#getDefaultExecutor
注意第一个红框的注释,此时 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#getDefaultExecutor
如果没有找到,则直接创建一个 SimpleAsyncTaskExecutor 类作为 @Async 注解底层使用的线程池。
从匹配线程池源码得知,如果你创建的线程池 Bean 非TaskExecutor 类型并且没有使用实现 AsyncConfigurer 接口方式创建线程池,就需要主动指定线程池 Bean 名称,否则 Spring 会使用默认策略。
总结
利用 BeanPostProcessor 机制在 Bean 初始化过程中创建一个 AsyncAnnotationAdvisor 切面,并且符合条件的 Bean 生成代理对象并将 AsyncAnnotationAdvisor 切面添加到代理中。
可以看出 Spring 的很多功能都是围绕着 Spring IOC 和 AOP 实现的。
Spring 默认线程池策略分析
有时候为了方便,我们不自定义创建线程池 bean 时,Spring 默认会为我们提供什么样的线程池呢?
我们先来看下结果:
很奇怪,明明我们都没有在项目中自定义线程池 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
从以上源码可以看出,默认的线程池的参数还可以手动在 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 接口方式:
@Componentpublic 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。
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源码分析(一)
spring aop实现原理_Spring 异步实现原理与实战分享相关推荐
- spring aop实例讲解_Spring框架核心知识点
文章内容输出来源:拉勾教育Java高薪训练营 前言: 由于工作需要提升自身技术能力,在各方比较下,报名了拉勾教育的java高薪训练营,目前已经学了半个月啦,来说说自身学习的感受吧: 课程内容有广度更有 ...
- java spring aop 注解包_Spring AOP 注解配置实例
Spring AOP注解例子 一:导入相关jar包. 首先导入Spring的相关包(这里就不多说了,我这里是3.2.4版本的) 然后导入AOP注解的相关包(不是spring的包)aspectjrt-1 ...
- spring aop实例讲解_Spring核心技术详解(一)
一.Sring简介 Spring是一个分层的Java SE/EE应用一站式的轻量级开源框架.Spring核心是IOC和AOP. Spring主要优点包括: 方便解耦,简化开发,通过Spring提供的I ...
- 底层原理_Spring框架底层原理IoC
一.概述 Spring是一个轻量级的开源JavaEE框架 Spring可以解决企业应用开发的复杂性 Spring两大核心部分:IoC和AOP 特点: 方便解耦,简化开发 AOP编程支持 方便程序测试 ...
- Spring AOP 增强框架 Nepxion Matrix 详解
点击上方"方志朋",选择"置顶或者星标" 你的关注意义重大! 概述 在<深入聊一聊 Spring AOP 实现机制>一文中,介绍了 Spring A ...
- 4种实例 advice aop_JAVA动态代理 和 Spring AOP 4种通知的简单实现
学习Spring AOP 之前,先要了解下JAVA的动态代理.如果不清楚动态代理的概念就百度一下吧.废话不多说,直接上代码. 我们模拟一个简单的登录 首先我们创建一个用户登录的接口 package c ...
- spring aop原理_Spring知识点总结!已整理成142页离线文档(源码笔记+思维导图)...
写在前面 由于Spring家族的东西很多,一次性写完也不太现实.所以这一次先更新Spring[最核心]的知识点:AOP和IOC 无论是入门还是面试,理解AOP和IOC都是非常重要的.在面试的时候,我没 ...
- spring aop 必须的包 及里面用到的东西_Spring 原理初探——IoC、AOP
前言 众所周知, 现在的 Spring 框架已经成为构建企业级 Java 应用事实上的标准了,众多的企业项目都构建在 Spring 项目及其子项目之上,特别是 Java Web 项目. Spring ...
- Spring异步调用原理及SpringAop拦截器链原理
一.Spring异步调用底层原理 开启异步调用只需一个注解@EnableAsync @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTI ...
最新文章
- JSon数据查询---Jlinq
- 关于Sqlite的一个demo
- 我该建数仓、大数据平台还是数据中台?看完脑子终于清醒了
- 让IE6支持图片半透明
- oracle protocol=beq 不可用,学习笔记:Oracle数据库坏块 深入研究obj$坏块导致exp/expdp不能执行原因...
- dubbo学习(四)配置dubbo 注解方式配置
- 博文视点 OpenParty第11期:世界黑客大会那些事
- blob字段乱码怎么处理_下载的附件名总乱码?你该去读一下 RFC 文档了!
- Netty工作笔记0052---Pipeline组件剖析
- 想买一辆别克GL8用做家庭用车,跑长途自驾游可以吗?
- docker export import后,导入镜像,启动时的错误,Error response from daemon: No command specified...
- idea java gitignore,关于idea的gitignore文件编写及解决ignore文件不生效问题
- 百度云的云推送中的Native liberary not found 问题
- 银行业务系统数据库设计与实现
- 18.	Zigbee应用程序框架开发指南 - 应用框架V6
- UU快修-家电维修网点综合查询平台
- NodeJS解析前端请求图片链接,将服务器目录下的图片返回给前端用于页面展示
- VMWare中CentOS7增加系统盘空间
- 一段神奇的代码-关于PHP字符变量奇怪现象的解释
- CSS魔法堂:重新认识Box Model、IFC、BFC和Collapsing margins
热门文章
- python 学习笔记一
- Point和PointF
- python面试题总结(5)--数据类型(字典)
- padding和卷积的区别_TensorFlow笔记1——20.CNN卷积神经网络padding两种模式SAME和VALID...
- SPI 读取不同长度 寄存器_SPI协议,MCP2515裸机驱动详解
- TensorFlow 实例一:线性回归模型
- python 管道队列_关于python:Multiprocessing-管道与队列
- python实现哈希表
- 【VC++技术杂谈005】如何与程控仪器通过GPIB接口进行通信
- 14 Scroll 滚动搜索