問題:
最近因為業務需要,在項目中獲取用戶信息時需要做特殊處理,於是本人想到了使用AOP的方式來實現。但是在實現的過程中發現@Before注解對UserDao失效,對於其它Bean則能正常使用(before1不生效,before2生效)。注解如下:
集成shiro的代碼:
shiroRealmManager依賴了userDao:
解決過程:
首先,既然對其它bean做aop有效,那么配置就不存在問題。
其次,面向切面編程是使用動態代理來實現的,因此在調用userDao時使用的應該是代理對象,但是通過調試發現事實並非如此。
因此將問題鎖定在了UserDao的初始化上。
補充一點:本人使用@EnableAspectJAutoProxy注解的方式啟用@AspectJ支持,而@EnableAspectJAutoProxy注解會注入AspectJAutoProxyRegistrar,AspecttJAutoProxyRegistrar實現了ImportBeanDefinitionRegistrar,
查看ImportBeanDefinitionRegistrar的注釋:Interface to be implemented by types that register additional bean definitions(注冊額外的bean定義使用),了解到該接口用於需要注冊額外bean 定義的實現類使用,AspecttJAutoProxyRegistrar實現如下:
跟蹤到底,這里注冊了一個名為org.springframework.aop.config.internalAutoProxyCreator的bean定義,bean定義的類為AnnotationAwareAspectJAutoProxyCreator,該類實現了BeanPostProcessor
:
補充結束。既然是bean的生成過程出了問題,那么就去創建bean的源碼里面找。通過斷點調試發現在創建創建bean之后,AbstractAutowireCapableBeanFactory中會有這么一段代碼:
其它能夠面向切面編程的bean生成代理對象的地方就在這里,而UserDao在這里並沒有生成代理對象。那么問題就出在這里(曾經粗略看過spring源碼,知道bean的大致生成過程,不然能不能調試到這里還是個問題。。心累)!並且日志輸出
Bean 'userDaoImpl' of type ... is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
重點就在於initializeBean這個方法,見名思義,這個方法做的事情大致就是初始化創建好的Bean,進入該方法,里面有兩個重要的過程applyBeanPostProcessorsBeforeInitialization(bean前置處理過程)和applyBeanPostProcessorsAfterInitialization(bean后置處理過程),在AOP中,這兩個方法就是將生成的Bean包裝為代理對象的方法:
進入上面兩個方法任一個發現正常情況下(能夠生成代理)beanPostProcessors有14個,而處理UserDao時只有9個。
這里就涉及到context初始化的過程了,在應用程序啟動時,AbstractApplicationContext的refresh方法會被調用(本項目依賴於Servlet3.0的SPI機制啟動:),這里重點在於解決aop失效問題,就不深入應用程序啟動過程了,以后有空再寫),注意,在這個過程中會向BeanFactory注冊BeanPostProcessor。代碼如下:
這里的注冊過程分為多步,如下:
首先,從容器中獲取BeanPostProcessor類型的bean名稱:
注冊BeanPostProcessorChecker,上面UserDao后處理時打印的...not eligible for auto-proxying就是通過BeanPostProcessorChecker打印的。注釋也說明了它的功能。
然后,將實現了PriorityOrdered、實現了Ordered和其它的PostBeanProcessor或它的名稱分別放入三個容器中
接下來注冊三個容器中的BeanPostProcessor(BPP):
先注冊實現了PriorityOrdered的BPP,注意,真正注冊BPP的方法是registerBeanPostProcessors。
再注冊實現了Ordered的BPP
最后注冊其它BPP
以上的BPP都在BeanFactory中注冊,在bean初始化過程中被initializeBean方法調用。而通過斷點調試發現在注冊實現Ordered的BPP 前BeanFactory中注冊的BPP剛好為9個(和初始化UserDao時BeanFactory中BPP的個數一樣),而注冊之后就變為13個了。
實現Ordered的BPP注冊前:
實現Ordered的注冊后:
並且,上面補充的使@AspectJ生效的BPP(internalAutoProxyCreator)就實現了Ordered接口,同時也是在這一步注冊的。
那么問題必然出現在這兩個時間點之間。通過斷點調試的方式證明確實如此,UserDao實例化的時間點就在這之間,而且是在創建一個名為org.springframework.context.annotation.internalAsyncAnnotationProcessor的bean時被依賴到的。
在BeanFactory中,org.springframework.context.annotation.internalAsyncAnnotationProcessor對應的mbd(bean定義)中使用的時ProxyAsyncConfiguration,在該類的父類AbstractAsyncConfiguration中依賴了AsyncConfigurer,如圖:
在創建ProxyAsyncConfiguration時就會根據類型去獲取AsyncConfigurer,關鍵點就在這里:
以上代碼根據類型在BeanFactory中獲取對應的Bean,這里遍歷了beanFactory中所有注冊的bean名稱(上圖for 循環),並調用了isTypeMatch方法,進入isTypeMatch方法中查看,有這么一段代碼
這里如果遍歷的bean是FactoryBean類型就查看該factoryBean創建的是什么類型,並調用getTypeForFactoryBean方法校驗(集成shiro時正好注冊了一個FactoryBean類型的ShiroFilterFactoryBean),該方法執行了getSingletonFactoryBeanForTypeCheck,在getSingletonFactoryBeanForTypeCheck中,會先嘗試能否通過該factoryBean獲取生成的對象類型,如果找不到且創建該FactoryBean的bean工廠(我這里是帶有@Configuration的配置類mvcconfig)沒有初始化就執行創建factoryBean對象的方法
此處就創建了名為shiroFilter的ShiroFilterFactoryBean對象。好了,到這里已經有足夠的證據找出真凶,就不往下走代碼了。
讓我們理一理目前得到的信息:
1.bean初始化會執行BeanFactory中注冊的BPP;
2.BPP會分為3個階段注冊:注冊實現了PriorityOrdered的BPP、注冊實現Ordered的BPP和注冊其它BPP,第一個階段注冊的BPP能在第二階段使用,第二階段注冊的BPP能在第三階段使用,第三階段注冊的BPP能給還未生成的 Bean對象使用;
3.注冊BPP時會創建BPP對象以及相關依賴,在根據類型注入依賴時會判斷注冊的bean是否實現了FactoryBean,如果實現了並且無法通過現有條件找到該FactoryBean返回的對象類型,就會創建該bean。
綜上所訴,在注冊實現Ordered的BPP時會創建名稱為org.springframework.context.annotation.internalAsyncAnnotationProcessor(ProxyAsyncConfiguration)的BPP,而創建ProxyAsyncConfiguration時依賴AsyncConfigurar,就通過類型在BeanFactory中查找實現了AsyncConfigurar接口的類型,在驗證到shiroFilter時,shiroFilter為FactoryBean且通過bean定義無法識別該FactoryBean對象返回的類型,因此創建了該對象,而shiroFilter間接依賴了本項目中實現了AuthorizingRealm的ShiroRealmManager,而ShiroRealmManager又依賴了UserDao,因此,UserDao在這個時間被提前初始化了。導致實現了Ordered和其它類型的BPP無法處理UserDao,最終使得AOP功能失敗。
解決:
既然問題已經發現了,那么解決起來就好辦了,通過反向查找,ProxyAsyncConfiguration是通過@EnableAsync注入的,因此去掉該注解就不會提前去加載UserDao了(但是這種方法治標不治本,有可能其它實現了Ordered的BPP會通過類型在BeanFactory中查找),或者在ShiroRealmManager中使用@Autowired依賴UserDao時加上@Lazy注解(推薦),lazy注解在autowired時做判斷,該判斷在DefaultListableBeanFactory的resolveDependency方法中:
總結:
1.shiro和spring不太兼容,還是springsecurity好用;2.開發的過程中要注意啟動日志里面的warn信息,比如這次打印的Bean 'userDaoImpl' of type ... is not eligible for getting processed by all BeanPostProcessors就被我忽略了(不然解決這個問題也不會這么麻煩);3.多看看Spring文檔,Spring文檔中就有相關警示(雖然不是此次問題所在):
大意是SpringAop就是通過BeanPostProcessor實現的,BeanPostProcessor和其間接引用的bean都不適用於自動代理,因此,別在這些bean中使用切面。輕噴。。