在 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.