Springboot源码分析之代理对象内嵌调用
摘要:
关于这个话题可能最多的是@Async和@Transactional一起混用,我先解释一下什么是代理对象内嵌调用,指的是一个代理方法调用了同类的另一个代理方法。首先在这儿我要声明事务直接的嵌套调用除外,至于为什么,是它已经将信息保存在线程级别了,是不是又点儿抽象,感觉吃力,可以看看我前面关于事务的介绍。
@Async和@Transactional共存
@Component
public class AsyncWithTransactional {
@Async
@Transactional
public void test() {
}
}
这样一段代码会发生什么?熟悉的人都会感觉疑惑,都有效果么?谁先被代理增强?
自动代理创建器AbstractAutoProxyCreator它实际也是个BeanPostProcessor,所以它们的执行顺序很重要~~~
两者都继承自
ProxyProcessorSupport所以都能创建代理,且实现了Ordered接口- - -- - ---AsyncAnnotationBeanPostProcessor默认的order值为Ordered.LOWEST_PRECEDENCE。但可以通过@EnableAsync指定order属性来改变此值。AsyncAnnotationBeanPostProcessor在创建代理时有这样一个逻辑:若已经是Advised对象了,那就只需要把@Async的增强器添加进去即可。若不是代理对象才会自己去创建
public abstract class AbstractAdvisingBeanPostProcessor extends ProxyProcessorSupport implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (bean instanceof Advised) {
advised.addAdvisor(this.advisor);
return bean;
}
// 上面没有return,这里会继续判断自己去创建代理~
}
}
AbstractAutoProxyCreator默认值也同上。但是在把自动代理创建器添加进容器的时候有这么一句代码:beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE);自动代理创建器这个处理器是最高优先级- 由上可知因为标注有
@Transactional,所以自动代理会生效,因此它会先交给AbstractAutoProxyCreator把代理对象生成好了,再交给后面的处理器执行
由于AbstractAutoProxyCreator先执行,所以AsyncAnnotationBeanPostProcessor执行的时候此时Bean已经是代理对象了,此时它会沿用这个代理,只需要把切面添加进去即可~
方法调用顺序影响
想必大家都知道一点就是同类的方法调用只有入口方法被代理才会被增强,这是由于源码级别只处理入口方法调用,是你的话你也这样设计,不然方法栈那么深,你管得了那么多吗?既然知道了这个原因,那么我们接下来在看一下后面的列子。
沿用代理对象
java.lang.IllegalStateException: Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available.
at org.springframework.aop.framework.AopContext.currentProxy(AopContext.java:69)
at com.fsx.dependency.B.funTemp(B.java:14)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:343)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:206)
at com.sun.proxy.$Proxy44.funTemp(Unknown Source)
...
这个异常在上述情况最容易出现,然而解决的方法都是@EnableAspectJAutoProxy(exposeProxy = true)
咦,是不是我们可以从容器中获取代理对象呢?没有错,从容器获取代理对象也是一种沿用代理对象来调用方法链的手段,但是你会用么?依赖于代理的具体实现而书写代码,这样移植性会非常差的。
揭秘@EnableAspectJAutoProxy(exposeProxy = true)
Spring内建的类且都是代理类的处理类:CglibAopProxy和JdkDynamicAopProxy两者很类似,在处理这个逻辑上。所以此处只以JdkDynamicAopProxy作为代表进行说明即可。
我们知道在执行代理对象的目标方法的时候,都会交给InvocationHandler处理,因此做事情的在invoke()方法里:
final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable {
...
@Override
@Nullable
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
...
if (this.advised.exposeProxy) {
// Make invocation available if necessary.
oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true;
}
...
finally {
if (setProxyContext) {
// Restore old proxy.
AopContext.setCurrentProxy(oldProxy);
}
}
}
}
最终决定是否会调用set方法是由this.advised.exposeProxy这个值决定的,因此下面我们只需要关心ProxyConfig.exposeProxy这个属性值什么时候被赋值为true的就可以了。
ProxyConfig.exposeProxy这个属性的默认值是false。其实最终调用设置值的是同名方法Advised.setExposeProxy()方法,而且是通过反射调用的,再次强调 看清楚后置处理器,@EnableAspectJAutoProxy(exposeProxy = true)作用的范围在AbstractAutoProxyCreator创建器,异步注解和缓存注解等就不行了,怎么解决后面在分析。
class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {
AspectJAutoProxyRegistrar() {
}
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);
AnnotationAttributes enableAspectJAutoProxy = AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
if (enableAspectJAutoProxy != null) {
if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
}
//处理是否设置了该属性
if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {
AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
}
}
}
}
看一下是如何设置属性值的,我们后面可以采用这样的方式来设置
public static void forceAutoProxyCreatorToExposeProxy(BeanDefinitionRegistry registry) {
if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) {
BeanDefinition definition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
definition.getPropertyValues().add("exposeProxy", Boolean.TRUE);
}
}
什么时候使用的呢?

AopContext.setCurrentProxy(@Nullable Object proxy)在CglibAopProxy和JdkDynamicAopProxy代理都有使用。
案例分析
@Component
public class AsyncWithTransactional {
//入口方法
@Transactional
public void transactional() {
//不使用代理对象调用的话,后续方法不会被增强
AsyncWithTransactional asyncWithTransactional = AsyncWithTransactional.class.cast(AopContext.currentProxy());
asyncWithTransactional.async();
}
@Async
public void async() {
}
}
这样都完全ok的,但是如果换一下呢就会跑出异常。
子线程引起的问题
@Transactional//@Transactional有此注解和没有毫无关系
@Async
public void transactional() {
AsyncWithTransactional asyncWithTransactional = AsyncWithTransactional.class.cast(AopContext.currentProxy());
asyncWithTransactional.async();
}
public void async() {
}
根本原因就是关键节点的执行时机问题。在执行代理对象transactional方法的时候,先执行绑定动作AopContext.setCurrentProxy(proxy);然后目标方法执行(包括增强器的执行)invocation.proceed()。其实在执行绑定的还是在主线程里而并非是新的异步线程,所以在你在方法体内(已经属于异步线程了)执行AopContext.currentProxy()那可不就报错了嘛~
所以入口方法用了类似@Async的效果注解都会导致代理对象绑定不对,继而导致调用错误。
如何解决类似子线程引起的问题呢?
@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(TaskManagementConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME);
beanDefinition.getPropertyValues().add("exposeProxy", true);
}
}
这样解决了@Async的绑定问题,@EnableCaching也可以基于这样的思想来解决,以上就是我的简单例子,但是配合我的文字说明,相信大家可以举一反三,随意玩弄它们之间的调用关系。
其实如果Spring做出源码改变会更好的解决这个问题
@Async的代理也交给自动代理创建器来完成(Spring做出源码改变)@EnableAsync增加exposeProxy属性,默认值给false即可(Spring做出源码改变)
总结:
不要在异步线程里使用
AopContext.currentProxy()AopContext.currentProxy()不能使用在非代理对象所在方法体内
Springboot源码分析之代理对象内嵌调用的更多相关文章
- Spring AOP 源码分析 - 创建代理对象
1.简介 在上一篇文章中,我分析了 Spring 是如何为目标 bean 筛选合适的通知器的.现在通知器选好了,接下来就要通过代理的方式将通知器(Advisor)所持有的通知(Advice)织入到 b ...
- Springboot源码分析之代理三板斧
摘要: 在Spring的版本变迁过程中,注解发生了很多的变化,然而代理的设计也发生了微妙的变化,从Spring1.x的ProxyFactoryBean的硬编码岛Spring2.x的Aspectj注解, ...
- SpringBoot源码分析之SpringBoot的启动过程
SpringBoot源码分析之SpringBoot的启动过程 发表于 2017-04-30 | 分类于 springboot | 0 Comments | 阅读次数 SpringB ...
- MyBatis源码分析(5)——内置DataSource实现
@(MyBatis)[DataSource] MyBatis源码分析(5)--内置DataSource实现 MyBatis内置了两个DataSource的实现:UnpooledDataSource,该 ...
- Flask框架 (四)—— 请求上下文源码分析、g对象、第三方插件(flask_session、flask_script、wtforms)、信号
Flask框架 (四)—— 请求上下文源码分析.g对象.第三方插件(flask_session.flask_script.wtforms).信号 目录 请求上下文源码分析.g对象.第三方插件(flas ...
- Net6 DI源码分析Part5 在Kestrel内Di Scope生命周期是如何根据请求走的?
Net6 DI源码分析Part5 在Kestrel内Di Scope生命周期是如何根据请求走的? 在asp.net core中的DI生命周期有一个Scoped是根据请求走的,也就是说在处理一次请求时, ...
- Springboot源码分析之项目结构
Springboot源码分析之项目结构 摘要: 无论是从IDEA还是其他的SDS开发工具亦或是https://start.spring.io/ 进行解压,我们都会得到同样的一个pom.xml文件 4. ...
- JVM源码分析之Java对象头实现
原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 “365篇原创计划”第十一篇. 今天呢!灯塔君跟大家讲: JVM源码分析之Java对象头实现 HotSpot虚拟机中,对象在内存中的布局分为三 ...
- 从SpringBoot源码分析 配置文件的加载原理和优先级
本文从SpringBoot源码分析 配置文件的加载原理和配置文件的优先级 跟入源码之前,先提一个问题: SpringBoot 既可以加载指定目录下的配置文件获取配置项,也可以通过启动参数( ...
随机推荐
- 11、增强型for循环对二维数组的输出(test8.java)
由于笔者原因,这部分知识,尚不能整理出代码,笔者会好好学习增强型for循环中迭代起的相关知识,在笔者有能力,书写好这段代码后,将对本篇文章,进行二次修改,也同时欢迎大家与笔者交流,共同学习,共同进步. ...
- SVN服务器更改ip地址客户端怎么设置
SVN 服务器 IP 地址修改后,客户端对服务器的连接可以采用以下的方法重定位: 1. 如果客户端工具是TortoiseSVN,直接在工作副本上右键,选择TortoiseSVN->relocat ...
- MQ服务器端和客户端通信浅谈
MQ服务器端和客户端通信浅谈 1. WebSphere MQ的服务端的安装和配置 (1)创建名为venus.queue.manager的默认队列管理器. 在DOS窗口命令提示符下,输入以下命令: cr ...
- [Spring cloud 一步步实现广告系统] 17. 根据流量类型查询广告
广告检索服务 功能介绍 媒体方(手机APP打开的展示广告,走在路上看到的大屏幕广告等等) 请求数据对象实现 从上图我们可以看出,在媒体方向我们的广告检索系统发起请求的时候,请求中会有很多的请求参数信息 ...
- IntelliJ IDEA 激活(最新)
注:此文以 Mac 为例,Windows 的激活方法也大同小异.如果不差钱的话,建议购买正版! 1.下载安装 直接通过下面的链接到官网下载最新的 Ultimate 版本即可: https://www. ...
- 【模板】质数判断(Miller_Rabin)
题意简述 给定一个范围N,你需要处理M个某数字是否为质数的询问(每个数字均在范围1-N内) 题解思路 费马小定理: n是一个奇素数,a是任何整数(\(1≤ a≤n-1\)) ,则\(a^{p-1}≡1 ...
- thinkPhP 引入Smarty模板引擎及配置
做配置: TMPL_ENGINE_TYPE = “Smarty” 给smarty做配置: TMPL_ENGINE_CONFIG = array( 左标记, 右标记, )
- laya2d 与 cad 之间的坐标转换
坐标系基本概念 直角坐标系可分为左手坐标系与右手坐标系,cad 中用到的是右手坐标系, Laya2D 中用到的是左手坐标系, Laya3D 中使用右手坐标系. 那么如何判断二维直角坐标系是左手还是右手 ...
- 域名、主机名、网站名以及 URL 基础概念
这个东西,在百度经验上已经有人讲得非常清楚了, 作为 web 方向的小白, 我写下我的理解,以便日后查看. 以 https:// www.3vjia.com 为例, 见下图: DNS (Domain ...
- 使用flash2print 代替 printflash 将office文档 转为flash 在页面中播放
前一些日子公司需求把用户上传的一些word等 文档 能像百度文库那样 显示给用户, 但是如果是直接显示office文档的话就需要 些控件的支持 .非常的不友好,所以 一开始我就想能不能转成pdf 来 ...