https://cloud.tencent.com/developer/article/1497700
前言
本文標題包含有'靚麗'
的字眼:Spring框架bug
。相信有的小伙伴心里小九九就會說了:又是一篇標題黨文章。 鑒於此,此處可以很負責任的對大伙說:本人所有文章絕不嘩眾取寵
,除了干貨只剩干貨。
相信關注過我的小伙伴都是知道的,我只遞送干貨,絕不標題黨來浪費大家的時間和精力~那無異於
謀財害命
(說得嚴重了,不喜勿噴) 關於標題黨的好與壞、優與劣,此處我不置可否
本篇文章能讓你知道exposeProxy=true
真實作用和實際作用范圍,從而能夠在開發中更精准的使用到它。
背景
這篇文章可定位為是基於上篇文章的續文: 【小家Spring】使用@Async異步注解導致該Bean在循環依賴時啟動報BeanCurrentlyInCreationException異常的根本原因分析,以及提供解決方案
本來一切本都那么平靜,直到我用了@Async
注解,好多問題都接踵而至(上篇文章已經解決大部分了)。在上篇文章中,為了解決@Async
同類方法調用問題我提出了兩個方向的解決方案:
- 自己注入自己,然后再調用接口方法(當然此處的一個變種是使用編程方式形如:AInterface a = applicationContext.getBean(AInterface.class);這樣子手動獲取也是可行的~~~本文不討論這種比較直接簡單的方式)
- 使用
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內建的類且都是代理類的處理類: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); } } } }
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
注入的也是自動代理創建器~soexposeProxy = true
對它也是有效的
示例二分析
很顯然本例是執行AopContext.currentProxy()
這句代碼的時候報錯了。報錯的原因相信我此處不說,小伙伴應該個大概了。
@EnableAsync
給容器注入的是AsyncAnnotationBeanPostProcessor
,它用於給@Async
生成代理,但是它僅僅是個BeanPostProcessor
並不屬於自動代理創建器,因此exposeProxy = true
對它無效。 所以AopContext.setCurrentProxy(proxy);
這個set方法肯定就不會執行,so但凡只要業務方法中調用AopContext.currentProxy()
方法就鐵定拋異常~~
示例三分析
這個示例的結論,相信是很多小伙伴都沒有想到的。僅僅只是加入了事務,@Asycn
竟然就能夠完美的使用AopContext.currentProxy()
獲取當前代理對象了。
為了便於理解,我分步驟講述如下,不出意外你肯定就懂了:
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
它實際也是個BeanPostProcessor
,所以它和上面處理器的執行順序很重要~~~ - 兩者都繼承自
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);
自動代理創建器這個處理器是最高優先級
- 由上可知因為標注有
@Transactional
,所以自動代理會生效,因此它會先交給AbstractAutoProxyCreator
把代理對象生成好了,再交給后面的處理器執行 - 由於
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); } }
這樣我們可以在@Async
和AopContext.currentProxy()
就自如使用了,不再對別的啥的有依賴性~
其實我認為最佳的解決方案是如下兩個(都需要Spring框架做出修改): 1、
@Async
的代理也交給自動代理創建器來完成 2、@EnableAsync
增加exposeProxy
屬性,默認值給false即可(此種方案的原理同我示例的最佳實踐~)
總結
通過6組不同的示例,演示了不同場景使用@Async
,並且對結論進行解釋,不出意外,小伙伴們讀完之后都能夠掌握它的來龍去脈了吧。
最后再總結兩點,小伙伴們使用的時候稍微注意下就行:
- 請不要在異步線程里使用
AopContext.currentProxy()
AopContext.currentProxy()
不能使用在非代理對象所在方法體內
The last:如果覺得本文對你有幫助,不妨點個贊唄。當然分享到你的朋友圈讓更多小伙伴看到也是被作者本人許可的~
本文參與騰訊雲自媒體分享計划,歡迎正在閱讀的你也加入,一起分享。