在 Spring 应用程序中使用 AspectJ
本章到目前为止我们所讨论的内容都是纯 Spring AOP。在本节中,我们将探讨如何使用 AspectJ 编译器或织入器,作为 Spring AOP 的替代或补充,以满足超出 Spring AOP 单独提供的功能的需求。
Spring 附带一个小型的 AspectJ 切面库,该库在您的发行版中作为 spring-aspects.jar
独立提供。您需要将其添加到您的类路径中,以便使用其中的切面。 使用 AspectJ 通过 Spring 进行领域对象的依赖注入 和 其他 Spring AspectJ 切面 讨论了该库的内容以及如何使用它。 通过使用 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
)。由于通过 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
beans 指定显式的基于注解的依赖注入(有关更多详细信息,请参见 Annotation-based Container Configuration)。
最后,您可以通过使用 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 编织器结合使用。您可以使用构建时的 Ant 或 Maven 任务来完成此操作(例如,请参见 AspectJ 开发环境指南)或加载时织入(请参见 Spring 框架中的 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>
除非您确实打算在运行时依赖 @Configurable
的语义,否则请不要通过 bean 配置器切面激活 @Configurable
处理。特别是,请确保您不在作为常规 Spring beans 注册到容器的 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
切面,以提供您自己的切点定义。有关更多信息,请参见 AbstractBeanConfigurerAspect
和 AbstractTransactionAspect
切面的源代码。作为示例,以下摘录展示了如何编写一个切面,以使用与完全限定类名匹配的原型 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>
注意
factory-method="aspectOf"
属性
非单例方面的配置更为复杂。然而,可以通过创建原型 bean 定义并使用 spring-aspects.jar
中的 @Configurable
支持来配置方面实例,一旦它们由 AspectJ 运行时创建了 bean。
如果您有一些 @AspectJ 切面希望与 AspectJ 织入(例如,使用加载时织入来处理领域模型类型),还有其他 @AspectJ 切面希望与 Spring AOP 一起使用,并且这些切面都在 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 运行时。
使用 AspectJ 在 Spring 框架中进行加载时织入
Load-time weaving (LTW) 指的是在 Java 虚拟机 (JVM) 加载应用程序的类文件时,将 AspectJ 方面织入这些类文件的过程。本节的重点是配置和使用 LTW,特别是在 Spring Framework 的上下文中。本节不是 LTW 的一般介绍。有关 LTW 的具体细节以及如何仅使用 AspectJ 配置 LTW(与 Spring 完全无关),请参见 AspectJ 开发环境指南的 LTW 部分。
Spring 框架为 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 示例应用程序。
第一个示例
假设你是一名应用程序开发人员,负责诊断系统中一些性能问题的原因。我们将启用一个简单的性能分析方面,而不是使用性能分析工具,这样可以快速获取一些性能指标。然后,我们可以在之后立即对该特定区域应用更细粒度的性能分析工具。
以下示例展示了分析方面,这并不华丽。它是一个基于时间的分析器,使用 @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()
}
我们还有最后一件事要做。本节的介绍确实提到,可以在每个 ClassLoader
的基础上选择性地开启 LTW,这一点是正确的。然而,在这个例子中,我们使用一个 Java agent(由 Spring 提供)来开启 LTW。我们使用以下命令来运行之前显示的 Main
类:
java -javaagent:C:/projects/xyz/lib/spring-instrument.jar com.xyz.Main
-javaagent
是一个标志,用于指定和启用 在 JVM 上运行的程序的代理进行插装。Spring 框架附带了这样的代理,InstrumentationSavingAgent
,它被打包在 spring-instrument.jar
中,该 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 beans。以下对 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,因此我们在此不再进一步描述。
所需库 (JARS)
至少,您需要以下库才能使用 Spring Framework 对 AspectJ LTW 的支持:
-
spring-aop.jar
-
aspectjweaver.jar
如果您使用 Spring 提供的代理来启用仪器化,您还需要:
spring-instrument.jar
Spring 配置
Spring 的 LTW 支持的关键组件是 LoadTimeWeaver
接口(位于 org.springframework.instrument.classloading
包中),以及随 Spring 发行版一起提供的众多实现。LoadTimeWeaver
负责在运行时将一个或多个 java.lang.instrument.ClassFileTransformers
添加到 ClassLoader
中,这为各种有趣的应用打开了大门,其中之一恰好是方面的 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,例如 LoadTimeWeaver
和 AspectJWeavingEnabler
。默认的 LoadTimeWeaver
是 DefaultContextLoadTimeWeaver
类,它试图装饰一个自动检测到的 LoadTimeWeaver
。被“自动检测”的 LoadTimeWeaver
的确切类型取决于您的运行时环境。下表总结了各种 LoadTimeWeaver
实现:
表 1. DefaultContextLoadTimeWeaver 加载时间织入器
运行环境 | LoadTimeWeaver 实现 |
---|---|
在 Apache Tomcat 中运行 | TomcatLoadTimeWeaver |
在 GlassFish 中运行(仅限于 EAR 部署) | GlassFishLoadTimeWeaver |
在 Red Hat 的 JBoss AS 或 WildFly 中运行 | 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
可以通过众所周知的名称 loadTimeWeaver
从 Spring 容器中检索。请记住,LoadTimeWeaver
仅作为 Spring 的 LTW 基础设施的机制,用于添加一个或多个 ClassFileTransformers
。实际执行 LTW 的 ClassFileTransformer
是 ClassPreProcessorAgentAdapter
(来自 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 编织是开启的。否则,它是关闭的。这是默认值。 |
环境特定配置
最后这一部分包含了在使用 Spring 的 LTW 支持时,在应用服务器和 Web 容器等环境中所需的任何额外设置和配置。
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 启动脚本,这可能会阻止您在应用服务器环境中使用此功能(具体取决于您的服务器和操作政策)。也就是说,对于每个 JVM 部署一个应用程序的情况,例如独立的 Spring Boot 应用程序,您通常可以在任何情况下控制整个 JVM 设置。