從@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:如果覺得本文對你有幫助,不妨點個贊唄。當然分享到你的朋友圈讓更多小伙伴看到也是被作者本人許可的~

本文參與騰訊雲自媒體分享計划,歡迎正在閱讀的你也加入,一起分享。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM