对于最新稳定版本,请使用 Spring Framework 7.0.6spring-doc.cadn.net.cn

在 Spring 应用程序中使用 AspectJ

到目前为止,本章所涵盖的所有内容都是纯 Spring AOP。在本节中,我们将探讨如果你的需求超出了 Spring AOP 单独提供的功能范围,如何使用 AspectJ 编译器或织入器来替代或补充 Spring AOP。spring-doc.cadn.net.cn

Spring 自带了一个小型的 AspectJ 切面库,在您的发行包中以独立的 spring-aspects.jar 形式提供。您需要将其添加到类路径中,才能使用其中的切面。使用 AspectJ 通过 Spring 对领域对象进行依赖注入适用于 AspectJ 的其他 Spring 切面 介绍了该库的内容以及如何使用它。使用 Spring IoC 配置 AspectJ 切面 讨论了如何对使用 AspectJ 编译器织入的 AspectJ 切面进行依赖注入。最后,在 Spring 框架中使用 AspectJ 进行加载时织入 为使用 AspectJ 的 Spring 应用程序提供了加载时织入(Load-time Weaving)的入门介绍。spring-doc.cadn.net.cn

使用 AspectJ 通过 Spring 对领域对象进行依赖注入

Spring 容器会实例化并配置在您的应用程序上下文中定义的 bean。此外,也可以要求 bean 工厂根据某个包含所需配置的 bean 定义名称,来配置一个已存在的对象。spring-aspects.jar 包含一个注解驱动的切面(aspect),它利用这一功能,允许对任意对象进行依赖注入。该支持旨在用于那些在任何容器控制范围之外创建的对象。领域对象(Domain objects)通常就属于这一类,因为它们通常是通过 new 操作符以编程方式创建的,或者由 ORM 工具在执行数据库查询后创建的。spring-doc.cadn.net.cn

@Configurable 注解用于将一个类标记为可接受 Spring 驱动的配置。在最简单的情况下,你可以仅将其用作一个标记注解,如下例所示:spring-doc.cadn.net.cn

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 会使用一个与该注解类型(本例中为 Account)完全限定类名(com.xyz.domain.Account)同名的 bean 定义(通常为 prototype 作用域)来配置该类型的全新实例。由于 bean 的默认名称就是其类型的完全限定名,因此声明该 prototype 定义的一种便捷方式是省略 id 属性,如下例所示:spring-doc.cadn.net.cn

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

如果你想显式指定要使用的原型(prototype)bean 定义的名称,可以直接在注解中进行指定,如下例所示:spring-doc.cadn.net.cn

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 实例。spring-doc.cadn.net.cn

您还可以使用自动装配来完全避免指定专用的 Bean 定义。要让 Spring 应用自动装配,请使用 autowire 注解的 @Configurable 属性。您可以分别指定 @Configurable(autowire=Autowire.BY_TYPE)@Configurable(autowire=Autowire.BY_NAME) 以按类型或按名称进行自动装配。作为替代方案,更推荐通过 @Autowired@Inject 在字段或方法级别为您的 @Configurable Bean 指定明确的、基于注解的依赖注入(详见 基于注解的容器配置)。spring-doc.cadn.net.cn

最后,你可以通过使用 dependencyCheck 属性(例如,@Configurable(autowire=Autowire.BY_NAME,dependencyCheck=true))来为新创建并已配置的对象中的对象引用启用 Spring 的依赖项检查。如果该属性设置为 true,Spring 会在配置完成后验证所有属性(非基本类型或集合类型的属性)是否都已被设置。spring-doc.cadn.net.cn

请注意,单独使用该注解本身不会产生任何效果。真正对注解的存在作出响应的是 AnnotationBeanConfigurerAspect 中的 spring-aspects.jar。本质上,该切面表达的意思是:“在完成一个带有 @Configurable 注解类型的新对象的初始化之后,根据该注解的属性,使用 Spring 对新创建的对象进行配置”。在此上下文中,“初始化”既指新实例化的对象(例如,通过 new 操作符创建的对象),也指正在经历反序列化的 Serializable 对象(例如,通过 readResolve() 方法)。spring-doc.cadn.net.cn

上文中的一个关键词是“本质上”。在大多数情况下,“在新对象初始化完成后”这一表述的确切语义是合适的。在此上下文中,“初始化之后”意味着依赖项是在对象构造完成之后才被注入的。这意味着在类的构造函数体中无法使用这些依赖项。如果你希望在构造函数体执行之前就注入依赖项,从而使其在构造函数体内可用,那么你需要在@Configurable声明中进行如下定义:spring-doc.cadn.net.cn

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

您可以在《AspectJ 编程指南》的附录中找到有关 AspectJ 各种切入点类型语言语义的更多信息。spring-doc.cadn.net.cn

要使此功能正常工作,带注解的类型必须经过 AspectJ 编织器(weaver)进行编织。你可以使用构建时的 Ant 或 Maven 任务来完成此操作(例如,参见AspectJ 开发环境指南),也可以使用加载时编织(参见Spring 框架中的 AspectJ 加载时编织)。AnnotationBeanConfigurerAspect 本身需要由 Spring 进行配置(以便获取用于配置新对象的 bean 工厂引用)。如果你使用基于 Java 的配置,可以在任意 @EnableSpringConfigured 类上添加 @Configuration 注解,如下所示:spring-doc.cadn.net.cn

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

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

<context:spring-configured/>

在切面(aspect)配置完成之前创建的 @Configurable 对象实例, 会导致向调试日志中输出一条消息,并且该对象不会被配置。 一个典型的例子是 Spring 配置中的某个 Bean,在 Spring 初始化它时会创建领域对象。 在这种情况下,你可以使用 depends-on Bean 属性来手动指定该 Bean 依赖于配置切面。 以下示例展示了如何使用 depends-on 属性:spring-doc.cadn.net.cn

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

	<!-- ... -->

</bean>
除非你确实打算在运行时依赖其语义,否则不要通过 bean 配置器切面(bean configurer aspect)激活 @Configurable 处理。特别要注意的是,请确保不要在那些已作为常规 Spring bean 注册到容器中的 bean 类上使用 @Configurable。这样做会导致重复初始化:一次通过容器,另一次通过切面。

单元测试@Configurable对象

@Configurable 支持的目标之一是使领域对象能够进行独立的单元测试,而无需面对硬编码查找所带来的困难。 如果 @Configurable 类型尚未被 AspectJ 织入,则该注解在单元测试期间不会产生任何影响。 你可以在被测试的对象中设置模拟(mock)或桩(stub)属性引用,并像平常一样进行测试。 如果 @Configurable 类型已被 AspectJ 织入,你仍然可以像平常一样在容器外部进行单元测试, 但每次创建 @Configurable 对象时,都会看到一条警告消息,提示该对象尚未被 Spring 配置。spring-doc.cadn.net.cn

使用多个应用上下文

用于实现 AnnotationBeanConfigurerAspect 支持的 @Configurable 是一个 AspectJ 单例切面。单例切面的作用域与 static 成员的作用域相同:每个定义该类型的 ClassLoader 都只有一个切面实例。 这意味着,如果你在同一个 ClassLoader 层次结构中定义了多个应用上下文,则需要考虑在哪里定义 @EnableSpringConfigured Bean,以及将 spring-aspects.jar 放置在类路径的什么位置。spring-doc.cadn.net.cn

考虑一个典型的 Spring Web 应用程序配置:它具有一个共享的父应用上下文,用于定义通用的业务服务以及支持这些服务所需的一切组件,同时为每个 Servlet 配置一个子应用上下文(其中包含该 Servlet 特有的 Bean 定义)。所有这些上下文都共存于同一个 ClassLoader 层次结构中,因此 AnnotationBeanConfigurerAspect 只能持有对其中一个上下文的引用。 在这种情况下,我们建议在共享的(父)应用上下文中定义 @EnableSpringConfigured Bean。这样可以定义你可能希望注入到领域对象中的服务。其结果是,你无法通过 @Configurable 机制将领域对象配置为引用子上下文(即 Servlet 特有上下文)中定义的 Bean(这通常也不是你想要做的事情)。spring-doc.cadn.net.cn

在同一容器中部署多个 Web 应用程序时,请确保每个 Web 应用程序都通过其自身的 spring-aspects.jar 加载 ClassLoader 中的类型(例如,将 spring-aspects.jar 放置在 WEB-INF/lib 目录下)。如果仅将 spring-aspects.jar 添加到容器级别的类路径中(从而由共享的父 ClassLoader 加载),则所有 Web 应用程序将共享同一个切面实例(这很可能不是您期望的行为)。spring-doc.cadn.net.cn

面向 AspectJ 的其他 Spring 方面

除了 @Configurable 切面之外,spring-aspects.jar 还包含一个 AspectJ 切面,可用于为使用 @Transactional 注解标注的类型和方法驱动 Spring 的事务管理。这主要是为那些希望在 Spring 容器之外使用 Spring 框架事务支持的用户而设计的。spring-doc.cadn.net.cn

解释 @Transactional 注解的切面是 AnnotationTransactionAspect。当您使用此切面时,必须在实现类(或该类中的方法,或两者)上添加注解,而不是在该类所实现的接口(如果有的话)上添加注解。AspectJ 遵循 Java 的规则,即接口上的注解不会被继承。spring-doc.cadn.net.cn

类上的 @Transactional 注解为该类中任何公共方法的执行指定了默认的事务语义。spring-doc.cadn.net.cn

类中方法上的 @Transactional 注解会覆盖类级别注解(如果存在)所指定的默认事务语义。任何可见性的方法都可以添加注解,包括私有方法。直接对非公共方法进行注解是为这类方法的执行获得事务边界控制的唯一方式。spring-doc.cadn.net.cn

从 Spring Framework 4.2 起,spring-aspects 提供了一个类似的切面,为标准的 jakarta.transaction.Transactional 注解提供了完全相同的功能。更多详情请参阅 JtaAnnotationTransactionAspect

对于希望使用 Spring 的配置和事务管理支持,但不想(或不能)使用注解的 AspectJ 程序员来说,spring-aspects.jar 还包含了一些 abstract 切面,你可以通过继承这些切面来提供自己的切入点(pointcut)定义。有关更多信息,请参阅 AbstractBeanConfigurerAspectAbstractTransactionAspect 切面的源代码。例如,以下代码片段展示了如何编写一个切面,通过使用与完全限定类名匹配的原型(prototype)bean 定义,来配置领域模型中定义的所有对象实例:spring-doc.cadn.net.cn

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 子句)。spring-doc.cadn.net.cn

大多数 AspectJ 切面都是单例切面。这些切面的配置非常简单。您可以像平常一样创建一个引用切面类型的 bean 定义,并包含 factory-method="aspectOf" bean 属性。这样可确保 Spring 通过向 AspectJ 请求来获取切面实例,而不是尝试自行创建实例。以下示例展示了如何使用 factory-method="aspectOf" 属性:spring-doc.cadn.net.cn

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

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

非单例的切面更难配置。然而,可以通过创建原型(prototype)bean定义,并使用 @Configurable 中的 spring-aspects.jar 支持,在 AspectJ 运行时创建切面实例后对其进行配置。spring-doc.cadn.net.cn

如果你有一些 @AspectJ 切面希望使用 AspectJ 进行织入(例如,对领域模型类型使用加载时织入),同时还有其他 @AspectJ 切面希望与 Spring AOP 一起使用,并且所有这些切面都在 Spring 中进行了配置,那么你需要告知 Spring AOP 的 @AspectJ 自动代理支持机制:在配置中定义的 @AspectJ 切面中,究竟哪一部分子集应当用于自动代理。你可以在 <include/> 声明内部使用一个或多个 <aop:aspectj-autoproxy/> 元素来实现这一点。每个 <include/> 元素指定一个名称模式,只有至少匹配其中一个模式的 bean 才会被用于 Spring AOP 的自动代理配置。以下示例展示了如何使用 <include/> 元素:spring-doc.cadn.net.cn

<aop:aspectj-autoproxy>
	<aop:include name="thisBean"/>
	<aop:include name="thatBean"/>
</aop:aspectj-autoproxy>
不要被 <aop:aspectj-autoproxy/> 元素的名称所误导。使用它会创建 Spring AOP 代理。此处采用的是 @AspectJ 风格的切面声明方式,但并未涉及 AspectJ 运行时。

Spring 框架中使用 AspectJ 的加载时织入

加载时织入(Load-time weaving,LTW)是指在应用程序的类文件被加载到 Java 虚拟机(JVM)时,将 AspectJ 切面织入这些类文件的过程。 本节重点介绍在 Spring 框架特定上下文中配置和使用 LTW。本节并非对 LTW 的一般性介绍。有关 LTW 的详细信息,以及仅使用 AspectJ(完全不涉及 Spring)来配置 LTW 的具体方法,请参阅 AspectJ 开发环境指南中的 LTW 章节spring-doc.cadn.net.cn

Spring Framework 为 AspectJ LTW(加载时织入)带来的价值在于能够对织入过程实现更细粒度的控制。“原生”的 AspectJ LTW 通过使用一个 Java(5+)代理来实现,该代理在启动 JVM 时通过指定一个虚拟机参数来启用。因此,这是一种 JVM 范围内的全局设置,在某些情况下可能适用,但通常粒度过于粗糙。而 Spring 支持的 LTW 允许你以每个 ClassLoader 为基础来启用 LTW,这种控制更加精细,在“单 JVM 多应用”环境(例如典型的应用服务器环境)中也更为合理。spring-doc.cadn.net.cn

此外,在某些环境中,这种支持使得无需修改应用服务器的启动脚本(该脚本通常需要添加 -javaagent:path/to/aspectjweaver.jar 或(如本节稍后所述)-javaagent:path/to/spring-instrument.jar)即可启用加载时织入(load-time weaving)。开发者通过配置应用程序上下文来启用加载时织入,而无需依赖通常负责部署配置(例如启动脚本)的管理员。spring-doc.cadn.net.cn

现在推销部分已经结束,让我们首先通过一个使用 Spring 的 AspectJ 加载时织入(LTW)的快速示例,然后详细介绍该示例中引入的各个元素。完整的示例,请参见Petclinic 示例应用程序spring-doc.cadn.net.cn

第一个示例

假设你是一名应用程序开发人员,任务是诊断系统中某些性能问题的根本原因。我们不会立即使用专业的性能分析工具,而是先启用一个简单的性能监控切面(profiling aspect),以便快速获取一些性能指标。随后,我们可以立即在该特定区域应用更细粒度的性能分析工具。spring-doc.cadn.net.cn

此处展示的示例使用了 XML 配置。您也可以通过Java 配置来配置和使用 @AspectJ。具体来说,您可以使用@EnableLoadTimeWeaving注解作为<context:load-time-weaver/>的替代方案(详见下文)。

以下示例展示了该性能分析切面,它并不复杂。 这是一个基于时间的性能分析器,使用了 @AspectJ 风格的切面声明方式:spring-doc.cadn.net.cn

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 类路径下存在名为 META-INF/aop.xml 的文件(或多个文件)——是 AspectJ 的标准做法。以下示例展示了该 aop.xml 文件:spring-doc.cadn.net.cn

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

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

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

</aspectj>

现在我们可以继续进行 Spring 特定部分的配置了。我们需要配置一个 LoadTimeWeaver(稍后会详细解释)。这个加载时织入器(load-time weaver)是核心组件,负责将一个或多个 META-INF/aop.xml 文件中的切面配置织入到应用程序的类中。好消息是,它不需要大量配置(尽管你还可以指定一些其他选项,但这些内容将在后面详细介绍),如下例所示:spring-doc.cadn.net.cn

<?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)的实际效果:spring-doc.cadn.net.cn

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 类:spring-doc.cadn.net.cn

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

-javaagent 是一个用于指定和启用代理(agents)以对在 JVM 上运行的程序进行字节码增强(instrumentation)的标志。Spring 框架自带了这样一个代理,即 InstrumentationSavingAgent,它被打包在 spring-instrument.jar 中,如前例所示,该 JAR 文件被作为 -javaagent 参数的值提供。spring-doc.cadn.net.cn

执行Main程序的输出结果类似于下面的示例。 (我在Thread.sleep(..)的实现中加入了一条calculateEntitlement()语句, 以便分析器实际捕获到的执行时间不为0毫秒 (这里的01234毫秒并非由AOP引入的开销)。 以下列表展示了我们运行分析器时得到的输出:spring-doc.cadn.net.cn

Calculating entitlement

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

由于这种LTW(加载时织入)是通过使用完整的AspectJ实现的,因此我们不仅限于对Spring Bean进行通知。下面对Main程序稍作修改,即可得到相同的结果:spring-doc.cadn.net.cn

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 的新实例。然而,性能分析通知(profiling advice)仍然被织入其中。spring-doc.cadn.net.cn

诚然,这个示例过于简单。然而,Spring 中 LTW(加载时织入)支持的基本要素已在前面的示例中全部介绍过,本节其余部分将详细解释每项配置和用法背后的“原因”。spring-doc.cadn.net.cn

本示例中使用的 ProfilingAspect 可能比较简单,但却非常实用。这是一个很好的开发期切面示例,开发者可以在开发过程中使用它,然后在构建部署到 UAT(用户验收测试)或生产环境的应用程序时轻松地将其排除。

切面

你在LTW(加载时织入)中使用的切面必须是AspectJ切面。你可以直接使用AspectJ语言编写这些切面,也可以采用@AspectJ风格来编写。这样编写的切面既是有效的AspectJ切面,也是有效的Spring AOP切面。 此外,编译后的切面类需要在类路径(classpath)中可用。spring-doc.cadn.net.cn

'META-INF/aop.xml'

AspectJ LTW 基础设施通过使用一个或多个位于 Java 类路径上的 META-INF/aop.xml 文件进行配置(这些文件可直接位于类路径中,但更常见的是打包在 JAR 文件中)。spring-doc.cadn.net.cn

该文件的结构和内容在AspectJ 参考文档的 LTW(加载时织入)部分中有详细说明。由于 aop.xml 文件完全是 AspectJ 的内容,因此我们在此不再赘述。spring-doc.cadn.net.cn

所需库(JARS)

要使用 Spring 框架对 AspectJ LTW(加载时织入)的支持,至少需要以下库:spring-doc.cadn.net.cn

Spring 配置

Spring 对加载时织入(LTW)支持的关键组件是 LoadTimeWeaver 接口(位于 org.springframework.instrument.classloading 包中),以及 Spring 发行版中附带的多种该接口的实现。一个 LoadTimeWeaver 负责在运行时向 java.lang.instrument.ClassFileTransformers 添加一个或多个 ClassLoader,从而为各种有趣的应用打开大门,其中之一便是切面的加载时织入(LTW)。spring-doc.cadn.net.cn

如果你不熟悉运行时类文件转换的概念,请在继续阅读之前先查看 java.lang.instrument 包的 Javadoc API 文档。 尽管该文档并不全面,但至少你可以看到关键的接口和类(供你在阅读本节内容时参考)。

为特定的LoadTimeWeaver配置一个ApplicationContext可能只需添加一行代码即可。(请注意,您几乎肯定需要使用ApplicationContext作为您的Spring容器——通常,仅使用BeanFactory是不够的,因为LTW支持依赖于BeanFactoryPostProcessors。)spring-doc.cadn.net.cn

要启用 Spring 框架的加载时织入(LTW)支持,您需要配置一个 LoadTimeWeaver, 通常通过使用 @EnableLoadTimeWeaving 注解来完成,如下所示:spring-doc.cadn.net.cn

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

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

<?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,例如 LoadTimeWeaverAspectJWeavingEnabler。 默认的 LoadTimeWeaverDefaultContextLoadTimeWeaver 类,它会尝试对一个自动检测到的 LoadTimeWeaver 进行封装。 所“自动检测”到的 LoadTimeWeaver 的具体类型取决于您的运行环境。 下表总结了各种 LoadTimeWeaver 的实现:spring-doc.cadn.net.cn

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

Apache Tomcat 中运行spring-doc.cadn.net.cn

TomcatLoadTimeWeaverspring-doc.cadn.net.cn

GlassFish 中运行(仅限 EAR 部署)spring-doc.cadn.net.cn

GlassFishLoadTimeWeaverspring-doc.cadn.net.cn

在 Red Hat 的 JBoss ASWildFly 中运行spring-doc.cadn.net.cn

JBossLoadTimeWeaverspring-doc.cadn.net.cn

JVM 使用 Spring InstrumentationSavingAgent 启动 (java -javaagent:path/to/spring-instrument.jarspring-doc.cadn.net.cn

InstrumentationLoadTimeWeaverspring-doc.cadn.net.cn

回退机制,期望底层的 ClassLoader 遵循通用约定 (即包含 addTransformer 方法,以及可选的 getThrowawayClassLoader 方法)spring-doc.cadn.net.cn

ReflectiveLoadTimeWeaverspring-doc.cadn.net.cn

请注意,该表格仅列出在使用 LoadTimeWeavers 时自动检测到的 DefaultContextLoadTimeWeaver。您可以明确指定要使用的 LoadTimeWeaver 实现。spring-doc.cadn.net.cn

要通过 Java 配置指定特定的 LoadTimeWeaver,请实现 LoadTimeWeavingConfigurer 接口并重写 getLoadTimeWeaver() 方法。 以下示例指定了一个 ReflectiveLoadTimeWeaverspring-doc.cadn.net.cn

@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 的配置,可以将完全限定类名指定为 weaver-class 元素的 <context:load-time-weaver/> 属性值。同样,以下示例指定了一个 ReflectiveLoadTimeWeaverspring-doc.cadn.net.cn

<?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,因为这些细节超出了本文档的范围。spring-doc.cadn.net.cn

还有一个配置属性尚未讨论:aspectjWeaving 属性(如果你使用 XML,则为 aspectj-weaving)。该属性用于控制是否启用 LTW(加载时织入)。它可接受三个可能的值之一,如果未指定该属性,则默认值为 autodetect。下表总结了这三个可能的值:spring-doc.cadn.net.cn

表2. AspectJ织入属性值
注解值 XML 值 说明

ENABLEDspring-doc.cadn.net.cn

onspring-doc.cadn.net.cn

AspectJ 编织已启用,切面会在适当的时候进行加载时编织。spring-doc.cadn.net.cn

DISABLEDspring-doc.cadn.net.cn

offspring-doc.cadn.net.cn

LTW 已关闭。没有切面在加载时被织入。spring-doc.cadn.net.cn

AUTODETECTspring-doc.cadn.net.cn

autodetectspring-doc.cadn.net.cn

如果 Spring LTW 基础设施能够找到至少一个 META-INF/aop.xml 文件, 则启用 AspectJ 编织。否则,将禁用 AspectJ 编织。这是默认值。spring-doc.cadn.net.cn

环境特定配置

最后一节包含在应用服务器和Web容器等环境中使用Spring的LTW(加载时织入)支持时所需的任何额外设置和配置。spring-doc.cadn.net.cn

Tomcat, JBoss, WildFly

Tomcat 和 JBoss/WildFly 提供了一个通用的应用程序 ClassLoader,该加载器支持本地 instrumentation。Spring 的原生 LTW(加载时织入)可以利用这些 ClassLoader 实现来提供 AspectJ 织入。 您可以按照前面所述简单地启用加载时织入。 具体来说,您无需修改 JVM 启动脚本以添加 -javaagent:path/to/spring-instrument.jarspring-doc.cadn.net.cn

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

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

通用 Java 应用程序

当在特定 LoadTimeWeaver 实现不支持的环境中需要类织入(class instrumentation)时,JVM 代理(agent)是一种通用解决方案。 针对此类情况,Spring 提供了 InstrumentationLoadTimeWeaver,它需要一个 Spring 特定(但非常通用)的 JVM 代理 spring-instrument.jar,该代理会被常见的 @EnableLoadTimeWeaving 注解和 <context:load-time-weaver/> 配置自动检测到。spring-doc.cadn.net.cn

要使用它,您必须通过提供以下 JVM 选项来启动带有 Spring 代理的虚拟机:spring-doc.cadn.net.cn

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

请注意,这需要修改 JVM 启动脚本,可能会导致您无法在应用服务器环境中使用此功能(具体取决于您的服务器和运维策略)。不过,对于每个 JVM 只部署一个应用的场景(例如独立的 Spring Boot 应用),您通常无论如何都能完全控制整个 JVM 的配置。spring-doc.cadn.net.cn