在 Spring 应用中使用 AspectJ
本章到目前为止讨论的所有内容都是纯粹的 Spring AOP。在本节中,我们将探讨当您的需求超出 Spring AOP 本身提供的功能时,如何使用 AspectJ 编译器或 weaver 来替代或补充 Spring AOP。
Spring 附带了一个小的 AspectJ 切面库,它在您的发行版中作为 spring-aspects.jar
独立提供。您需要将其添加到类路径中才能使用其中的切面。使用 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 定义(通常是 prototype 作用域)来配置注解类型(在本例中为 Account
)的新实例。由于通过 XML 定义的 bean 的默认名称是其类型的完全限定名称,因此声明 prototype 定义的一种便捷方式是省略 id
属性,如下例所示
<bean class="com.xyz.domain.Account" scope="prototype">
<property name="fundsTransferService" ref="fundsTransferService"/>
</bean>
如果您想显式指定要使用的 prototype 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())。
上一段中的一个关键短语是“实质上”。在大多数情况下,“从新对象的初始化返回后”的确切语义是没问题的。在这种情况下,“初始化后”意味着依赖项在对象构造后注入。这意味着依赖项在类的构造函数体中不可用。如果您希望在构造函数体运行之前注入依赖项,以便它们在构造函数体中可用,您需要在
您可以在 AspectJ 编程指南 的 本附录 中找到有关 AspectJ 中各种切点类型的语言语义的更多信息。 |
要使其工作,带注解的类型必须通过 AspectJ weaver 进行织入。您可以使用构建时的 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 configurer 切面激活 @Configurable 处理。特别是,请确保不要在已注册为常规 Spring bean 的 bean 类上使用 @Configurable 。这样做会导致双重初始化,一次通过容器,一次通过切面。 |
单元测试 @Configurable
对象
@Configurable
支持的目标之一是实现领域对象的独立单元测试,而无需硬编码查找带来的困难。如果 @Configurable
类型尚未被 AspectJ 织入,则在单元测试期间该注解没有任何作用。您可以在被测对象中设置 mock 或 stub 属性引用,然后像往常一样进行。如果 @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 Framework 事务支持的用户。
解释 @Transactional
注解的切面是 AnnotationTransactionAspect
。当您使用此切面时,必须注解实现类(或该类中的方法或两者),而不是该类实现的接口(如果有的话)。AspectJ 遵循 Java 的规则,即接口上的注解不会被继承。
类上的 @Transactional
注解指定了该类中任何公共操作执行的默认事务语义。
类中方法上的 @Transactional
注解会覆盖类注解(如果存在)提供的默认事务语义。任何可见性的方法都可以被注解,包括私有方法。直接注解非公共方法是对此类方法的执行进行事务划分的唯一方法。
从 Spring Framework 4.2 开始,spring-aspects 提供了一个类似的切面,为标准的 jakarta.transaction.Transactional 注解提供了完全相同的功能。请查看 JtaAnnotationTransactionAspect 获取更多详细信息。 |
对于希望使用 Spring 配置和事务管理支持但不希望(或不能)使用注解的 AspectJ 程序员,spring-aspects.jar
还包含您可以扩展的 abstract
切面,以提供您自己的切点定义。请参阅 AbstractBeanConfigurerAspect
和 AbstractTransactionAspect
切面的源代码了解更多信息。例如,以下摘录展示了如何编写一个切面,通过使用与完全限定类名匹配的 prototype 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" 属性 |
非单例切面更难配置。然而,可以通过创建 prototype 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 配置 LTW(完全不涉及 Spring)的完整详情,请参阅 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 weaver 我们想将 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,这是正确的。然而,对于本例,我们使用一个 Java agent(Spring 提供)来开启 LTW。我们使用以下命令运行前面展示的 Main
类:
java -javaagent:C:/projects/xyz/lib/spring-instrument.jar com.xyz.Main
-javaagent
是一个用于指定和启用 用于检测(instrument)运行在 JVM 上的程序 的 agent 的标志。Spring Framework 自带一个这样的 agent,即 InstrumentationSavingAgent
,它打包在 spring-instrument.jar
中,在前面的示例中作为 -javaagent
参数的值提供。
Main
程序的执行输出如下例所示。(我在 calculateEntitlement()
实现中加入了 Thread.sleep(..)
语句,以便 profiler 实际捕获到非 0 毫秒的数据( 01234
毫秒不是 AOP 引入的开销)。)以下列表显示了我们运行 profiler 时得到的输出:
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 基础设施通过使用 classpath 上的一个或多个 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 dump 文件和警告之类的副作用。从效率角度来看,这也是一个最佳实践。 |
此文件的结构和内容在 AspectJ 参考文档 的 LTW 部分有详细介绍。由于 aop.xml
文件是纯 AspectJ 文件,我们在此不再详细描述。
所需的库 (JARS)
要使用 Spring Framework 对 AspectJ LTW 的支持,您至少需要以下库:
-
spring-aop.jar
-
aspectjweaver.jar
如果您使用Spring 提供的 agent 来启用 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,例如 LoadTimeWeaver
和 AspectJWeavingEnabler
。默认的 LoadTimeWeaver
是 DefaultContextLoadTimeWeaver
类,它会尝试装饰一个自动检测到的 LoadTimeWeaver
。“自动检测到”的 LoadTimeWeaver
的确切类型取决于您的运行时环境。下表总结了各种 LoadTimeWeaver
实现:
运行时环境 | LoadTimeWeaver 实现 |
---|---|
在 Apache Tomcat 中运行 |
|
在 GlassFish 中运行 (仅限于 EAR 部署) |
|
|
|
JVM 使用 Spring |
|
回退机制,期望底层 ClassLoader 遵循通用约定(即 |
|
请注意,该表仅列出了使用 DefaultContextLoadTimeWeaver
时自动检测到的 LoadTimeWeaver
实现。您可以精确指定要使用的 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
。下表总结了这三个可能的值:
注解值 | XML 值 | 说明 |
---|---|---|
|
|
AspectJ weaving 已开启,并在加载时按需织入切面。 |
|
|
LTW 已关闭。在加载时不会织入任何切面。 |
|
|
如果 Spring LTW 基础设施能够找到至少一个 |
特定环境配置
这最后一节包含在应用程序服务器和 Web 容器等环境中使用 Spring LTW 支持时所需的任何额外设置和配置。
Tomcat, JBoss, WildFly
Tomcat 和 JBoss/WildFly 提供了一种通用的应用程序 ClassLoader
,能够进行本地 instrumentation。Spring 的原生 LTW 可以利用这些 ClassLoader 实现来提供 AspectJ weaving。您可以简单地启用加载时织入,如前文所述。具体来说,您不需要修改 JVM 启动脚本来添加 -javaagent:path/to/spring-instrument.jar
。
请注意,在 JBoss 上,您可能需要禁用应用程序服务器扫描,以防止它在应用程序实际启动之前加载类。一个快速的变通方法是向您的 artifact 添加一个名为 WEB-INF/jboss-scanning.xml
的文件,内容如下:
<scanning xmlns="urn:jboss:scanning:1.0"/>
通用 Java 应用程序
当在特定 LoadTimeWeaver
实现不支持的环境中需要类 instrumentation 时,JVM agent 是通用的解决方案。对于这种情况,Spring 提供了 InstrumentationLoadTimeWeaver
,它需要一个 Spring 特定的(但非常通用的)JVM agent,即 spring-instrument.jar
,该 agent 会被常见的 @EnableLoadTimeWeaving
和 <context:load-time-weaver/>
设置自动检测到。
要使用它,您必须使用 Spring agent 启动虚拟机,提供以下 JVM 选项:
-javaagent:/path/to/spring-instrument.jar
请注意,这需要修改 JVM 启动脚本,这可能会阻止您在应用程序服务器环境中使用此方法(取决于您的服务器和操作策略)。尽管如此,对于每个 JVM 一个应用程序的部署,例如独立的 Spring Boot 应用程序,您通常无论如何都会控制整个 JVM 设置。