在 Spring 应用程序中使用 AspectJ

本章到目前为止介绍的所有内容都是纯 Spring AOP。在本节中,我们将探讨如何使用 AspectJ 编译器或编织器来代替或补充 Spring AOP,以满足 Spring AOP 本身无法提供的功能。

Spring 附带了一个小型 AspectJ 方面库,该库在您的发行版中以 spring-aspects.jar 的形式独立提供。您需要将它添加到您的类路径中才能使用其中的方面。 使用 AspectJ 通过 Spring 依赖注入域对象其他用于 AspectJ 的 Spring 方面 讨论了该库的内容以及如何使用它。 使用 Spring IoC 配置 AspectJ 方面 讨论了如何依赖注入使用 AspectJ 编译器编织的 AspectJ 方面。最后, 在 Spring 框架中使用 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)新实例。由于 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 框架中使用 AspectJ 的加载时织入)。`AnnotationBeanConfigurerAspect` 本身需要由 Spring 配置(以便获取对将用于配置新对象的 bean 工厂的引用)。如果您使用基于 Java 的配置,则可以将 `@EnableSpringConfigured` 添加到任何 `@Configuration` 类中,如下所示

  • Java

  • Kotlin

@Configuration
@EnableSpringConfigured
public class AppConfig {
}
@Configuration
@EnableSpringConfigured
class AppConfig {
}

如果您更喜欢基于 XML 的配置,则 Spring context 命名空间 定义了一个方便的 `context:spring-configured` 元素,您可以按如下方式使用它

<context:spring-configured/>

在方面配置之前创建的 `@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 的位置。

考虑一个典型的 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 框架的事务支持的用户。

解释@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 框架中使用 AspectJ 进行加载时编织

加载时编织 (LTW) 指的是将 AspectJ 方面编织到应用程序的类文件中的过程,这些类文件正在被加载到 Java 虚拟机 (JVM) 中。本节的重点是在 Spring 框架的特定上下文中配置和使用 LTW。本节不是对 LTW 的一般介绍。有关 LTW 的具体细节以及仅使用 AspectJ(Spring 完全不参与)配置 LTW 的完整信息,请参阅 AspectJ 开发环境指南的 LTW 部分

Spring 框架为 AspectJ LTW 带来的价值在于能够对编织过程进行更细粒度的控制。'Vanilla' 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 示例,然后详细介绍示例中介绍的元素。有关完整示例,请参阅 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 类路径上存在名为 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 转储文件和警告。从效率的角度来看,这也是最佳实践。

现在我们可以继续进行 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 代理(随 Spring 提供)来打开 LTW。我们使用以下命令运行前面显示的 Main 类。

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

-javaagent 是一个标志,用于指定和启用 代理来检测在 JVM 上运行的程序。Spring 框架附带了这样一个代理,即 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方面。此外,编译后的方面类需要在类路径上可用。

META-INF/aop.xml

AspectJ LTW基础设施是通过使用一个或多个位于Java类路径上的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 转储文件和警告。从效率的角度来看,这也是最佳实践。

此文件的结构和内容在AspectJ参考文档的LTW部分中详细介绍。由于aop.xml文件是100%的AspectJ,因此我们在此不再赘述。

所需库(JAR)

至少,您需要以下库才能使用Spring Framework对AspectJ LTW的支持

  • spring-aop.jar

  • aspectjweaver.jar

如果您使用Spring提供的代理来启用检测,您还需要

  • 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 框架的 LTW 支持,您需要配置一个 `LoadTimeWeaver`,这通常通过使用 `@EnableLoadTimeWeaving` 注解来完成,如下所示

  • Java

  • Kotlin

@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}
@Configuration
@EnableLoadTimeWeaving
class AppConfig {
}

或者,如果您更喜欢基于 XML 的配置,请使用 `<context:load-time-weaver/>` 元素。请注意,该元素在 `context` 命名空间中定义。以下示例展示了如何使用 `<context:load-time-weaver/>`

<?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">

	<context:load-time-weaver/>

</beans>

前面的配置会自动为您定义和注册许多 LTW 特定的基础设施 bean,例如 `LoadTimeWeaver` 和 `AspectJWeavingEnabler`。默认的 `LoadTimeWeaver` 是 `DefaultContextLoadTimeWeaver` 类,它尝试装饰自动检测到的 `LoadTimeWeaver`。自动检测到的 `LoadTimeWeaver` 的确切类型取决于您的运行时环境。下表总结了各种 `LoadTimeWeaver` 实现

表 1. DefaultContextLoadTimeWeaver LoadTimeWeavers
运行时环境 `LoadTimeWeaver` 实现

Apache Tomcat 中运行

TomcatLoadTimeWeaver

GlassFish 中运行(仅限于 EAR 部署)

GlassFishLoadTimeWeaver

在 Red Hat 的 JBoss ASWildFly 中运行

JBossLoadTimeWeaver

使用 Spring `InstrumentationSavingAgent` 启动 JVM(`java -javaagent:path/to/spring-instrument.jar`)

InstrumentationLoadTimeWeaver

回退,期望底层 ClassLoader 遵循通用约定(即 `addTransformer` 以及可选的 `getThrowawayClassLoader` 方法)

ReflectiveLoadTimeWeaver

请注意,该表仅列出了使用 `DefaultContextLoadTimeWeaver` 时自动检测到的 `LoadTimeWeavers`。您可以指定要使用的确切 `LoadTimeWeaver` 实现。

要使用 Java 配置指定特定的 `LoadTimeWeaver`,请实现 `LoadTimeWeavingConfigurer` 接口并覆盖 `getLoadTimeWeaver()` 方法。以下示例指定了一个 `ReflectiveLoadTimeWeaver`

  • Java

  • Kotlin

@Configuration
@EnableLoadTimeWeaving
public class AppConfig implements LoadTimeWeavingConfigurer {

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

	override fun getLoadTimeWeaver(): LoadTimeWeaver {
		return ReflectiveLoadTimeWeaver()
	}
}

如果您使用基于 XML 的配置,则可以将完全限定的类名指定为 `<context:load-time-weaver/>` 元素的 `weaver-class` 属性的值。同样,以下示例指定了一个 `ReflectiveLoadTimeWeaver`

<?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">

	<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 编织属性值
注解值 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,它能够进行本地检测。Spring 的原生 LTW 可以利用这些 ClassLoader 实现来提供 AspectJ 编织。您可以简单地启用加载时编织,如前面所述。具体来说,您不需要修改 JVM 启动脚本以添加-javaagent:path/to/spring-instrument.jar

请注意,在 JBoss 上,您可能需要禁用应用程序服务器扫描以防止它在应用程序实际启动之前加载类。一个简单的解决方法是在您的工件中添加一个名为WEB-INF/jboss-scanning.xml的文件,其内容如下

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

通用 Java 应用程序

当在不受特定LoadTimeWeaver实现支持的环境中需要类检测时,JVM 代理是通用的解决方案。对于这种情况,Spring 提供了InstrumentationLoadTimeWeaver,它需要一个特定于 Spring(但非常通用)的 JVM 代理,spring-instrument.jar,由常见的@EnableLoadTimeWeaving<context:load-time-weaver/>设置自动检测。

要使用它,您必须使用以下 JVM 选项启动带有 Spring 代理的虚拟机

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

请注意,这需要修改 JVM 启动脚本,这可能会阻止您在应用程序服务器环境中使用它(取决于您的服务器和操作策略)。也就是说,对于诸如独立 Spring Boot 应用程序之类的每个 JVM 部署一个应用程序,您通常在任何情况下都控制整个 JVM 设置。