SpringBoot中使用LoadTimeWeaving技術實現AOP功能
目錄
1. 關於LoadTimeWeaving
1.1 LTW與不同的切面織入時機
AOP——面向切面程式設計,通過為目標類織入切面的方式,實現對目標類功能的增強。按切面被織如到目標類中的時間劃分,主要有以下幾種:
1.執行期織入
這是最常見的,比如在執行期通過為目標類生成動態代理的方式實現AOP就屬於執行期織入,這也是Spring AOP中的預設實現,並且提供了兩種建立動態代理的方式:JDK自帶的針對介面的動態代理和使用CGLib動態建立子類的方式建立動態代理。2.編譯期織入
使用特殊的編譯器在編譯期將切面織入目標類,這種比較少見,因為需要特殊的編譯器的支援。3.類載入期織入
通過位元組碼編輯技術在類載入期將切面織入目標類中,這是本篇介紹的重點。它的核心思想是:在目標類的class檔案被JVM載入前,通過自定義類載入器或者類檔案轉換器將橫切邏輯織入到目標類的class檔案中,然後將修改後class檔案交給JVM載入。這種織入方式可以簡稱為LTW(LoadTimeWeaving)。
1.2 JDK實現LTW的原理
可以使用JKD的代理功能讓代理器訪問到JVM的底層元件,藉此向JVM註冊類檔案轉換器,在類載入時對類檔案的位元組碼進行轉換。具體而言,java.lang.instrument包下定義了ClassFileTransformer介面,該介面的作用如下面的註釋所描述
* An agent provides an implementation of this interface in order
* to transform class files.
* The transformation occurs before the class is defined by the JVM.
可以通過實現該介面,並重寫如下抽象方法自定義類檔案轉換規則
byte[] transform( ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException;
classfileBuffer是原始類檔案對應的位元組碼陣列,返回的byte[]為轉化後的位元組碼陣列,如果返回null,則表示不進行位元組碼處理。
而java.lang.instrument包下的Instrumentation介面則可以將我們自定義的ClassTransFormer向JVM內部的元件進行註冊
void
addTransformer(ClassFileTransformer transformer);
在實際使用中,可以通過JVM的-javaagent代理引數在啟動時獲取JVM內部元件的引用,將ClassFileTransformer例項註冊到JVM中,JVM在載入Class檔案時,會先呼叫這個ClassTransformer的transform()方法對Class檔案的位元組碼進行轉換,比如織入切面中定義的橫切邏輯,實現AOP功能。整個過程可以入下所示
1.3 如何在Spring中實現LTW
Spring中預設通過執行期生成動態代理的方式實現切面的織入,實現AOP功能,但是Spring也可以使用LTW技術來實現AOP,並且提供了細粒度的控制,支援在單個ClassLoader範圍內實施類檔案轉換。
Spring中的org.springframework.instrument.classloading.LoadTimeWeaver介面定義了為類載入器新增ClassFileTransfomer的抽象
* Defines the contract for adding one or more
* {@link ClassFileTransformer ClassFileTransformers} to a {@link ClassLoader}.
*
public interface LoadTimeWeaver {
Spring的LTW支援AspectJ定義的切面,既可以是直接使用AspectJ語法定義的切面,也可以是使用@AspectJ註解,通過java類定義的切面。Spring LTW通過讀取classpath下META-INF/aop.xml檔案,獲取切面類和要被切面織入的目標類的相關資訊,通過LoadTimeWeaver在ClassLoader載入類檔案時將切面織入目標類中,其工作原理如下所示
Spring中可以通過LoadTimeWeaver將Spring提供的ClassFileTransformer註冊到ClassLoader中。在類載入期,註冊的ClassFileTransformer讀取類路徑下META-INF/aop.xml檔案中定義的切面類和目標類資訊,在目標類的class檔案真正被VM載入前織入切面資訊,生成新的Class檔案位元組碼,然後交給VM載入。因而之後建立的目標類的例項,就已經實現了AOP功能。
2. Springboot中使用LTW實現AOP的例子
實現一個簡單的AOP需求,在方法呼叫前後打印出開始和結束的日誌資訊。
- 相關的maven依賴和外掛
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>
-javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar"
-javaagent:"${settings.localRepository}/org/springframework/spring-instrument/${spring.version}/spring-instrument-${spring.version}.jar"
<!-- -Dspring.profiles.active=test-->
</argLine>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<agent>
${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar
</agent>
<agent>
${settings.localRepository}/org/springframework/spring-instrument/${spring.version}/spring-instrument-${spring.version}.jar
</agent>
</configuration>
</plugin>
</plugins>
</build>
這裡通過maven外掛的方式為JVM設定代理,通過-javaagent引數指定織入器類包的路徑,這樣就可以在類載入期將切面織入,更多關於javaagent的知識可以參考javaagent
- 織入目標類
/**
* @author: takumiCX
* @create: 2018-12-19
**/
@Component
public class LtwBean {
public void test(){
System.out.println("process.......");
}
}
只有一個test()方法,通過@Componet註解向容器註冊。
- 切面類
/**
* @author: takumiCX
* @create: 2018-12-19
**/
@Aspect
public class LogMethodInvokeAspect {
@Pointcut("execution(public * com.takumiCX.ltw.*.*(..))")
public void pointCut(){
}
@Around("pointCut()")
public void advise(ProceedingJoinPoint pjp) throws Throwable {
Signature signature = pjp.getSignature();
System.out.println(signature+" start..... ");
pjp.proceed();
System.out.println(signature+" end......");
}
}
@Aspect註解表示這是一個切面類
- 配置類
/**
* @author: takumiCX
* @create: 2018-12-19
**/
@Configuration
@ComponentScan("com.takumiCX.ltw")
@EnableLoadTimeWeaving(aspectjWeaving=AUTODETECT)
public class CustomLtwConfig{
}
通過@@EnableLoadTimeWeaving開啟LTW功能,可以通過屬性aspectjWeaving指定LTW的開啟策略
- ENABLED
開啟LTW - DISABLED
不開啟LTW - AUTODETECT
如果類路徑下能讀取到META-INF/aop.xml檔案,則開啟LTW,否則關閉
- 在META-INF資料夾下編寫aop.xml檔案
aop.xml檔案內容
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "http://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>
<!--要織入切面的目標類-->
<weaver>
<include within="com.takumiCX.ltw..*" />
</weaver>
<!--切面類-->
<aspects>
<aspect name="com.takumiCX.ltw.aspect.LogMethodInvokeAspect" />
</aspects>
</aspectj>
這樣我們的Spring容器就能載入該檔案讀取到描述目標類和切面類的相關資訊,容器在載入目標類的class檔案到jvm之前,會將切面類中定義的增強邏輯織入到class檔案中,真正載入到jvm中的是織入切面後的class檔案,因而通過該class檔案創建出的目標類的例項,不需要經過動態代理就能實現AOP相關功能。
- 測試類
/**
* @author: takumiCX
* @create: 2018-12-20
**/
@RunWith(SpringRunner.class)
@SpringBootTest(classes ={CustomLtwConfig.class})
public class LTWTest {
@Autowired
private LtwBean ltwBean;
@Test
public void testLTW() throws InterruptedException {
ltwBean.test();
}
}
最後的結果如下
方法呼叫前後分別記錄的開始和結束的日誌資訊,說明我們的切面成功的織入到了目標類。但是這裡可能有一個疑問,這真的是LTW(Load TimeWeaving)通過在類載入期織入切面起到的作用嗎?有沒有可能是LTW沒起作用,是Spring AOP預設通過執行期生成動態代理的方式實現的AOP。
我們的LogMethodInvokeAspect切面類上並沒有加@Component註解向容器註冊,並且配置類CustomLtwConfig上也沒有加@EnableAspectJAutoProxy註解開啟Aspectj的執行時動態代理,所以這裡基於動態代理的AOP並不會生效。
為了驗證我們的想法,將aop.xml檔案刪除
重新執行測試程式碼
AOP沒起到作用,說明剛才的AOP功能確實是通過LTW技術實現的。
當我們給切面類加上@Component註解,給配置類加上@EnableAspectJAutoProxy
/**
* @author: takumiCX
* @create: 2018-12-19
**/
@Aspect
@Component
public class LogMethodInvokeAspect {
/**
* @author: takumiCX
* @create: 2018-12-19
**/
@Configuration
@ComponentScan("com.takumiCX.ltw")
@EnableAspectJAutoProxy
@EnableLoadTimeWeaving(aspectjWeaving=AUTODETECT)
public class CustomLtwConfig{
}
再次執行測試類時,發現AOP又生效了,這時候類路徑下並沒有aop.xml,所以這時候AOP是Spring在執行期通過動態代理的方式實現的。
3. 參考資料
《精通Spring4.x企業應用開發實戰》
《spring揭祕》
https://sexycoding.iteye.com/blog/1062372