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內部的組件進行註冊
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在運行期通過動態代理的方式實現的。