Spring結合AspectJ的研究


本文闡述以下內容:
1、AspectJ是什么及使用方式
2、Spring AOP和AspectJ的區別
3、Spring結合AspectJ的使用方法和原理
4、Spring注解方式使用AspectJ遇到的問題
5、總結

一、AspectJ是什么

提到面向切面編程(AOP,Aspect Oriented Programming),大家首先想到的是Spring AOP,或許有人也會想到AspectJ,也有人搞不清楚這兩者的區別。
AOP是一種編程思想,AOP的作用是在不修改源程序的情況下修改源程序的動態執行流和靜態屬性。AspectJ是一種基於Java平台的面向切面編程的語言,兼容Java平台,可以無縫擴展,易學易用。干凈的模塊化橫切關注點(也就是說單純,基本上無侵入),如錯誤檢查和處理,同步,上下文敏感的行為,性能優化,監控和記錄,調試支持,多目標的協議。
AspectJ有單獨的語法,AspectJ項目的源文件經AspectJ Compiler(ajc)編譯后生成完全兼容Java語法的Class文件,可以看出AspectJ可以在編譯期將Advice功能織入連接點。AspectJ還可以在類加載期切面織入(Load Time Weaver,LTW),此時就需要在jvm啟動參數配置 -javaagent:[path to aspectj-weaver.jar],這樣在類加載時,AspectJ就可以將Advice功能織入連接點。比起編譯期織入更方便,不需要用專用的ajc編譯器編譯,利用java.lang.Instrument包提供的工具在Java程序運行時動態修改系統中的Class類型。

二、Spring AOP和AspectJ的區別

Spring AOP是常用的AOP實現方式,使用方便,無需做項目工程之外的配置和操作,純代碼實現,深受廣大開發人員的喜愛。Spring AOP是一個基於代理的AOP框架。運行時通過創建目標對象的代理類,對目標對象進行增強,主要使用JDK動態代理或CGLIB代理的方式。而AspectJ在運行時不做任何事情,在編譯階段或類加載階段,Advice就織入切面到代碼中了。這是和Spring AOP的本質區別。由Spring AOP使用的動態代理模式可知,通過實現目標的父接口或生成目標的子類來實現增強,因此只能對Spring管理范圍內的bean的方法執行進行連接。AspectJ就要靈活很多,不僅能對方法執行進行連接,此外還支持方法調用、構造器調用、構造器執行、對象初始化、字段引用、字段賦值、類靜態初始化、異常處理執行這些連接點。
此外,在Spring AOP中,切面不適用於同一個類中調用的方法。當我們在同一個類中調用一個方法時,我們並沒有調用Spring AOP提供的代理的方法。解決這個問題,可以在不同的beans中定義一個獨立的方法,或者獲取到本身的代理對象,或者使用AspectJ。
性能方便,運行前織入比運行時織入快很多。Spring AOP是基於代理的框架,因此應用運行時會有目標類的代理對象生成。另外,每個切面還有一些方法調用,這會對性能造成影響。AspectJ不同於Spring AOP,是在應用執行前織入切面到代碼中,沒有額外的運行時開銷。
簡而言之,選擇很大程度上取決我們的需求:
●框架:如果應用程序不使用Spring框架,那么我們別無選擇,只能放棄使用Spring AOP的想法,因為它無法管理任何超出spring容器范圍的東西。但是,如果我們的應用程序完全是使用Spring框架創建的,那么我們可以使用Spring AOP,因為它很直接便於學習和應用。
●靈活性:鑒於有限的連接點支持,Spring AOP並不是一個完整的AOP解決方案,但它解決了程序員面臨的最常見的問題。 如果我們想要深入挖掘並利用AOP達到其最大能力,並希望獲得來自各種可用連接點的支持,那么AspectJ是最佳選擇。
●性能:如果我們使用有限的切面,那么性能差異很小。但是,有時候應用程序有數萬個切面的情況。在這種情況下,我們不希望使用運行時織入,所以最好選擇AspectJ。已知AspectJ比Spring AOP快8到35倍。
●共同優點:這兩個框架是完全兼容的。我們可以隨時利用Spring AOP,並且仍然使用AspectJ來獲得前者不支持的連接點。

 三、Spring結合AspectJ的使用方法和原理

Spring AOP和AspectJ都可以使用AspectJ注解的方式來實現,實際上,Spring AOP為了遵循規范,或者和AspectJ保持兼容,借助了AspectJ的注解風格和AOP聯盟定義的部分底層接口,不能想當然的認為Spring AOP是借助AspectJ來實現的,在原理上Spring AOP和AspectJ沒有關系。
Spring AOP如何使用就不細說了,下面說下Spring結合AspectJ注解方式的使用。

切面聲明:

 1 @Aspect
 2 public class MyAspect {
 3 
 4     @Pointcut("execution(* com.mzsea.spring.service.*.*(..))")
 5     private void pointCut() {
 6 
 7     }
 8 
 9     @Around("pointCut()")
10     public Object myAround(ProceedingJoinPoint joinPoint) throws Throwable {
11         System.out.println("before");
12         Object obj = joinPoint.proceed();
13         System.out.println("after");
14         return obj;
15     }

業務邏輯:

1 @Component
2 public class UserServiceImpl {
3 
4     public void add() {
5         System.out.println("hello user!");
6 }

Spring配置:

1 <context:load-time-weaver/>
2 <context:component-scan base-package="com.mzsea.spring"/>

 AspectJ配置(class目錄下META-INF/aop.xml):

1 <aspectj>
2     <weaver options="-verbose -debug -showWeaveInfo">
3         <include within="com.mzsea.spring..*"/>
4     </weaver>
5     <aspects>
6         <aspect name="com.mzsea.spring.aspectj.MyAspect"/>
7     </aspects>
8 </aspectj>

Main方法:

1     public static void main(String[] args) {
2         ApplicationContext applicationContext = new ClassPathXmlApplicationContext("application.xml");
3         UserServiceImpl userServiceImpl = applicationContext.getBean(UserServiceImpl.class);
4         userServiceImpl.add();
5     }

運行結果:

before
hello user!
after

以上就是一個簡單的AspectJ應用,和Spring AOP相比,配置略有不同。

<aop:aspectj-autoproxy/>換成<context:load-time-weaver/>
還多了META-INF/aop.xml配置,並且在運行時需要指定java agent參數-javaagent:spring-instrument-{version}.jar

@Aspect注解的類並不在spring的bean管轄之內,完全由AspectJ使用。Spring只是整合了AspectJ的入口,讓AspectJ在類加載時改變Class字節碼,然后就與AspectJ無關了。

接下來分析下spring是如何結合AspectJ的。

Java引入了java.lang.Instrument包,該包提供了一些工具幫助開發人員在Java程序運行時,動態修改系統中的Class類型,並不需要自定義類加載器,使用該軟件包的一個關鍵組件就是Java agent。Java agent的使用規范就是在指定的jar包內的MANIFEST.MF文件指定Premain-Class項,Premain-Class指定的那個類必須實現premain()方法。

打開spring-instrument.jar,找到Premain-Class指定的類,代碼如下:

1     private static volatile Instrumentation instrumentation;
2     public static void premain(String agentArgs, Instrumentation inst) {
3         instrumentation = inst;
4     }

發現超級簡單啊,就是把Instrumentation參數賦值給靜態變量了,一看感覺沒起任何作用啊,怎么可能就這么簡單就實現了,確實如你所想,spring在下一盤大棋,這邊只是暴露出Instrumentation,后面如何對Instrumentation操作就是關鍵了。

Spring啟動過程中,會解析<context:load-time-weaver/>這個配置。
我們知道這個xml元素會由ContextNamespaceHandler處理,打開此類:

 1     public void init() {
 2         registerBeanDefinitionParser("property-placeholder", new PropertyPlaceholderBeanDefinitionParser());
 3         registerBeanDefinitionParser("property-override", new PropertyOverrideBeanDefinitionParser());
 4         registerBeanDefinitionParser("annotation-config", new AnnotationConfigBeanDefinitionParser());
 5         registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser());
 6         registerBeanDefinitionParser("load-time-weaver", new LoadTimeWeaverBeanDefinitionParser());
 7         registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser());
 8         registerBeanDefinitionParser("mbean-export", new MBeanExportBeanDefinitionParser());
 9         registerBeanDefinitionParser("mbean-server", new MBeanServerBeanDefinitionParser());
10     }

果然看到load-time-weaver對應的解析器,LoadTimeWeaverBeanDefinitionParser。
解析器通過配置屬性或者自動識別有無META-INF/aop.xml來判斷是否開啟AspectJWeaving,開啟了就會注冊相關的BeanDefinition。這里會注冊
AspectJWeavingEnabler、DefaultContextLoadTimeWeaver這2個BeanDefinition。

Spring配置xml的解析在著名的refresh()方法的obtainFreshBeanFactory()階段就已經完成,此時xml中定義的配置都已經轉換成各種BeanDefinition對象存儲在BeanFactory中,接着在prepareBeanFactory()中有這樣的代碼:

1    if (beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
2        beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
3        // Set a temporary ClassLoader for type matching.
4        beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
5    }

意思就是如果存在名為“loadTimeWeaver”的bean,則注冊LoadTimeWeaverAwareProcessor這個BeanPostProcessor,這個很好理解,就是實現了LoadTimeWeaverAware接口的bean會自動注入“loadTimeWeaver”這個bean,和其他的Aware接口一個道理。setTempClassLoader這個操作是創建一個臨時ClassLoader,ContextTypeMatchClassLoader,從名字大概能看出是上下文類型匹配類加載器的意思,但是到底有何用意暫時還不得而知。暫且留下疑問。

refresh()在執行完prepareBeanFactory()后,接着執行postProcessBeanFactory()、invokeBeanFactoryPostProcessors()、registerBeanPostProcessors()等關鍵操作,最后階段還會初始化不是懶加載的單例bean。
invokeBeanFactoryPostProcessors()是執行BeanFactory后處理器,此時BeanFactory已經加載結束,正式觸發BeanFactory后處理器的時候,這里是非常重要的擴展點,包括Spring本身,都利用這個擴展點干很多事。

回看AspectJWeavingEnabler,其實就是BeanFactoryPostProcessor的實現類之一,找到postProcessBeanFactory方法:

 1 public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
 2         enableAspectJWeaving(this.loadTimeWeaver, this.beanClassLoader);
 3     }
 4 public static void enableAspectJWeaving(LoadTimeWeaver weaverToUse, ClassLoader beanClassLoader) {
 5         if (weaverToUse == null) {
 6             if (InstrumentationLoadTimeWeaver.isInstrumentationAvailable()) {
 7                 weaverToUse = new InstrumentationLoadTimeWeaver(beanClassLoader);
 8             }
 9             else {
10                 throw new IllegalStateException("No LoadTimeWeaver available");
11             }
12         }
13         weaverToUse.addTransformer(
14                 new AspectJClassBypassingClassFileTransformer(new ClassPreProcessorAgentAdapter()));
15     }

在執行postProcessBeanFactory時,作為bean的AspectJWeavingEnabler已經實例化了,已經注入loadTimeWeaver為DefaultContextLoadTimeWeaver,DefaultContextLoadTimeWeaver中會根據不同的應用環境(Tomcat、GlassFish、JBoss、WebSphere、WebLogic、默認)創建相應的LoadTimeWeaver,因為不同的應用容器都實現了自己的類加載器,注冊transformer的方式各有差異,默認環境中使用的類加載器是sun.misc.Launcher.AppClassLoader,DefaultContextLoadTimeWeaver包裝了實際的LoadTimeWeaver,是裝飾器模式的應用,LoadTimeWeaver提供統一的接口屏蔽了不同類加載器注冊transformer的差異,實現一致處理。這里實際創建了InstrumentationLoadTimeWeaver,上述代碼13行調用了addTransformer(),最終調用了Instrumentation.addTransformer(),參數為通過委托模式包裝過的AspectJ的ClassPreProcessorAgentAdapter,至此,這就是通過Spring整合AspectJ的過程,至於具體的類加載階段的處理則由AspectJ接管。

可見Spring通過BeanFactoryPostProcessor對AspectJ進行了整合,在這個階段,用戶自定義的Bean還沒有初始化,對應的Class也大概率未加載,整合后,加載Class大部分情況下就會被AspectJ攔截,根據配置進行字節碼修改,實現切面增強。如果需要增強的Class在Spring和AspectJ整合之前就已經加載過了,根據ClassLoader的加載規則可知,對於同一個ClassLoader,同一Class很少情況下會加載兩次(Class被gc回收的條件苛刻),此時需要增強的Class就錯過了切面織入過程,AOP就失效了。所以需要盡早的對AspectJ進行了整合,BeanFactoryPostProcessor是Spring初始化過程中比較靠前的擴展點,AspectJ在此整合不失為一個合理的時機。

四、Spring注解方式使用AspectJ遇到的問題

從上節中了解的Spring結合AspectJ的原理,在使用時,更傾向於簡單的配置。
現在流行的Spring Boot項目結構,提倡簡潔配置,舍棄xml,Spring Boot針對一些常見配置做了默認處理,用戶無需配置過多,結合Spring提供的xml與注解對應關系,使應用結構大為簡化。
<context:load-time-weaver/>這個配置,與之對應的注解配置為@EnableLoadTimeWeaving,只要在@Configuration聲明的類上配置上@EnableLoadTimeWeaving,應用啟動過程中就可以解析注解,生成對應的BeanDefinition。

ConfigurationClassPostProcessor這個類為識別Spring Class注解的入口,@Configuration、@ComponentScans、@ImportResource、@Import等等這些注解,都由ConfigurationClassPostProcessor負責解析,生成BeanDefinition,具體的解析過程,可以跟蹤postProcessBeanFactory觀得全貌。

查看@EnableLoadTimeWeaving代碼:

1 @Import(LoadTimeWeavingConfiguration.class)
2 public @interface EnableLoadTimeWeaving {
3 4 }

發現對這個注解又聲明了@Import注解,@Import的作用就和xml配置中的import標簽類似,用於加載指定參數中的bean配置,查看LoadTimeWeavingConfiguration代碼:

 1 @Bean(name = ConfigurableApplicationContext.LOAD_TIME_WEAVER_BEAN_NAME)
 2 @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
 3 public LoadTimeWeaver loadTimeWeaver() {
 4    LoadTimeWeaver loadTimeWeaver = null;
 5 
 6    if (this.ltwConfigurer != null) {
 7       // The user has provided a custom LoadTimeWeaver instance
 8       loadTimeWeaver = this.ltwConfigurer.getLoadTimeWeaver();
 9    }
10 
11    if (loadTimeWeaver == null) {
12       // No custom LoadTimeWeaver provided -> fall back to the default
13       loadTimeWeaver = new DefaultContextLoadTimeWeaver(this.beanClassLoader);
14    }
15 
16    AspectJWeaving aspectJWeaving = this.enableLTW.getEnum("aspectjWeaving");
17    switch (aspectJWeaving) {
18       case DISABLED:
19          // AJ weaving is disabled -> do nothing
20          break;
21       case AUTODETECT:
22          if (this.beanClassLoader.getResource(AspectJWeavingEnabler.ASPECTJ_AOP_XML_RESOURCE) == null) {
23             // No aop.xml present on the classpath -> treat as 'disabled'
24             break;
25          }
26          // aop.xml is present on the classpath -> enable
27          AspectJWeavingEnabler.enableAspectJWeaving(loadTimeWeaver, this.beanClassLoader);
28          break;
29       case ENABLED:
30          AspectJWeavingEnabler.enableAspectJWeaving(loadTimeWeaver, this.beanClassLoader);
31          break;
32    }
33 
34    return loadTimeWeaver;
35 }

看到了熟悉的“loadTimeWeaver”配置。
loadTimeWeaver實例化過程中,從代碼16行開始,通過判斷配置的aspectJWeaving屬性來決定啟用AspectJWeaving,代碼27行,和前一小節提及的AspectJWeavingEnabler中postProcessBeanFactory調用了同樣的方法,由此可見,xml配置方式和注解方式最后都殊途同歸,只不過開始階段的解析不一樣。

實踐,舍去xml,換上注解:

1 @Configuration
2 @EnableLoadTimeWeaving
3 public class AppConfig {
4 
5 }

啟動項目,預計結果應該和之前一樣,可是事與願違,發現切面沒被織入,業務代碼沒有增強。
配置仔細推敲了好幾遍,還是不行,為什么xml方式配置就可以,注解方式就失效了呢。

猜想,可能是需要被增強的Class在AspectJ生效前被加載了!
打開JVM參數-verbose,控制台會打印class加載信息,觀察class加載情況。
打開AspectJ調試參數:<weaver options="-verbose -debug -showWeaveInfo">
觀察兩種配置下的UserServiceImpl.class加載位置。

AspectJ調試日志:

[AppClassLoader@18b4aac2] info AspectJ Weaver Version 1.8.13 built on Wednesday Nov 15, 2017 at 19:26:44 GMT
[AppClassLoader@18b4aac2] info register classloader sun.misc.Launcher$AppClassLoader@18b4aac2
[AppClassLoader@18b4aac2] info using configuration /D:/Workspaces/eclipse/aspectj/target/classes/META-INF/aop.xml

意外的是,xml配置和注解配置在AspectJ加載之前都加載了UserServiceImpl.class,但是加載日志有所區別:

Xml配置下UserServiceImpl.class加載:

[Loaded com.mzsea.spring.service.UserServiceImpl from __JVM_DefineClass__]

注解配置下UserServiceImpl.class加載:

[Loaded com.mzsea.spring.service.UserServiceImpl from file:/D:/Workspaces/eclipse/aspectj/target/classes/]

比較發現一個是從JVM內部加載,一個是從class文件加載,這可能就是產生問題所在。

反復調試加載位置上下文,發現在調用ConfigurationClassPostProcessor.postProcessBeanFactory()中有加載UserServiceImpl.class,問題浮出水面,因為同一個ClassLoader對同一個Class只加載一次的規則,還沒有執行到LoadTimeWeaver相關的代碼,UserServiceImpl.class就已經被加載了,后面AspectJ加載后,自然不會再對UserServiceImpl.class攔截增強。

雖然兩種不同的配置都有加載UserServiceImpl.class,但是從日志上看就是有區別的。

深入UserServiceImpl.class初始加載的代碼:org.springframework.beans.factory.support.AbstractBeanFactory#doResolveBeanClass

 1 private Class<?> doResolveBeanClass(RootBeanDefinition mbd, Class<?>... typesToMatch)
 2       throws ClassNotFoundException {
 3 
 4    ClassLoader beanClassLoader = getBeanClassLoader();
 5    ClassLoader classLoaderToUse = beanClassLoader;
 6    if (!ObjectUtils.isEmpty(typesToMatch)) {
 7       // When just doing type checks (i.e. not creating an actual instance yet),
 8       // use the specified temporary class loader (e.g. in a weaving scenario).
 9       ClassLoader tempClassLoader = getTempClassLoader();
10       if (tempClassLoader != null) {
11          classLoaderToUse = tempClassLoader;
12          if (tempClassLoader instanceof DecoratingClassLoader) {
13             DecoratingClassLoader dcl = (DecoratingClassLoader) tempClassLoader;
14             for (Class<?> typeToMatch : typesToMatch) {
15                dcl.excludeClass(typeToMatch.getName());
16             }
17          }
18       }
19    }
20    String className = mbd.getBeanClassName();
21    if (className != null) {
22       Object evaluated = evaluateBeanDefinitionString(className, mbd);
23       if (!className.equals(evaluated)) {
24          // A dynamically resolved expression, supported as of 4.2...
25          if (evaluated instanceof Class) {
26             return (Class<?>) evaluated;
27          }
28          else if (evaluated instanceof String) {
29             return ClassUtils.forName((String) evaluated, classLoaderToUse);
30          }
31          else {
32             throw new IllegalStateException("Invalid class name expression result: " + evaluated);
33          }
34       }
35       // When resolving against a temporary class loader, exit early in order
36       // to avoid storing the resolved Class in the bean definition.
37       if (classLoaderToUse != beanClassLoader) {
38          return ClassUtils.forName(className, classLoaderToUse);
39       }
40    }
41    return mbd.resolveBeanClass(beanClassLoader);
42 }

發現此處在對class加載時會選擇不同的ClassLoader,beanClassLoader和tempClassLoader。getTempClassLoader()即是上文
beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
如果tempClassLoader為null,就使用beanClassLoader,beanClassLoader顯然是加載bean class用的,如果沒有tempClassLoader,bean class就被提前加載了,導致AspectJ失效。

查看方法棧,發現調用doResolveBeanClass的外層方法有:

1 AbstractBeanFactory#isFactoryBean()
2 ListableBeanFactory#getBeanNamesForType()

可以看到,在spring啟動過程中,經常需要通過判定bean的類型去做處理,如何知道bean的類型,就需要再加bean對應的Class,這就是問題所在。

啟動過程中,prepareBeanFactory()中判斷beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)的目的就是如果存在“loadTimeWeaver”,那么就設置tempClassLoader,而使用注解方式配置,使“loadTimeWeaver”的定義延遲到invokeBeanFactoryPostProcessors()中了,雖然spring也在invokeBeanFactoryPostProcessors()中進行了補救:

 1 protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
 2    PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());
 3 
 4    // Detect a LoadTimeWeaver and prepare for weaving, if found in the meantime
 5    // (e.g. through an @Bean method registered by ConfigurationClassPostProcessor)
 6    if (beanFactory.getTempClassLoader() == null && beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
 7       beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
 8       beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
 9    }
10 }

第一行調用中處理了各種注解,過程中調用了isFactoryBean()、getBeanNamesForType()等判斷類型的方法,由於TempClassLoader為null,使用beanClassLoader加載了大部分bean class,雖然生成了“loadTimeWeaver”,所以接下來的setTempClassLoader也沒多大作用了。

那如果用注解配置下是不是及時檢查“loadTimeWeaver”設置TempClassLoader就可以了呢?

AspectJWeavingEnabler#enableAspectJWeaving()這是結合的關鍵,這個方法不執行,AspectJ就不起作用。xml配置的時候,這個方法在postProcessBeanFactory就執行了,還算靠前;但是注解配置的時候,這個方法卻延遲到“loadTimeWeaver”實例化時進行,bean實例化已經是spring啟動過程的尾聲了,在refresh()方法末尾的finishBeanFactoryInitialization()中才進行,而且bean的實例化先后順序不固定,有可能需要織入增強邏輯的bean先實例化,而“loadTimeWeaver”后實例化,此時AspectJ是徹底不起作用了。

總之,要使AspectJ起作用,必須盡早發現“loadTimeWeaver”,setTempClassLoader,盡早加載AspectJ,postProcessBeanFactory()是比較早的機會,但是也不能百分百避免bean class在AspectJ前被beanClassLoader加載,此處為Spring的一個bug,修復需要重新調整代碼結構,有空會提交PR。

五、總結

通過分析,Spring將AspectJ融入體系不是容易的事,繞了很多路,稍不注意AspectJ就不起作用。使用另一個ClassLoader去加載Class用於做bean的類型的判定,而不影響本身bean class的加載,AspectJ的加載點很重要,盡早加載越好。


免責聲明!

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



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