在 Spring 應用中使用 AspectJ

本章到目前為止討論的所有內容都是純粹的 Spring AOP。在本節中,我們將探討當您的需求超出 Spring AOP 本身提供的功能時,如何使用 AspectJ 編譯器或 weaver 來替代或補充 Spring AOP。

Spring 附帶了一個小的 AspectJ 切面庫,它在您的發行版中作為 spring-aspects.jar 獨立提供。您需要將其新增到類路徑中才能使用其中的切面。使用 AspectJ 在 Spring 中進行領域物件依賴注入AspectJ 的其他 Spring 切面 討論了這個庫的內容以及如何使用它。使用 Spring IoC 配置 AspectJ 切面 討論瞭如何依賴注入使用 AspectJ 編譯器織入的 AspectJ 切面。最後,在 Spring Framework 中使用 AspectJ 進行載入時織入 介紹了為使用 AspectJ 的 Spring 應用進行載入時織入。

使用 AspectJ 在 Spring 中進行領域物件依賴注入

Spring 容器例項化並配置您的應用程式上下文中定義的 bean。也可以要求 bean 工廠配置一個已存在的物件,給定包含要應用的配置的 bean 定義的名稱。spring-aspects.jar 包含一個註解驅動的切面,它利用此能力允許對任何物件進行依賴注入。此支援旨在用於在任何容器控制之外建立的物件。領域物件通常屬於此類別,因為它們通常使用 new 運算子以程式設計方式建立,或者由 ORM 工具作為資料庫查詢的結果建立。

@Configurable 註解將類標記為符合 Spring 驅動的配置。在最簡單的情況下,您可以純粹地將其用作標記註解,如下例所示

  • Java

  • Kotlin

package com.xyz.domain;

import org.springframework.beans.factory.annotation.Configurable;

@Configurable
public class Account {
	// ...
}
package com.xyz.domain

import org.springframework.beans.factory.annotation.Configurable

@Configurable
class Account {
	// ...
}

當以此方式用作標記介面時,Spring 透過使用與完全限定型別名稱(com.xyz.domain.Account)同名的 bean 定義(通常是 prototype 作用域)來配置註解型別(在本例中為 Account)的新例項。由於透過 XML 定義的 bean 的預設名稱是其型別的完全限定名稱,因此宣告 prototype 定義的一種便捷方式是省略 id 屬性,如下例所示

<bean class="com.xyz.domain.Account" scope="prototype">
	<property name="fundsTransferService" ref="fundsTransferService"/>
</bean>

如果您想顯式指定要使用的 prototype bean 定義的名稱,您可以直接在註解中進行,如下例所示

  • Java

  • Kotlin

package com.xyz.domain;

import org.springframework.beans.factory.annotation.Configurable;

@Configurable("account")
public class Account {
	// ...
}
package com.xyz.domain

import org.springframework.beans.factory.annotation.Configurable

@Configurable("account")
class Account {
	// ...
}

現在 Spring 會查詢名為 account 的 bean 定義,並將其用作配置新的 Account 例項的定義。

您還可以使用自動裝配,完全避免指定專門的 bean 定義。要讓 Spring 應用自動裝配,請使用 @Configurable 註解的 autowire 屬性。您可以指定 @Configurable(autowire=Autowire.BY_TYPE)@Configurable(autowire=Autowire.BY_NAME),分別用於按型別或按名稱自動裝配。作為替代方案,更推薦的方式是透過在欄位或方法級別使用 @Autowired@Inject 為您的 @Configurable bean 指定顯式的、註解驅動的依賴注入(更多詳情請參閱 基於註解的容器配置)。

最後,您可以透過使用 dependencyCheck 屬性(例如,@Configurable(autowire=Autowire.BY_NAME,dependencyCheck=true))來啟用 Spring 對新建立和配置的物件中的物件引用的依賴檢查。如果此屬性設定為 true,Spring 會在配置後驗證所有屬性(非基本型別或集合)是否已設定。

請注意,單獨使用該註解沒有任何作用。真正起作用的是 spring-aspects.jar 中的 AnnotationBeanConfigurerAspect,它根據註解的存在執行操作。實質上,該切面表示,“從用 @Configurable 註解的型別的新物件的初始化返回後,根據註解的屬性使用 Spring 配置新建立的物件”。在此上下文中,“初始化”指的是新例項化的物件(例如,使用 new 運算子例項化的物件)以及正在進行反序列化的 Serializable 物件(例如,透過 readResolve())。

上一段中的一個關鍵短語是“實質上”。在大多數情況下,“從新物件的初始化返回後”的確切語義是沒問題的。在這種情況下,“初始化後”意味著依賴項在物件構造後注入。這意味著依賴項在類的建構函式體中不可用。如果您希望在建構函式體執行之前注入依賴項,以便它們在建構函式體中可用,您需要在 @Configurable 宣告中定義此項,如下所示

  • Java

  • Kotlin

@Configurable(preConstruction = true)
@Configurable(preConstruction = true)

您可以在 AspectJ 程式設計指南本附錄 中找到有關 AspectJ 中各種切點型別的語言語義的更多資訊。

要使其工作,帶註解的型別必須透過 AspectJ weaver 進行織入。您可以使用構建時的 Ant 或 Maven 任務來完成此操作(例如,參見 AspectJ 開發環境指南),或者使用載入時織入(參見 在 Spring Framework 中使用 AspectJ 進行載入時織入)。AnnotationBeanConfigurerAspect 本身需要由 Spring 進行配置(以便獲得用於配置新物件的 bean 工廠引用)。您可以按如下方式定義相關配置

  • Java

  • Kotlin

  • Xml

@Configuration
@EnableSpringConfigured
public class ApplicationConfiguration {
}
@Configuration
@EnableSpringConfigured
class ApplicationConfiguration
<beans xmlns="http://www.springframework.org/schema/beans"
	   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	   xmlns:context="http://www.springframework.org/schema/context"
	   xsi:schemaLocation="http://www.springframework.org/schema/beans
			https://www.springframework.org/schema/beans/spring-beans.xsd
			http://www.springframework.org/schema/context
			https://www.springframework.org/schema/context/spring-context.xsd">

	<context:spring-configured />

</beans>

在切面配置之前建立的 @Configurable 物件例項會導致向除錯日誌發出訊息,並且不會對物件進行配置。例如,Spring 配置中的某個 bean 在 Spring 初始化時會建立領域物件。在這種情況下,您可以使用 depends-on bean 屬性手動指定該 bean 依賴於配置切面。下例演示瞭如何使用 depends-on 屬性

<bean id="myService"
		class="com.xyz.service.MyService"
		depends-on="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect">

	<!-- ... -->

</bean>
除非您確實打算在執行時依賴其語義,否則不要透過 bean configurer 切面啟用 @Configurable 處理。特別是,請確保不要在已註冊為常規 Spring bean 的 bean 類上使用 @Configurable。這樣做會導致雙重初始化,一次透過容器,一次透過切面。

單元測試 @Configurable 物件

@Configurable 支援的目標之一是實現領域物件的獨立單元測試,而無需硬編碼查詢帶來的困難。如果 @Configurable 型別尚未被 AspectJ 織入,則在單元測試期間該註解沒有任何作用。您可以在被測物件中設定 mock 或 stub 屬性引用,然後像往常一樣進行。如果 @Configurable 型別已被 AspectJ 織入,您仍然可以在容器外部像往常一樣進行單元測試,但每次構造 @Configurable 物件時都會看到一條警告訊息,指示它尚未被 Spring 配置。

使用多個應用程式上下文

用於實現 @Configurable 支援的 AnnotationBeanConfigurerAspect 是一個 AspectJ 單例切面。單例切面的作用域與 static 成員的作用域相同:每個定義型別的 ClassLoader 中只有一個切面例項。這意味著,如果您在同一個 ClassLoader 層次結構中定義多個應用程式上下文,則需要考慮在哪裡定義 @EnableSpringConfigured bean 以及將 spring-aspects.jar 放在類路徑的哪個位置。

考慮一個典型的 Spring Web 應用程式配置,它有一個共享的父應用程式上下文,定義了通用的業務服務以及支援這些服務所需的一切,並且每個 servlet 都有一個子應用程式上下文(其中包含特定於該 servlet 的定義)。所有這些上下文都共存於同一個 ClassLoader 層次結構中,因此 AnnotationBeanConfigurerAspect 只能持有其中一個的引用。在這種情況下,我們建議在共享(父)應用程式上下文中定義 @EnableSpringConfigured bean。這定義了您可能想要注入到領域物件中的服務。一個結果是,您不能透過 @Configurable 機制配置領域物件以引用子(特定於 servlet)上下文中定義的 bean(這可能也不是您無論如何想做的事情)。

在同一容器中部署多個 Web 應用程式時,確保每個 Web 應用程式都使用自己的 ClassLoader 載入 spring-aspects.jar 中的型別(例如,將 spring-aspects.jar 放置在 WEB-INF/lib 中)。如果 spring-aspects.jar 僅新增到容器範圍的類路徑(因此由共享的父 ClassLoader 載入),所有 Web 應用程式將共享同一個切面例項(這可能不是您想要的)。

AspectJ 的其他 Spring 切面

除了 @Configurable 切面之外,spring-aspects.jar 還包含一個 AspectJ 切面,您可以使用它來驅動 Spring 對用 @Transactional 註解標記的型別和方法的事務管理。這主要面向希望在 Spring 容器之外使用 Spring Framework 事務支援的使用者。

解釋 @Transactional 註解的切面是 AnnotationTransactionAspect。當您使用此切面時,必須註解實現類(或該類中的方法或兩者),而不是該類實現的介面(如果有的話)。AspectJ 遵循 Java 的規則,即介面上的註解不會被繼承。

類上的 @Transactional 註解指定了該類中任何公共操作執行的預設事務語義。

類中方法上的 @Transactional 註解會覆蓋類註解(如果存在)提供的預設事務語義。任何可見性的方法都可以被註解,包括私有方法。直接註解非公共方法是對此類方法的執行進行事務劃分的唯一方法。

從 Spring Framework 4.2 開始,spring-aspects 提供了一個類似的切面,為標準的 jakarta.transaction.Transactional 註解提供了完全相同的功能。請檢視 JtaAnnotationTransactionAspect 獲取更多詳細資訊。

對於希望使用 Spring 配置和事務管理支援但不希望(或不能)使用註解的 AspectJ 程式設計師,spring-aspects.jar 還包含您可以擴充套件的 abstract 切面,以提供您自己的切點定義。請參閱 AbstractBeanConfigurerAspectAbstractTransactionAspect 切面的原始碼瞭解更多資訊。例如,以下摘錄展示瞭如何編寫一個切面,透過使用與完全限定類名匹配的 prototype bean 定義來配置領域模型中定義的所有物件例項

public aspect DomainObjectConfiguration extends AbstractBeanConfigurerAspect {

	public DomainObjectConfiguration() {
		setBeanWiringInfoResolver(new ClassNameBeanWiringInfoResolver());
	}

	// the creation of a new bean (any object in the domain model)
	protected pointcut beanCreation(Object beanInstance) :
		initialization(new(..)) &&
		CommonPointcuts.inDomainModel() &&
		this(beanInstance);
}

使用 Spring IoC 配置 AspectJ 切面

當您在 Spring 應用中使用 AspectJ 切面時,自然會希望並且能夠透過 Spring 配置這些切面。AspectJ 執行時本身負責切面的建立,透過 Spring 配置由 AspectJ 建立的切面的方式取決於切面使用的 AspectJ 例項化模型(per-xxx 子句)。

大多數 AspectJ 切面是單例切面。配置這些切面很容易。您可以建立一個 bean 定義,正常引用切面型別,幷包含 factory-method="aspectOf" bean 屬性。這確保了 Spring 透過向 AspectJ 請求來獲取切面例項,而不是嘗試自己建立例項。下例展示瞭如何使用 factory-method="aspectOf" 屬性

<bean id="profiler" class="com.xyz.profiler.Profiler"
		factory-method="aspectOf"> (1)

	<property name="profilingStrategy" ref="jamonProfilingStrategy"/>
</bean>
1 注意 factory-method="aspectOf" 屬性

非單例切面更難配置。然而,可以透過建立 prototype bean 定義並使用 spring-aspects.jar 中的 @Configurable 支援來配置由 AspectJ 執行時建立的切面例項。

如果您有一些希望使用 AspectJ 織入的 @AspectJ 切面(例如,對領域模型型別使用載入時織入)以及其他希望與 Spring AOP 一起使用的 @AspectJ 切面,並且這些切面都在 Spring 中配置,則需要告訴 Spring AOP @AspectJ 自動代理支援應使用配置中定義的 @AspectJ 切面的哪個精確子集進行自動代理。您可以透過在 <aop:aspectj-autoproxy/> 宣告中使用一個或多個 <include/> 元素來做到這一點。每個 <include/> 元素指定一個名稱模式,只有與至少一個模式匹配的名稱的 bean 才用於 Spring AOP 自動代理配置。下例展示瞭如何使用 <include/> 元素

<aop:aspectj-autoproxy>
	<aop:include name="thisBean"/>
	<aop:include name="thatBean"/>
</aop:aspectj-autoproxy>
不要被 <aop:aspectj-autoproxy/> 元素的名稱誤導。使用它會導致建立 Spring AOP 代理。這裡使用了 @AspectJ 風格的切面宣告,但 AspectJ 執行時並未參與。

在 Spring Framework 中使用 AspectJ 進行載入時織入

載入時織入 (LTW) 指的是在應用程式的類檔案載入到 Java 虛擬機器 (JVM) 時,將 AspectJ 切面織入到這些類檔案中的過程。本節的重點是在 Spring Framework 的特定上下文中配置和使用 LTW。本節不是 LTW 的一般性介紹。有關 LTW 的具體細節以及僅使用 AspectJ 配置 LTW(完全不涉及 Spring)的完整詳情,請參閱 AspectJ 開發環境指南的 LTW 部分

Spring Framework 為 AspectJ LTW 帶來的價值在於能夠對織入過程進行更細粒度的控制。'原生的' AspectJ LTW 是透過使用 Java (5+) 代理實現的,該代理在啟動 JVM 時透過指定 VM 引數來開啟。因此,它是一個 JVM 範圍的設定,這在某些情況下可能沒問題,但通常有點太粗糙了。Spring 啟用的 LTW 允許您以每個 ClassLoader 為基礎開啟 LTW,這更細粒度,並且在“單個 JVM-多個應用程式”環境(例如典型的應用伺服器環境)中可能更有意義。

此外,在某些環境中,這種支援無需對應用程式伺服器的啟動指令碼進行任何修改即可實現載入時織入,而傳統方式需要新增 -javaagent:path/to/aspectjweaver.jar 或(如本節後面所述)-javaagent:path/to/spring-instrument.jar。開發人員配置應用程式上下文來啟用載入時織入,而不是依賴通常負責部署配置(例如啟動指令碼)的管理員。

在銷售推廣結束之後,讓我們首先快速瞭解一個使用 Spring 的 AspectJ LTW 示例,然後詳細介紹示例中引入的元素。完整的示例請參閱基於 Spring Framework 的 Petclinic 示例應用程式

第一個示例

假設您是一名應用程式開發者,任務是診斷系統中一些效能問題的根源。我們不使用效能分析工具,而是開啟一個簡單的效能分析切面,以便快速獲得一些效能指標。之後,我們可以立即對特定區域應用更精細的效能分析工具。

此處展示的示例使用 XML 配置。您也可以透過 Java 配置 來配置和使用 @AspectJ。具體來說,您可以使用 @EnableLoadTimeWeaving 註解替代 <context:load-time-weaver/>(詳情見 下文)。

以下示例展示了效能分析切面,它並不複雜。這是一個基於時間的分析器,使用 @AspectJ 風格的切面宣告:

  • Java

  • Kotlin

package com.xyz;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.util.StopWatch;
import org.springframework.core.annotation.Order;

@Aspect
public class ProfilingAspect {

	@Around("methodsToBeProfiled()")
	public Object profile(ProceedingJoinPoint pjp) throws Throwable {
		StopWatch sw = new StopWatch(getClass().getSimpleName());
		try {
			sw.start(pjp.getSignature().getName());
			return pjp.proceed();
		} finally {
			sw.stop();
			System.out.println(sw.prettyPrint());
		}
	}

	@Pointcut("execution(public * com.xyz..*.*(..))")
	public void methodsToBeProfiled(){}
}
package com.xyz

import org.aspectj.lang.ProceedingJoinPoint
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Around
import org.aspectj.lang.annotation.Pointcut
import org.springframework.util.StopWatch
import org.springframework.core.annotation.Order

@Aspect
class ProfilingAspect {

	@Around("methodsToBeProfiled()")
	fun profile(pjp: ProceedingJoinPoint): Any? {
		val sw = StopWatch(javaClass.simpleName)
		try {
			sw.start(pjp.getSignature().getName())
			return pjp.proceed()
		} finally {
			sw.stop()
			println(sw.prettyPrint())
		}
	}

	@Pointcut("execution(public * com.xyz..*.*(..))")
	fun methodsToBeProfiled() {
	}
}

我們還需要建立一個 META-INF/aop.xml 檔案,以便告知 AspectJ weaver 我們想將 ProfilingAspect 織入到我們的類中。這種檔案約定,即在 Java classpath 上存在名為 META-INF/aop.xml 的檔案(或多個檔案),是標準的 AspectJ 約定。以下示例展示了 aop.xml 檔案:

<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "https://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>

	<weaver>
		<!-- only weave classes in our application-specific packages and sub-packages -->
		<include within="com.xyz..*"/>
	</weaver>

	<aspects>
		<!-- weave in just this aspect -->
		<aspect name="com.xyz.ProfilingAspect"/>
	</aspects>

</aspectj>
建議只織入特定的類(通常是應用程式包中的類,如上面的 aop.xml 示例所示),以避免諸如 AspectJ dump 檔案和警告之類的副作用。從效率角度來看,這也是一個最佳實踐。

現在我們可以進入 Spring 特定的配置部分了。我們需要配置一個 LoadTimeWeaver(稍後解釋)。這個載入時織入器是負責將一個或多個 META-INF/aop.xml 檔案中的切面配置織入到應用程式類中的關鍵元件。好訊息是它不需要太多配置(您可以指定更多選項,但這些將在後面詳細介紹),如下例所示:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="
		http://www.springframework.org/schema/beans
		https://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context
		https://www.springframework.org/schema/context/spring-context.xsd">

	<!-- a service object; we will be profiling its methods -->
	<bean id="entitlementCalculationService"
			class="com.xyz.StubEntitlementCalculationService"/>

	<!-- this switches on the load-time weaving -->
	<context:load-time-weaver/>
</beans>

現在所有必需的元件(切面、META-INF/aop.xml 檔案和 Spring 配置)都已準備就緒,我們可以建立以下帶有 main(..) 方法的驅動類來演示 LTW 的實際效果:

  • Java

  • Kotlin

package com.xyz;

// imports

public class Main {

	public static void main(String[] args) {
		ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");

		EntitlementCalculationService service =
				ctx.getBean(EntitlementCalculationService.class);

		// the profiling aspect is 'woven' around this method execution
		service.calculateEntitlement();
	}
}
package com.xyz

// imports

fun main() {
	val ctx = ClassPathXmlApplicationContext("beans.xml")

	val service = ctx.getBean(EntitlementCalculationService.class)

	// the profiling aspect is 'woven' around this method execution
	service.calculateEntitlement()
}

我們還有最後一件事要做。本節的引言確實提到,使用 Spring 可以在每個 ClassLoader 的基礎上選擇性地開啟 LTW,這是正確的。然而,對於本例,我們使用一個 Java agent(Spring 提供)來開啟 LTW。我們使用以下命令執行前面展示的 Main 類:

java -javaagent:C:/projects/xyz/lib/spring-instrument.jar com.xyz.Main

-javaagent 是一個用於指定和啟用 用於檢測(instrument)執行在 JVM 上的程式 的 agent 的標誌。Spring Framework 自帶一個這樣的 agent,即 InstrumentationSavingAgent,它打包在 spring-instrument.jar 中,在前面的示例中作為 -javaagent 引數的值提供。

Main 程式的執行輸出如下例所示。(我在 calculateEntitlement() 實現中加入了 Thread.sleep(..) 語句,以便 profiler 實際捕獲到非 0 毫秒的資料( 01234 毫秒不是 AOP 引入的開銷)。)以下列表顯示了我們執行 profiler 時得到的輸出:

Calculating entitlement

StopWatch 'ProfilingAspect': running time (millis) = 1234
------ ----- ----------------------------
ms     %     Task name
------ ----- ----------------------------
01234  100%  calculateEntitlement

由於此 LTW 是透過使用完整的 AspectJ 實現的,我們不僅限於通知 Spring bean。以下對 Main 程式稍作修改也能產生相同的結果:

  • Java

  • Kotlin

package com.xyz;

// imports

public class Main {

	public static void main(String[] args) {
		new ClassPathXmlApplicationContext("beans.xml");

		EntitlementCalculationService service =
				new StubEntitlementCalculationService();

		// the profiling aspect will be 'woven' around this method execution
		service.calculateEntitlement();
	}
}
package com.xyz

// imports

fun main(args: Array<String>) {
	ClassPathXmlApplicationContext("beans.xml")

	val service = StubEntitlementCalculationService()

	// the profiling aspect will be 'woven' around this method execution
	service.calculateEntitlement()
}

請注意,在前面的程式中,我們引導了 Spring 容器,然後完全在 Spring 上下文之外建立了 StubEntitlementCalculationService 的新例項。效能分析通知仍然會被織入。

誠然,這個示例很簡單。然而,Spring 中 LTW 支援的基本內容都在前面的示例中介紹了,本節的其餘部分將詳細解釋每項配置和用法的“為什麼”。

本例中使用的 ProfilingAspect 可能很簡單,但它非常有用。這是一個很好的開發時切面示例,開發者可以在開發期間使用,然後輕鬆將其從部署到 UAT 或生產環境的應用程式構建中排除。

切面

您在 LTW 中使用的切面必須是 AspectJ 切面。您可以使用 AspectJ 語言本身編寫它們,也可以使用 @AspectJ 風格編寫切面。您的切面既是有效的 AspectJ 切面,也是有效的 Spring AOP 切面。此外,編譯後的切面類需要位於 classpath 上。

META-INF/aop.xml

AspectJ LTW 基礎設施透過使用 classpath 上的一個或多個 META-INF/aop.xml 檔案進行配置(可以直接存在,或更常見的是在 jar 檔案中)。例如:

<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "https://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>

	<weaver>
		<!-- only weave classes in our application-specific packages and sub-packages -->
		<include within="com.xyz..*"/>
	</weaver>

</aspectj>
建議只織入特定的類(通常是應用程式包中的類,如上面的 aop.xml 示例所示),以避免諸如 AspectJ dump 檔案和警告之類的副作用。從效率角度來看,這也是一個最佳實踐。

此檔案的結構和內容在 AspectJ 參考文件 的 LTW 部分有詳細介紹。由於 aop.xml 檔案是純 AspectJ 檔案,我們在此不再詳細描述。

所需的庫 (JARS)

要使用 Spring Framework 對 AspectJ LTW 的支援,您至少需要以下庫:

  • spring-aop.jar

  • aspectjweaver.jar

如果您使用Spring 提供的 agent 來啟用 instrumentation,您還需要:

  • spring-instrument.jar

Spring 配置

Spring 的 LTW 支援中的關鍵元件是 LoadTimeWeaver 介面(位於 org.springframework.instrument.classloading 包中),以及 Spring 分發版中附帶的眾多實現。一個 LoadTimeWeaver 負責在執行時向 ClassLoader 新增一個或多個 java.lang.instrument.ClassFileTransformers,這為各種有趣的應用打開了大門,其中之一就是切面的 LTW。

如果您不熟悉執行時類檔案轉換的概念,請在繼續之前查閱 java.lang.instrument 包的 javadoc API 文件。雖然該文件並不全面,但至少您可以看到關鍵介面和類(以便在本節閱讀時參考)。

為特定的 ApplicationContext 配置 LoadTimeWeaver 可以像新增一行程式碼一樣簡單。(請注意,您幾乎肯定需要使用 ApplicationContext 作為您的 Spring 容器——通常 BeanFactory 不夠,因為 LTW 支援使用了 BeanFactoryPostProcessors。)

要啟用 Spring Framework 的 LTW 支援,您需要按如下方式配置 LoadTimeWeaver

  • Java

  • Kotlin

  • Xml

@Configuration
@EnableLoadTimeWeaving
public class ApplicationConfiguration {
}
@Configuration
@EnableLoadTimeWeaving
class ApplicationConfiguration
<beans xmlns="http://www.springframework.org/schema/beans"
	   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	   xmlns:context="http://www.springframework.org/schema/context"
	   xsi:schemaLocation="http://www.springframework.org/schema/beans
			https://www.springframework.org/schema/beans/spring-beans.xsd
			http://www.springframework.org/schema/context
			https://www.springframework.org/schema/context/spring-context.xsd">

	<context:load-time-weaver />

</beans>

前面的配置會自動為您定義和註冊許多 LTW 特定的基礎設施 bean,例如 LoadTimeWeaverAspectJWeavingEnabler。預設的 LoadTimeWeaverDefaultContextLoadTimeWeaver 類,它會嘗試裝飾一個自動檢測到的 LoadTimeWeaver。“自動檢測到”的 LoadTimeWeaver 的確切型別取決於您的執行時環境。下表總結了各種 LoadTimeWeaver 實現:

表 1. DefaultContextLoadTimeWeaver LoadTimeWeaver 實現
執行時環境 LoadTimeWeaver 實現

Apache Tomcat 中執行

TomcatLoadTimeWeaver

GlassFish 中執行 (僅限於 EAR 部署)

GlassFishLoadTimeWeaver

在 Red Hat 的 JBoss ASWildFly 中執行

JBossLoadTimeWeaver

JVM 使用 Spring InstrumentationSavingAgent 啟動 (java -javaagent:path/to/spring-instrument.jar)

InstrumentationLoadTimeWeaver

回退機制,期望底層 ClassLoader 遵循通用約定(即 addTransformer 和可選的 getThrowawayClassLoader 方法)

ReflectiveLoadTimeWeaver

請注意,該表僅列出了使用 DefaultContextLoadTimeWeaver 時自動檢測到的 LoadTimeWeaver 實現。您可以精確指定要使用的 LoadTimeWeaver 實現。

要配置特定的 LoadTimeWeaver,請實現 LoadTimeWeavingConfigurer 介面並覆蓋 getLoadTimeWeaver() 方法(或使用等效的 XML 配置)。以下示例指定了一個 ReflectiveLoadTimeWeaver

  • Java

  • Kotlin

  • Xml

@Configuration
@EnableLoadTimeWeaving
public class CustomWeaverConfiguration implements LoadTimeWeavingConfigurer {

	@Override
	public LoadTimeWeaver getLoadTimeWeaver() {
		return new ReflectiveLoadTimeWeaver();
	}
}
@Configuration
@EnableLoadTimeWeaving
class CustomWeaverConfiguration : LoadTimeWeavingConfigurer {

	override fun getLoadTimeWeaver(): LoadTimeWeaver {
		return ReflectiveLoadTimeWeaver()
	}
}
<beans xmlns="http://www.springframework.org/schema/beans"
	   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	   xmlns:context="http://www.springframework.org/schema/context"
	   xsi:schemaLocation="
		http://www.springframework.org/schema/beans
		https://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context
		https://www.springframework.org/schema/context/spring-context.xsd">

	<context:load-time-weaver
			weaver-class="org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver"/>

</beans>

由配置定義和註冊的 LoadTimeWeaver 稍後可以使用熟知的名稱 loadTimeWeaver 從 Spring 容器中檢索到。請記住,LoadTimeWeaver 僅作為 Spring LTW 基礎設施新增一個或多個 ClassFileTransformers 的機制而存在。實際進行 LTW 的 ClassFileTransformerClassPreProcessorAgentAdapter 類(來自 org.aspectj.weaver.loadtime 包)。有關 ClassPreProcessorAgentAdapter 類的詳細資訊,請參閱其類級別的 javadoc 文件,因為實際織入方式的細節超出了本文件的範圍。

還有最後一個配置屬性需要討論:aspectjWeaving 屬性(如果您使用 XML,則是 aspectj-weaving)。此屬性控制 LTW 是否啟用。它接受三個可能的值之一,如果屬性不存在,則預設值為 autodetect。下表總結了這三個可能的值:

表 2. AspectJ weaving 屬性值
註解值 XML 值 說明

ENABLED

on

AspectJ weaving 已開啟,並在載入時按需織入切面。

DISABLED

off

LTW 已關閉。在載入時不會織入任何切面。

AUTODETECT

autodetect

如果 Spring LTW 基礎設施能夠找到至少一個 META-INF/aop.xml 檔案,則 AspectJ weaving 開啟。否則,關閉。這是預設值。

特定環境配置

這最後一節包含在應用程式伺服器和 Web 容器等環境中使用 Spring LTW 支援時所需的任何額外設定和配置。

Tomcat, JBoss, WildFly

Tomcat 和 JBoss/WildFly 提供了一種通用的應用程式 ClassLoader,能夠進行本地 instrumentation。Spring 的原生 LTW 可以利用這些 ClassLoader 實現來提供 AspectJ weaving。您可以簡單地啟用載入時織入,如前文所述。具體來說,您不需要修改 JVM 啟動指令碼來新增 -javaagent:path/to/spring-instrument.jar

請注意,在 JBoss 上,您可能需要停用應用程式伺服器掃描,以防止它在應用程式實際啟動之前載入類。一個快速的變通方法是向您的 artifact 新增一個名為 WEB-INF/jboss-scanning.xml 的檔案,內容如下:

<scanning xmlns="urn:jboss:scanning:1.0"/>

通用 Java 應用程式

當在特定 LoadTimeWeaver 實現不支援的環境中需要類 instrumentation 時,JVM agent 是通用的解決方案。對於這種情況,Spring 提供了 InstrumentationLoadTimeWeaver,它需要一個 Spring 特定的(但非常通用的)JVM agent,即 spring-instrument.jar,該 agent 會被常見的 @EnableLoadTimeWeaving<context:load-time-weaver/> 設定自動檢測到。

要使用它,您必須使用 Spring agent 啟動虛擬機器,提供以下 JVM 選項:

-javaagent:/path/to/spring-instrument.jar

請注意,這需要修改 JVM 啟動指令碼,這可能會阻止您在應用程式伺服器環境中使用此方法(取決於您的伺服器和操作策略)。儘管如此,對於每個 JVM 一個應用程式的部署,例如獨立的 Spring Boot 應用程式,您通常無論如何都會控制整個 JVM 設定。