【Spring技術原理】Aspectj和LoadTimeWeaving的動態代理技術實現指南


前提介紹

當我們聊到Spring框架的項目實際開發中,用的強大的功能之一就是(面向切面編程)的這門AOP技術。如果使用得當,它的最大的作用就是侵入性比較少並且簡化我們的工作任務(節省大量的重復性編碼),最為重要的一點是,它可以讓我們在不改變原有代碼的情況下,織入我們的邏輯,尤其是在我們沒有源代碼的時候,而且當我們恢復之前的邏輯的時候,只需要去掉代理就可以了。

AOP的動態代理

Spring AOP的常規的實現方式為cglib和jdk動態代理。兩者均可實現,只是性能上略有差異,此處不再詳述。

  1. 無論是JDK的動態代理和Cglib實現的Spring代理機制都有一些的局限性,那就是生成的代理對象在內部調用方法時,AOP功能無效,因為畢竟不是代理對象的調用而是this的調用。
  2. 他們都只能對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加載類文件時將切面織入目標類中。

  1. Spring中可以通過LoadTimeWeaver將Spring提供的ClassFileTransformer注冊到ClassLoader中。

  2. 在類加載期,注冊的ClassFileTransformer讀取類路徑下META-INF/aop.xml文件中定義的切面類和目標類信息,在目標類的class文件真正被VM加載前織入切面信息,生成新的Class文件字節碼,然后交給VM加載。

  3. 因而之后創建的目標類的實例,就已經實現了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 官方文檔

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容器
		//...
	}
}


免責聲明!

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



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