zoukankan      html  css  js  c++  java
  • 从@Async案例找到Spring框架的bug:exposeProxy=true不生效原因大剖析+最佳解决方案【享学Spring】

    https://cloud.tencent.com/developer/article/1497700

    前言

    本文标题包含有'靓丽'的字眼:Spring框架bug。相信有的小伙伴心里小九九就会说了:又是一篇标题党文章。 鉴于此,此处可以很负责任的对大伙说:本人所有文章绝不哗众取宠,除了干货只剩干货。

    相信关注过我的小伙伴都是知道的,我只递送干货,绝不标题党来浪费大家的时间和精力~那无异于谋财害命(说得严重了,不喜勿喷) 关于标题党的好与坏、优与劣,此处我不置可否

    本篇文章能让你知道exposeProxy=true真实作用和实际作用范围,从而能够在开发中更精准的使用到它。

    背景

    这篇文章可定位为是基于上篇文章的续文: 【小家Spring】使用@Async异步注解导致该Bean在循环依赖时启动报BeanCurrentlyInCreationException异常的根本原因分析,以及提供解决方案

    本来一切本都那么平静,直到我用了@Async注解,好多问题都接踵而至(上篇文章已经解决大部分了)。在上篇文章中,为了解决@Async同类方法调用问题我提出了两个方向的解决方案:

    1. 自己注入自己,然后再调用接口方法(当然此处的一个变种是使用编程方式形如:AInterface a = applicationContext.getBean(AInterface.class);这样子手动获取也是可行的~~~本文不讨论这种比较直接简单的方式)
    2. 使用AopContext.currentProxy();方式

    方案一上篇文章已花笔墨重点分析,毕竟方案一我认为更为重要些。本文分析使用方案二的方式,它涉及到AOP、代理对象的暴露,因此我认为本文的内容对你平时开发的影响是不容小觑,可以重点浏览咯~

    我相信绝大多数小伙伴都遇到过这个异常:

     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)

    本文我想说的可能又是一个技术敏感性问题,其实绝大多数情况下你按照这么做是可行的,直到你遇到了@Async也需要调用本类方法的时候,你就有点绝望了,然后本文或许会成为了你的救星~

    本以为加了exposeProxy = true就能顺风顺水了,但它却出问题了:依旧报如上的异常信息。如果你看到这里也觉得不可思议,那么本文就更能体现它的价值所在~

    此问题我个人把它归类为Spring的bug我觉得是无可厚非的,因为它的语义与实际表现出来的结果想悖了,so我把定义为Spring框架的bug。 对使用者来说,标注了exposeProxy = true,理论上就应该能够通过AopContext.currentProxy()拿到代理对象,可惜Spring这里却掉链子了,有点名不副实之感~

    示例

    本文将以多个示例来模拟不同的使用case,首先从直观的结果上先了解@EnableAspectJAutoProxy(exposeProxy = true)的作用以及它存在的问题。

    备注:下面所有示例都建立在@EnableAspectJAutoProxy(exposeProxy = true)已经开启的前提下,形如:

    @Configuration
    @EnableAspectJAutoProxy(exposeProxy = true) // 暴露当前代理对象到当前线程绑定
    public class RootConfig {
    }

    示例一

    此示例大都用于解决事务不生效问题上(同类方法调用引起的事务不生效,关于Spring事务不生效的case,可以参考:【小家java】Spring事务不生效的原因大解读 )。

    @Service
    public class B implements BInterface {
    
        @Transactional
        @Override
        public void funTemp() {
            ...
    
            // 希望调用本类方法  但是它抛出异常,希望也能够回滚事务
            BInterface b = BInterface.class.cast(AopContext.currentProxy());
            System.out.println(b);
            b.funB();
        }
    
        @Override
        public void funB() {
            // ... 处理业务属于  
            System.out.println(1 / 0);
        }
    }

    结论:能正常work,事务也会生效~

    示例二

    同类内方法调用,希望异步执行被调用的方法(希望@Async生效)

    @Service
    public class B implements BInterface {
    
        @Override
        public void funTemp() {
            System.out.println("线程名称:" + Thread.currentThread().getName());
    
            // 希望调用本类方法  但是希望它去异步执行~
            BInterface b = BInterface.class.cast(AopContext.currentProxy());
            System.out.println(b);
            b.funB();
        }
    
        @Async
        @Override
        public void funB() {
            System.out.println("线程名称:" + Thread.currentThread().getName());
        }
    }

    结论:执行即报错

    java.lang.IllegalStateException: Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available.

    示例三

    同类内方法调用,希望异步执行被调用的方法,并且在入口方法处使用事务

    @Service
    public class B implements BInterface {
    
        @Transactional
        @Override
        public void funTemp() {
            System.out.println("线程名称:" + Thread.currentThread().getName());
    
            // 希望调用本类方法  但是希望它去异步执行~
            BInterface b = BInterface.class.cast(AopContext.currentProxy());
            System.out.println(b);
            b.funB();
        }
    
        @Async
        @Override
        public void funB() {
            System.out.println("线程名称:" + Thread.currentThread().getName());
        }
    }

    结论:正常work没有报错,@Async异步生效、事务也生效

    示例四

    示例三的唯一区别是把事务注解@Transactional标注在被调用的方法处(和@Async同方法):

    @Service
    public class B implements BInterface {
    
    
        @Override
        public void funTemp() {
            System.out.println("线程名称:" + Thread.currentThread().getName());
    
            // 希望调用本类方法  但是希望它去异步执行~
            BInterface b = BInterface.class.cast(AopContext.currentProxy());
            System.out.println(b);
            b.funB();
        }
    
        @Transactional
        @Async
        @Override
        public void funB() {
            System.out.println("线程名称:" + Thread.currentThread().getName());
        }
    }

    结论:同示例三

    示例五

    @Async标注在入口方法上:

    @Service
    public class B implements BInterface {
    
    
        @Transactional
        @Async
        @Override
        public void funTemp() {
            System.out.println("线程名称:" + Thread.currentThread().getName());
    
            BInterface b = BInterface.class.cast(AopContext.currentProxy());
            System.out.println(b);
            b.funB();
        }
    
        @Override
        public void funB() {
            System.out.println("线程名称:" + Thread.currentThread().getName());
        }
    }

    结论:请求即报错

    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)

    示例六

    偷懒做法:直接在实现类里写个方法(public/private)然后注解上@Async

    我发现我司同事有大量这样的写法,所以专门拿出作为示例,以儆效尤~

    @Service
    public class B implements BInterface {
    	...
        @Async
        public void fun2(){
            System.out.println("线程名称:" + Thread.currentThread().getName());
        }
    }

    结论:因为方法不在接口上,因此肯定无法通过获取代理对象调用它

    需要注意的是:即使该方法不属于接口方法,但是标注了@Async所以最终生成的还是B的代理对象~(哪怕是private访问权限也是代理对象)

    可能有的小伙伴会想通过context.getBean()获取到具体实现类再调用方法行不行。咋一想可行,实际则不是不行的。 这里再次强调一次,若你是AOP是JDK的动态代理的实现,这样100%报错的:

    BInterface bInterface = applicationContext.getBean(BInterface.class); // 正常获取到容器里的代理对象
    
    applicationContext.getBean(B.class); //报错  NoSuchBeanDefinitionException
    // 原因此处不再解释了,若是CGLIB代理,两种获取方式均可~

    备注:虽说CGLIB代理方式用实现类方式可以获取到代理的Bean,但是强烈不建议依赖于代理的具体实现而书写代码,这样移植性会非常差的,而且接手的人肯定也会一脸懵逼、二脸懵逼…

    因此当你看到你同事就在本类写个方法标注上@Async然后调用,请制止他吧,做的无用功~~~(关键自己还以为有用,这是最可怕的深坑~

    原因大剖析

    找错的常用方法:逆推法。 首先我们找到报错的最直接原因:AopContext.currentProxy()这句代码报错的,因此有必要看看AopContext这个工具类

    // @since 13.03.2003
    public final class AopContext {
    	private static final ThreadLocal<Object> currentProxy = new NamedThreadLocal<>("Current AOP proxy");
    	private AopContext() {
    	}
    
    	// 该方法是public static方法,说明可以被任意类进行调用
    	public static Object currentProxy() throws IllegalStateException {
    		Object proxy = currentProxy.get();
    
    		// 它抛出异常的原因是当前线程并没有绑定对象
    		// 而给线程版定对象的方法在下面:特别有意思的是它的访问权限是default级别,也就是说只能Spring内部去调用~
    		if (proxy == null) {
    			throw new IllegalStateException("Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available.");
    		}
    		return proxy;
    	}
    
    	// 它最有意思的地方是它的访问权限是default的,表示只能给Spring内部去调用~
    	// 调用它的类有CglibAopProxy和JdkDynamicAopProxy
    	@Nullable
    	static Object setCurrentProxy(@Nullable Object proxy) {
    		Object old = currentProxy.get();
    		if (proxy != null) {
    			currentProxy.set(proxy);
    		} else {
    			currentProxy.remove();
    		}
    		return old;
    	}
    
    }

    从此工具源码可知,决定是否抛出所示异常的直接原因就是请求的时候setCurrentProxy()方法是否被调用过。通过寻找发现只有两个类会调用此方法,并且都是Spring内建的类且都是代理类的处理类CglibAopProxyJdkDynamicAopProxy

    说明:本文所有示例,都基于接口的代理,所以此处只以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);
    			}
    		}
    	}
    }

    so,最终决定是否会调用set方法是由this.advised.exposeProxy这个值决定的,因此下面我们只需要关心ProxyConfig.exposeProxy这个属性值什么时候被赋值为true的就可以了。

    ProxyConfig.exposeProxy这个属性的默认值是false。其实最终调用设置值的是同名方法Advised.setExposeProxy()方法,而且是通过反射调用的

    关于Spring AOP以及自动代理创建器的详细,本文将不会作为重点讲解,有需要充电的可以参考: 【小家Spring】面向切面编程Spring AOP创建代理的方式:ProxyFactoryBean、ProxyFactory、AspectJProxyFactory(JDK Proxy和CGLIB) 【小家Spring】Spring AOP的核心类:AbstractAdvisorAutoProxy自动代理创建器深度剖析(AnnotationAwareAspectJAutoProxyCreator)

    @EnableAspectJAutoProxy(exposeProxy = true)的作用

    此注解它导入了AspectJAutoProxyRegistrar,最终设置此注解的两个属性的方法为:

    public abstract class AopConfigUtils {
    	public static void forceAutoProxyCreatorToUseClassProxying(BeanDefinitionRegistry registry) {
    		if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) {
    			BeanDefinition definition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
    			definition.getPropertyValues().add("proxyTargetClass", Boolean.TRUE);
    		}
    	}
    	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);
    		}
    	}
    }

    看到此注解标注的属性值最终都被设置到了internalAutoProxyCreator身上,也就是进而重要的一道菜:自动代理创建器。

    在此各位小伙伴需要先明晰的是:@Async的代理对象并不是由自动代理创建器来创建的,而是由AsyncAnnotationBeanPostProcessor一个单纯的BeanPostProcessor实现的。

    示例结论分析

    本章节在掌握了一定的理论的基础上,针对上面的各种示例进行结论性分析

    示例一分析

    本示例目的是事务,可以参考开启事务的注解@EnableTransactionManagement。该注解向容器注入的是自动代理创建器InfrastructureAdvisorAutoProxyCreator,所以exposeProxy = true对它的代理对象都是生效的,因此可以正常work~

    备注:@EnableCaching注入的也是自动代理创建器~so exposeProxy = true对它也是有效的

    示例二分析

    很显然本例是执行AopContext.currentProxy()这句代码的时候报错了。报错的原因相信我此处不说,小伙伴应该个大概了。

    @EnableAsync给容器注入的是AsyncAnnotationBeanPostProcessor,它用于给@Async生成代理,但是它仅仅是个BeanPostProcessor并不属于自动代理创建器,因此exposeProxy = true对它无效。 所以AopContext.setCurrentProxy(proxy);这个set方法肯定就不会执行,so但凡只要业务方法中调用AopContext.currentProxy()方法就铁定抛异常~~

    示例三分析

    这个示例的结论,相信是很多小伙伴都没有想到的。仅仅只是加入了事务,@Asycn竟然就能够完美的使用AopContext.currentProxy()获取当前代理对象了。

    为了便于理解,我分步骤讲述如下,不出意外你肯定就懂了:

    1. 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,这里会继续判断自己去创建代理~
    	}
    }
    1. 自动代理创建器AbstractAutoProxyCreator它实际也是个BeanPostProcessor,所以它和上面处理器的执行顺序很重要~~~
    2. 两者都继承自ProxyProcessorSupport所以都能创建代理,且实现了Ordered接口 1. AsyncAnnotationBeanPostProcessor默认的order值为Ordered.LOWEST_PRECEDENCE。但可以通过@EnableAsync指定order属性来改变此值。 执行代码语句:bpp.setOrder(this.enableAsync.<Integer>getNumber("order")); 2. AbstractAutoProxyCreator默认值也同上。但是在把自动代理创建器添加进容器的时候有这么一句代码:beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE); 自动代理创建器这个处理器是最高优先级
    3. 由上可知因为标注有@Transactional,所以自动代理会生效,因此它会先交给AbstractAutoProxyCreator把代理对象生成好了,再交给后面的处理器执行
    4. 由于AbstractAutoProxyCreator先执行,所以AsyncAnnotationBeanPostProcessor执行的时候此时Bean已经是代理对象了,由步骤1可知,此时它会沿用这个代理,只需要把切面添加进去即可~

    从上面步骤可知,加上了事务注解,最终代理对象是由自动代理创建器创建的,因此exposeProxy = true对它有效,这是解释它能正常work的最为根本的原因。

    示例四分析

    同上。

    @Transactional只为了创建代理对象而已,所在放在哪儿对@Async的作用都不会有本质的区别

    示例五分析

    此示例非常非常有意思,因此我特意拿出来讲解一下。

    咋一看其实以为是没有问题的,毕竟正常我们会这么思考:执行funTemp()方法会启动异步线程执行,同时它会把Proxy绑定在当前线程中,所以即使是新起的异步线程也有能够使用AopContext.currentProxy()才对。

    但有意思的地方就在此处:它报错了,正所谓你以为的不一定就是你以为的。 解释:根本原因就是关键节点的执行时机问题。在执行代理对象funTemp方法的时候,绑定动作oldProxy = AopContext.setCurrentProxy(proxy);在前,目标方法执行(包括增强器的执行)invocation.proceed()在后。so其实在执行绑定的还是在主线程里而并非是新的异步线程,所以在你在方法体内(已经属于异步线程了)执行AopContext.currentProxy()那可不就报错了嘛~

    示例六分析

    略。(上已分析)

    解决方案

    对上面现象原因可以做一句话的总结:@Async要想顺利使用AopContext.currentProxy()获取当前代理对象来调用本类方法,需要确保你本Bean已经被自动代理创建器提前代理

    在实际业务开发中:只要的类标注有@Transactional或者@Caching等注解,就可以放心大胆的使用吧

    知晓了原因,解决方案从来都是信手拈来的事。 不过如果按照如上所说需要隐式依赖这种方案我非常的不看好,总感觉不踏实,也总感觉报错迟早要来。(比如某个同学该方法不要事务了/不要缓存了,把对应注解摘掉就瞬间报错了,到时候你可能哭都没地哭诉去~)

    备注:墨菲定律在开发过程中从来都没有不好使过~~~程序员兄弟姐妹们应该深有感触吧

    下面根据我个人经验,介绍一种解决方案中的最佳实践:

    遵循的最基本的原则是:显示的指定比隐式的依赖来得更加的靠谱、稳定

    @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);
        }
    }

    这样我们可以在@AsyncAopContext.currentProxy()就自如使用了,不再对别的啥的有依赖性~

    其实我认为最佳的解决方案是如下两个(都需要Spring框架做出修改): 1、@Async的代理也交给自动代理创建器来完成 2、@EnableAsync增加exposeProxy属性,默认值给false即可(此种方案的原理同我示例的最佳实践~

    总结

    通过6组不同的示例,演示了不同场景使用@Async,并且对结论进行解释,不出意外,小伙伴们读完之后都能够掌握它的来龙去脉了吧。

    最后再总结两点,小伙伴们使用的时候稍微注意下就行:

    1. 请不要在异步线程里使用AopContext.currentProxy()
    2. AopContext.currentProxy()不能使用在非代理对象所在方法体内

    The last:如果觉得本文对你有帮助,不妨点个赞呗。当然分享到你的朋友圈让更多小伙伴看到也是被作者本人许可的~

    本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

  • 相关阅读:
    Linux内核通杀提权漏洞CVE-2016-5195
    Android APP安全评估工具 Drozer
    (转)手机安全测试
    微信公众号获取的图片不能正常显示的问题
    android开发时gen和bin目录的SVN管理(转)
    处理千万级以上的数据提高查询速度的方法(转)
    Java遍历JSON
    Java反射得到属性的值和设置属性的值(转)
    java如何得到GET和POST请求URL和参数列表(转)
    Oracle Database 11G R2 标准版 企业版 下载地址(转)
  • 原文地址:https://www.cnblogs.com/xiang--liu/p/12609749.html
Copyright © 2011-2022 走看看