在 Spring 應用中使用 AspectJ

到目前為止,本章所涵蓋的一切都是純粹的 Spring AOP。在本節中,如果你的需求超出了 Spring AOP 獨自提供的功能,我們將探討如何使用 AspectJ 編譯器或織入器來替代或補充 Spring AOP。

Spring 附帶了一個小的 AspectJ 切面庫,可在你的發行版中作為 spring-aspects.jar 獨立使用。你需要將其新增到你的 classpath 中才能使用其中的切面。使用 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 定義(通常是原型作用域的)來配置帶註解型別(在本例中為 Account)的新例項。由於透過 XML 定義的 bean 的預設名稱是其型別的完全限定名,因此宣告原型定義的一種便捷方法是省略 id 屬性,如下例所示:

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

如果你想明確指定要使用的原型 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 織入器進行織入。你可以使用構建時 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 配置器切面啟用 @Configurable 處理。特別要確保不要在已註冊為容器常規 Spring bean 的 bean 類上使用 @Configurable。這樣做會導致雙重初始化,一次透過容器,一次透過切面。

單元測試 @Configurable 物件

@Configurable 支援的目標之一是實現領域物件的獨立單元測試,而無需處理與硬編碼查詢相關的困難。如果 @Configurable 型別未被 AspectJ 織入,則該註解在單元測試期間不起作用。你可以設定被測物件中的模擬或存根屬性引用,並正常進行。如果 @Configurable 型別已被 AspectJ 織入,你仍然可以像往常一樣在容器外部進行單元測試,但每次構建 @Configurable 物件時都會看到一條警告訊息,指示它尚未由 Spring 配置。

使用多個應用程式上下文

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

考慮一個典型的 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 僅新增到容器範圍的 classpath 中(因此由共享的父 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 切面的原始碼。例如,以下摘錄顯示了你如何編寫一個切面,使用與完全限定類名匹配的原型 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" 屬性。

非單例切面更難配置。但是,可以透過建立原型 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(完全不涉及 Spring)配置 LTW 的完整詳細資訊,請參閱 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 織入器我們希望將 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,這是事實。但是,對於此示例,我們使用(Spring 提供的)Java 代理來開啟 LTW。我們使用以下命令執行前面顯示的 Main 類:

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

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

Main 程式的執行輸出如下一個示例所示。(我已在 calculateEntitlement() 實現中引入了 Thread.sleep(..) 語句,以便分析器實際捕獲的不僅僅是 0 毫秒(01234 毫秒不是 AOP 引入的開銷)。以下清單顯示了我們執行分析器時得到的輸出:

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 基礎設施透過使用 Java classpath 上(直接或更常見地在 jar 檔案中)的一個或多個 META-INF/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>

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

此檔案的結構和內容在 AspectJ 參考文件 的 LTW 部分中詳細說明。因為 aop.xml 檔案是 100% AspectJ,所以我們在此處不再贅述。

所需庫 (JARs)

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

  • spring-aop.jar

  • aspectjweaver.jar

如果你使用 Spring 提供的代理來啟用 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 LoadTimeWeavers
執行時環境 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 時自動檢測到的 LoadTimeWeavers。你可以指定要使用的具體 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 以後可以從 Spring 容器中使用眾所周知的名稱 loadTimeWeaver 檢索。請記住,LoadTimeWeaver 僅作為 Spring 的 LTW 基礎設施新增一個或多個 ClassFileTransformers 的機制而存在。實際執行 LTW 的 ClassFileTransformerClassPreProcessorAgentAdapter(來自 org.aspectj.weaver.loadtime 包)類。有關更多詳細資訊,請參閱 ClassPreProcessorAgentAdapter 類的類級別 javadoc,因為織入實際如何實現的細節超出了本文件的範圍。

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

表 2. AspectJ 織入屬性值
註解值 XML 值 解釋

ENABLED

on

AspectJ 織入已開啟,切面在載入時根據需要進行織入。

DISABLED

off

LTW 已關閉。沒有切面在載入時被織入。

AUTODETECT

autodetect

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

特定環境配置

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

Tomcat, JBoss, WildFly

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

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

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

通用 Java 應用程式

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

要使用它,你必須透過提供以下 JVM 選項來使用 Spring 代理啟動虛擬機器:

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

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

© . This site is unofficial and not affiliated with VMware.