github编辑

5.10. 在 Spring 应用程序中使用 AspectJ

到目前为止,我们在本章中介绍的所有内容都是纯 Spring AOP。在本节中,如果您的需求超出了 Spring AOP 单独提供的功能,我们将了解如何使用 AspectJ 编译器或编织器来代替 Spring AOP 或作为 Spring AOP 的补充。

Spring 附带了一个小的 AspectJ 切面库,它在您的发行版中为spring-aspects.jar. 您需要将其添加到您的类路径中才能使用其中的切面。Using AspectJ to Dependency Inject Domain Objects with Springarrow-up-rightAspectJ 的其他 Spring 切面arrow-up-right讨论了这个库的内容以及如何使用它。使用 Spring IoC 配置 AspectJ 切面arrow-up-right讨论了如何依赖注入使用 AspectJ 编译器编织的 AspectJ 切面。最后, 在 Spring Frameworkarrow-up-right中使用 AspectJ 进行加载时编织介绍了使用 AspectJ 的 Spring 应用程序的加载时编织。

5.10.1. 使用 AspectJ 通过 Spring 依赖注入域对象

Spring 容器实例化和配置应用程序上下文中定义的 bean。也可以要求 bean 工厂配置一个预先存在的对象,给定包含要应用的配置的 bean 定义的名称。 spring-aspects.jar包含一个注解驱动的切面,它利用此功能允许对任何对象进行依赖注入。该支持旨在用于在任何容器控制之外创建的对象。域对象通常属于这一类,因为它们通常是使用 new操作符以编程方式创建的,或者作为数据库查询的结果由 ORM 工具创建。

@Configurable注解将一个类标记为符合 Spring 驱动配置的条件。在最简单的情况下,您可以将其纯粹用作标记注解,如以下示例所示:

package com.xyz.myapp.domain;

import org.springframework.beans.factory.annotation.Configurable;

@Configurable
public class Account {
    // ...
}

当以这种方式用作标记接口时,SpringAccount通过使用与完全限定类型名称com.xyz.myapp.domain.Account(由于 bean 的默认名称是其类型的完全限定名称,因此声明原型定义的一种方便方法是省略id属性,如以下示例所示:

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

如果要显式指定要使用的原型 bean 定义的名称,可以直接在注解中这样做,如以下示例所示:

package com.xyz.myapp.domain;

import org.springframework.beans.factory.annotation.Configurable;

@Configurable("account")
public class Account {
    // ...
}

Spring 现在查找名为的 bean 定义并将account其用作配置新Account实例的定义。

您还可以使用自动装配来避免指定专用的 bean 定义。要让 Spring 应用自动装配,请使用@Configurable注释的 autowire 属性。您可以分别指定 @Configurable(autowire=Autowire.BY_TYPE)@Configurable(autowire=Autowire.BY_NAME) 来按类型或按名称进行自动装配。作为替代方案,最好在字段或方法级别通过 @Autowired @Inject @Configurable beans 指定显式的、注释驱动的依赖项注入(有关更多详细信息,请参阅基于注释的容器配置)。

最后,您可以使用 dependencyCheck 属性(例如 @Configurable(autowire=Autowire.BY_NAME,dependencyCheck=true))为新创建和配置的对象中的对象引用启用 Spring 依赖项检查。如果此属性设置为 true,Spring 将在配置后验证是否已设置所有属性(不是基元或集合)。

请注意,单独使用注释不会产生任何作用。 spring-aspects.jar 中的 AnnotationBeanConfigurerAspect 作用于注释的存在。实质上,该方面说,“从使用 @Configurable 注解的类型的新对象初始化返回后,根据注解的属性使用 Spring 配置新创建的对象”。在此上下文中,“初始化”指的是新实例化的对象(例如,使用 new 运算符实例化的对象)以及正在进行反序列化(例如,通过 readResolve())的可序列化对象。

上一段中的关键词之一是“本质上”。在大多数情况下,“从新对象的初始化返回后”的确切语义是可以的。在这种情况下,“初始化之后”意味着依赖项是在对象构建之后注入的。这意味着依赖项不能在类的构造函数体中使用。如果您希望在构造函数主体运行之前注入依赖项,从而可以在构造函数主体中使用,则需要在 @Configurable声明中定义这个,如下所示:@Configurable(preConstruction = true)

您可以在AspectJ Programming Guidearrow-up-right的这个附录arrow-up-right中找到有关 AspectJ 中各种切入点类型的语言语义的更多信息。

为此,必须使用 AspectJ 编织器编织带注解的类型。您可以使用构建时 Ant 或 Maven 任务来执行此操作(例如,参见 AspectJ 开发环境指南arrow-up-right)或加载时编织(参见Spring Framework 中使用 AspectJ 的加载时编织arrow-up-right)。 AnnotationBeanConfigurerAspect本身需要由Spring 配置(为了获得对用于配置新对象的 bean 工厂的引用)。如果使用基于 Java 的配置,则可以添加@EnableSpringConfigured到任何 @Configuration类中,如下所示:

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

在配置切面之前创建的对象实例会@Configurable导致向调试日志发出消息,并且不会进行对象配置。一个示例可能是 Spring 配置中的 bean,它在 Spring 初始化时创建域对象。在这种情况下,您可以使用 depends-onbean 属性手动指定 bean 依赖于配置切面。以下示例显示了如何使用该depends-on属性:

不要通过 bean 配置器切面激活@Configurable处理,除非您真的想在运行时依赖它的语义。特别是,请确保不要在容器中注册为常规 Spring bean 的 bean 类上使用@Configurable。这样做会导致双重初始化,一次通过容器,一次通过切面。

单元测试@Configurable对象

@Configurable支持的目标之一是启用域对象的独立单元测试,而不会遇到与硬编码查找相关的困难。如果AspectJ 没有编织@Configurable类型,则注解在单元测试期间没有影响。您可以在被测对象中设置模拟或存根属性引用并正常进行。如果@Configurable类型已由 AspectJ 编织,您仍然可以像往常一样在容器外进行单元测试,但每次构造@Configurable对象时都会看到一条警告消息,指示它尚未由 Spring 配置。

使用多个应用程序上下文

AnnotationBeanConfigurerAspect用于实现支持的@Configurable是 AspectJ 单例切面。单例切面的范围与static成员的范围相同:每个类加载器都有一个切面实例来定义类型。这意味着,如果您在同一个类加载器层次结构中定义多个应用程序上下文,您需要考虑在哪里定义@EnableSpringConfiguredbean 以及在spring-aspects.jar类路径中放置的位置。

考虑一个典型的 Spring Web 应用程序配置,它具有一个共享的父应用程序上下文,它定义了公共业务服务、支持这些服务所需的一切,以及每个 servlet 的一个子应用程序上下文(其中包含特定于该 servlet 的定义)。所有这些上下文共存于同一个类加载器层次结构中,因此AnnotationBeanConfigurerAspect只能保存对其中一个的引用。在这种情况下,我们推荐在共享(父)应用程序上下文中定义@EnableSpringConfigured bean。这定义了您可能想要注入到域对象中的服务。结果是您无法使用@Configurable 机制(这可能不是您想要做的事情)来配置域对象,并引用在子(特定于servlet)上下文中定义的bean。

在同一个容器中部署多个 Web 应用程序时,请确保每个 Web 应用程序都使用自己的 ClassLoader 加载 spring-aspects.jar 中的类型(例如,将 spring-aspects.jar 放在 WEB-INF/lib 中)。如果 spring-aspects.jar 仅添加到容器范围的类路径(因此由共享父类加载器加载),则所有 Web 应用程序共享相同的切面实例(这可能不是您想要的)。

5.10.2. AspectJ 的其他 Spring 切面

除了@Configurable切面之外,spring-aspects.jar还包含一个 AspectJ 切面,您可以使用它来驱动 Spring 的事务管理,以处理使用注解进行@Transactional注解的类型和方法。这主要适用于希望在 Spring 容器之外使用 Spring Framework 的事务支持的用户。

@Transactional注解的切面是 AnnotationTransactionAspect. 当您使用此切面时,您必须注解实现类(或该类中的方法或两者),而不是该类实现的接口(如果有)。AspectJ 遵循 Java 的规则,即不继承接口上的注解。

类上的@Transactional注解指定执行类中任何公共操作的默认事务语义。

类中方法的@Transactional注解会覆盖类注解(如果存在)给出的默认事务语义。可以注解任何可见性的方法,包括私有方法。直接注解非公共方法是获得执行此类方法的事务分界的唯一方法。

从 Spring Framework 4.2 开始,spring-aspects提供了一个类似的切面,为标准注解javax.transaction.Transactional提供完全相同的功能。检查 JtaAnnotationTransactionAspect更多细节。

对于想要使用 Spring 配置和事务管理支持但不想(或不能)使用注解的 AspectJ 程序员,spring-aspects.jar 还包含可以扩展以提供自己的切入点定义的abstract切面。有关更多信息,请参阅AbstractBeanConfigurerAspectAbstractTransactionAspect切面的来源。例如,以下摘录显示了如何编写一个切面来配置域模型中定义的所有对象实例,方法是使用与完全限定类名匹配的原型 bean 定义:

5.10.3. 使用 Spring IoC 配置 AspectJ 切面

当您将 AspectJ 切面与 Spring 应用程序一起使用时,自然希望并期望能够使用 Spring 配置这些切面。AspectJ 运行时本身负责切面创建,通过 Spring 配置 AspectJ 创建的切面的方式取决于切面使用的 AspectJ 实例化模型(per-xxx子句)。

大多数 AspectJ 切面都是单例切面。这些切面的配置很容易。您可以创建一个引用切面类型的 bean 定义,并包含factory-method="aspectOf"bean 属性。这确保 Spring 通过向 AspectJ 请求它而不是尝试自己创建实例来获取切面实例。以下示例显示了如何使用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/>元素的名称误导。使用它会导致创建 Spring AOP 代理。这里使用了@AspectJ 样式的切面声明,但不涉及 AspectJ 运行时。

5.10.4. 在 Spring 框架中使用 AspectJ 进行加载时编织

加载时编织 (LTW) 是指将 AspectJ 切面编织到应用程序的类文件中的过程,因为它们正在加载到 Java 虚拟机 (JVM) 中。本节的重点是在 Spring Framework 的特定上下文中配置和使用 LTW。本节不是对 LTW 的一般介绍。有关 LTW 的详细信息以及仅使用 AspectJ 配置 LTW(根本不涉及 Spring)的详细信息,请参阅 AspectJ 开发环境指南的 LTW 部分arrow-up-right

Spring Framework 为 AspectJ LTW 带来的价值在于能够对编织过程进行更细粒度的控制。'Vanilla' AspectJ LTW 是通过使用 Java (5+) 代理来实现的,该代理在启动 JVM 时通过指定 VM 参数来打开。因此,它是一个 JVM 范围的设置,在某些情况下可能很好,但通常有点过于粗糙。启用 Spring 的 LTW 允许您逐个打开 LTW ClassLoader,这更细粒度,并且在“单 JVM 多应用程序”环境中更有意义(例如在典型的应用程序服务器环境中) )。

此外,在某些环境中arrow-up-right,此支持支持加载时编织,而无需对需要添加-javaagent:path/to/aspectjweaver.jar或的应用程序服务器的启动脚本进行任何修改(正如我们在本节后面描述的那样)-javaagent:path/to/spring-instrument.jar。开发人员配置应用程序上下文以启用加载时编织,而不是依赖通常负责部署配置(例如启动脚本)的管理员。

现在推销已经结束,让我们先来看一个使用 Spring 的 AspectJ LTW 的快速示例,然后详细介绍示例中介绍的元素。有关完整示例,请参阅 Petclinic 示例应用程序arrow-up-right

第一个例子

假设您是一名应用程序开发人员,他的任务是诊断系统中某些性能问题的原因。与其打破一个分析工具,我们将打开一个简单的分析切面,让我们快速获得一些性能指标。然后,我们可以立即将更细粒度的分析工具应用于该特定区域。

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

以下示例显示了分析切面,这并不花哨。它是一个基于时间的分析器,使用@AspectJ 风格的切面声明:

我们还需要创建一个META-INF/aop.xml文件,通知 AspectJ 编织器我们想要将我们的类编织ProfilingAspect到我们的类中。这种文件约定,即在 Java 类路径中存在一个文件(或多个文件)称为META-INF/aop.xml标准 AspectJ。以下示例显示了该aop.xml文件:

现在我们可以继续进行配置的特定于 Spring 的部分。我们需要配置一个LoadTimeWeaver(稍后解释)。这个加载时编织器是负责将一个或多个META-INF/aop.xml文件中的切面配置编织到应用程序中的类中的基本组件。好处是它不需要太多的配置(还有一些选项可以指定,但后面会详细介绍),如下例所示:

现在所有必需的工件(切面、META-INF/aop.xml 文件和 Spring 配置)都已就位,我们可以创建以下驱动程序类,并使用一个main(..)方法来演示 LTW 的实际操作:

我们还有最后一件事要做。ClassLoader本节的介绍确实说过,可以根据 Spring有选择地打开 LTW ,这是真的。但是,对于本示例,我们使用 Java 代理(随 Spring 提供)来打开 LTW。我们使用以下命令来运行Main前面显示的类:

-javaagent 是一个标志,用于指定并启用代理来检测在 JVM 上运行的程序。 Spring 框架附带了这样一个代理,InstrumentationSavingAgent,它打包在 spring-instrument.jar 中,该 jar 在前面的示例中作为-javaagent参数的值提供。

程序执行的输出Main类似于下一个示例。(我在实现中引入了一条Thread.sleep(..)语句,calculateEntitlement() 以便探查器实际上捕获 0 毫秒以外的时间(01234毫秒不是 AOP 引入的开销)。以下清单显示了我们在运行探查器时得到的输出:

由于这个 LTW 是通过使用成熟的 AspectJ 来实现的,因此我们不仅限于通知 Spring bean。该Main程序的以下细微变化会产生相同的结果:

请注意,在前面的程序中,我们如何引导 Spring 容器,然后创建一个完全在 Spring 上下文之外的新StubEntitlementCalculationService实例。分析通知仍然被融入其中。

诚然,这个例子很简单。但是,Spring 中 LTW 支持的基础知识已经在前面的示例中介绍过,本节的其余部分将详细解释每一位配置和使用背后的“原因”。

这个ProfilingAspect例子中使用的可能是基本的,但它非常有用。这是开发时切面的一个很好的例子,开发人员可以在开发期间使用它,然后轻松地从部署到 UAT 或生产中的应用程序的构建中排除。

切面

您在 LTW 中使用的切面必须是 AspectJ 切面。您可以使用 AspectJ 语言本身编写它们,也可以使用 @AspectJ 样式编写切面。那么你的切面都是有效的 AspectJ 和 Spring AOP 切面。此外,编译的切面类需要在类路径上可用。

'META-INF/aop.xml'

AspectJ LTW 基础结构是通过使用META-INF/aop.xml Java 类路径中的一个或多个文件(直接或者更典型地在 jar 文件中)来配置的。

该文件的结构和内容在 AspectJ 参考文档arrow-up-right的 LTW 部分中有详细说明。因为该aop.xml文件是 100% AspectJ,所以我们在此不再赘述。

所需的库 (JARS)

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

  • spring-aop.jar

  • aspectjweaver.jar

如果使用Spring 提供的代理来启用检测arrow-up-right,还需要:

  • spring-instrument.jar

spring 配置

Spring 的 LTW 支持中的关键组件是LoadTimeWeaver接口(在 org.springframework.instrument.classloading包中),以及 Spring 发行版附带的众多实现。LoadTimeWeaver负责在运行时向 java.lang.instrument.ClassFileTransformers添加一个或多个ClassLoader,这为各种有趣的应用程序打开了大门,其中之一恰好是切面的 LTW。

如果您不熟悉运行时类文件转换的想法,请java.lang.instrument在继续之前查看包的 javadoc API 文档。虽然该文档并不全面,但至少您可以看到关键接口和类(供您阅读本节时参考)。

LoadTimeWeaver为特定配置 ApplicationContext可以像添加一行一样简单。(请注意,您几乎肯定需要使用 ApplicationContext作为 Spring 容器——通常, BeanFactory是不够的,因为 LTW 支持使用BeanFactoryPostProcessors.)

要启用 Spring Framework 的 LTW 支持,您需要配置一个LoadTimeWeaver,这通常通过使用@EnableLoadTimeWeaving注解来完成,如下所示:

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

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

运行环境

LoadTimeWeaver执行

GlassFisharrow-up-right中运行(仅限于 EAR 部署)

GlassFishLoadTimeWeaver

JBossLoadTimeWeaver

WebSphereLoadTimeWeaver

WebLogicLoadTimeWeaver

JVM 始于 Spring InstrumentationSavingAgent ( java -javaagent:path/to/spring-instrument.jar)

InstrumentationLoadTimeWeaver

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

ReflectiveLoadTimeWeaver

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

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

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

LoadTimeWeaver稍后可以使用众所周知的名称从 Spring 容器中检索由配置定义和注册的loadTimeWeaver. 请记住,它LoadTimeWeaver仅作为 Spring 的 LTW 基础架构添加一个或多个ClassFileTransformers. 执行 LTW的实际 ClassFileTransformerClassPreProcessorAgentAdapter(来自org.aspectj.weaver.loadtime包的)类。有关更多详细信息,请参阅该类的类级别 javadoc ClassPreProcessorAgentAdapter,因为实际如何实现编织的细节超出了本文档的范围。

还有一个配置的最后一个属性需要讨论:aspectjWeaving 属性(或者aspectj-weaving如果您使用 XML)。此属性控制是否启用 LTW。它接受三个可能的值之一,默认值是 autodetect如果属性不存在。下表总结了三个可能的值:

注解值
XML 值
解释

ENABLED

on

AspectJ weaving 已打开,并且在加载时适当地编织切面。

DISABLED

off

LTW 已关闭。在加载时没有编织任何切面。

AUTODETECT

autodetect

如果 Spring LTW 基础结构可以找到至少一个META-INF/aop.xml文件,则 AspectJ weaving 处于打开状态。否则,它会关闭。这是默认值。

特定于环境的配置

最后一部分包含在应用程序服务器和 Web 容器等环境中使用 Spring 的 LTW 支持时所需的任何其他设置和配置。

Tomcat、JBoss、WebSphere、WebLogic

Tomcat、JBoss/WildFly、IBM WebSphere Application Server 和 Oracle WebLogic Server 都提供了一个ClassLoader能够进行本地检测的通用应用程序。Spring 的本机 LTW 可以利用这些 ClassLoader 实现来提供 AspectJ 编织。如前所述arrow-up-right,您可以简单地启用加载时编织。具体来说,您无需修改 JVM 启动脚本即可添加: -javaagent:path/to/spring-instrument.jar.

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

通用 Java 应用程序

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

要使用它,您必须通过提供以下 JVM 选项来使用 Spring 代理启动虚拟机:

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

最后更新于