前提介紹
當我們聊到Spring框架的項目實際開發中,用的強大的功能之一就是(面向切面編程)的這門AOP技術。如果使用得當,它的最大的作用就是侵入性比較少並且簡化我們的工作任務(節省大量的重復性編碼),最為重要的一點是,它可以讓我們在不改變原有代碼的情況下,織入我們的邏輯,尤其是在我們沒有源代碼的時候,而且當我們恢復之前的邏輯的時候,只需要去掉代理就可以了。
AOP的動態代理
Spring AOP的常規的實現方式為cglib和jdk動態代理。兩者均可實現,只是性能上略有差異,此處不再詳述。
- 無論是JDK的動態代理和Cglib實現的Spring代理機制都有一些的局限性,那就是生成的代理對象在內部調用方法時,AOP功能無效,因為畢竟不是代理對象的調用而是this的調用。
- 他們都只能對public、protected類型的成員方法生效。
當然,也是有變通的方案解決,比如將bean當做屬性注入到自身,然后所有方法調用都通過這個屬性來調用。或者通過AopContext.currentProxy的方式去獲取代理對象。但是這些解決方案,在開發過程中開發者很容易因為疏忽導致出現問題。
所以,如果需要一種更加強大和易用的aop實現方案,那就是字節碼編織技術aspectj。通過修改字節碼,可以實現對所有方法進行切面,包括(final、private、static類型的方法),功能強大。並且spring支持aspectj方式的aop。
AOP技術類型
在介紹強大的Aspectj的技術之前,我們先進行對AOP技術實現基礎進行分類,通過為目標類織入切面的方式,實現對目標類功能的增強。按切面被織如到目標類中的時間划分
- 編譯時:使用特殊的編譯器在編譯期將切面織入目標類,這種比較少見,因為需要特殊的編譯器的支持。例如,AspectJ編譯器,很少有到,目前我還沒有用到。
- 加載時:通過字節碼編輯技術在類加載期將切面織入目標類中,它的核心思想是:在目標類的class文件被JVM加載前,通過自定義類加載器或者類文件轉換器將橫切邏輯織入到目標類的class文件中,然后將修改后class文件交給JVM加載。這種織入方式可以簡稱為LTW(LoadTimeWeaving),AspectJ的LoadTimeWeaving (LTW)
- 運行時:運行期通過為目標類生成動態代理的方式實現AOP就屬於運行期織入,這也是Spring AOP中的默認實現,並且提供了兩種創建動態代理的方式:JDK自帶的針對接口的動態代理和使用CGLib動態創建子類的方式創建動態代理。
這里我們介紹是Spring整合AspectJ的LTW機制。屬於動態加載織入。
LTW可以解決的問題
- 非spring管理的類依賴注入和切面不生效的問題。
- 調用類內方法切面不生效的問題。
- AOP 切面織入方式
LTW的原理
類加載期通過字節碼編輯技術將切面織入目標類,這種方式叫做LTW(Load Time Weaving)。
使用JDK5 新增的 java.lang.instrument包,在類加載時對字節碼進行轉換,從而實現 AOP功能。
JDK的代理功能讓代理器訪問到JVM的底層組件,借此向JVM注冊類文件轉換器,在類加載時對類文件的字節碼進行轉換ClassFileTransformer接口。具體方向可以研究一下java agent技術即可。
Spring中實現LTW
-
Spring中默認通過運行期生成動態代理的方式實現切面的織入,實現AOP功能,但是Spring也可以使用LTW技術來實現AOP,並且提供了細粒度的控制,單個ClassLoader范圍內實施類文件轉換。
-
Spring中的org.springframework.instrument.classloading.LoadTimeWeaver接口定義了為類加載器添加ClassFileTransfomer的抽象
-
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功能。
maven依賴和插件
spring-AOP 和 aspectJ
spring的maven配置
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.2.5</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.5</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.2.5</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.5</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-instrument</artifactId>
<version>5.2.5</version>
</dependency>
springboot的配置
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.3.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-instrument</artifactId>
<version>5.2.7.RELEASE</version>
</dependency>
</dependencies>
其中都會有maven aspectjWeaver包
<dependency>
<groupId>aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.5.3</version>
</dependency>
spring全注解方式
@Configuration
//開啟Aspectj運行時編織
@EnableLoadTimeWeaving(aspectjWeaving = AspectJWeaving.ENABLED)
//開啟非spring容器bean的依賴注入支持
@EnableSpringConfigured
@ComponentScan(basePackages = "demo")
public class AppConfig extends WebMvcConfigurationSupport {}
springboot全注解方式
@SpringBootApplication
@EnableLoadTimeWeaving
@EnableSpringConfigured
@EnableAsync(mode = AdviceMode.ASPECTJ)
切面類
@Aspect
//讓spring可以將依賴注入到切面對象中
@Configurable
public class SampleAspectj {
@Autowired
private RestBean rbean;
@Around(value = "@annotation(rpc) ")
public void aop(ProceedingJoinPoint joinPoint,Rpc rpc) {
rbean.rwar();
}
}
對象切點類
@Configurable
public class TestOriginObject {
// 依賴注入
@Autowired
public TestService testService;
// spring 的切面
@Async
public void print() {
System.out.println("TestOriginObject print thread " + Thread.currentThread().toString());
}
// 自定義的切面
@Rpc
public void print1() {
System.out.println("TestOriginObject print1");
}
}
@Component
public class TestService {
@Async
@Profile
public void asyncPrint() {
System.out.println("TestService print thread " + Thread.currentThread().toString());
}
public void print() {
asyncPrint();
}
private static void test04() {
log.info("------------test04-----------");
}
private void test03() {
log.info("------------test03-----------");
}
public void test02() {
log.info("------------test02-----------");
}
}
aop.xml配置
放到/src/main/resources/META-INF目錄下
<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "https://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>
<weaver options="-showWeaveInfo -XnoInline -Xset:weaveJavaxPackages=true -Xlint:ignore -verbose -XmessageHandlerClass:org.springframework.aop.aspectj.AspectJWeaverMessageHandler">
<!-- 只對指定包下的類進行編織 -->
<include within="demo..*"/>
</weaver>
<aspects>
<!-- 使用指定的切面類進行編織 -->
<aspect name="test.SampleAspectj"/>
</aspects>
</aspectj>
@SpringBootApplication
@EnableLoadTimeWeaving
@EnableSpringConfigured
@EnableAsync(mode = AdviceMode.ASPECTJ)
public class AppApplication {
public static void main(String[] args) {
// 初始化 spring context
ApplicationContext context = SpringApplication.run(AppApplication.class, args);
// 創建 POJO,此時 TestService 會被注入到 POJO 中
TestOriginObject pojo = new TestOriginObject();
System.out.println("inject bean " + pojo.testService);
TestService testService = context.getBean(TestService.class);
// 正常調用切面
testService.asyncPrint();
// 切面的內部調用
testService.print();
// 非 spring 管理的類切面調用,spring 定義的切面
pojo.print();
// 非 spring 管理的類切面調用,自定義的切面
pojo.print1();
testService.test02();
testService.test03();
testService.test04();
}
}
說明
- @EnableLoadTimeWeaving 為開啟 LTW,或使用 context:load-time-weaver/
- @Configurable 必須和 @EnableSpringConfigured (或 context:spring-configured/) 配合使用
- @Configurable 可指明在構造函數前或后注入
- @EnableAsync 或 @EnableCaching 必須使用 ASPECTJ 模式
@EnableAspectJAutoProxy 也會啟動運行時代理植入方式。
啟動 VM 參數
-javaagent:path\spring-instrument-5.1.6.RELEASE.jar
-javaagent:path\aspectjweaver-1.9.2.jar
可以采用Maven方式進行打包進入執行
<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>
LTW 官方文檔
-
AspectJ:https://www.eclipse.org/aspectj/doc/released/devguide/index.html
-
Spring AOP using AspectJ: (5.10.4):https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#aop-using-aspectj
public class Run {
public static void main(String[] args) {
//關鍵代碼,用於在程序中開啟agent
//通過bytebuddy拿到當前jvm的Instrumentation實例
Instrumentation instrumentation = ByteBuddyAgent.install();
//激活Aspectj的代理對象
Agent.agentmain("", instrumentation);
//激活spring代理對象
InstrumentationSavingAgent.agentmain("", instrumentation);
//啟動spring容器
//...
}
}