每篇一句
技術的發展總會實在掌聲中,伴隨着噓聲中前進。因此,需要有一顆擁抱變革的心態~
前言
本文標題包含有'靚麗'
的字眼:Spring框架bug
。相信有的小夥伴心裏小九九就會說了:又是一篇標題黨文章。
鑑於此,此處可以很負責任的對大夥說:本人所有文章絕不譁衆取寵
,除了乾貨只剩乾貨。
相信關注過我的小夥伴都是知道的,我只遞送乾貨,絕不標題黨來浪費大家的時間和精力~那無異於
謀財害命
(說得嚴重了,不喜勿噴)
關於標題黨的好與壞、優與劣,此處我不置可否
本篇文章能讓你知道exposeProxy=true
真實作用和實際作用範圍,從而能夠在開發中更精準的使用到它。
背景
本來一切本都那麼平靜,直到我用了@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 {
}
示例一
@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()
方法,而且是通過反射調用的
@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()
不能使用在非代理對象所在方法體內