https://docs.spring.io/spring-framework/reference/core/aop.html
面向切面编程(AOP)通过提供另一种思考程序结构的方式,补充了面向对象编程(OOP)。在OOP中,模块化的关键单位是类,而在AOP中,模块化的单位是切面。切面使跨多个类型和对象的关注点(如事务管理)模块化成为可能。(在AOP文献中,这样的关注点通常被称为“跨领域”关注点。)
Spring 的一个关键组件是 AOP 框架。尽管 Spring IoC 容器不依赖于 AOP(意味着如果你不想使用 AOP,则不需要使用它),但 AOP 补充了 Spring IoC,以提供一个功能非常强大的中间件解决方案。
使用 AspectJ 切点的 Spring AOP
Spring 提供了简单而强大的方式,通过使用基于模式的方法或 @AspectJ
注解风格来编写自定义切面。这两种风格都提供了完全类型化的通知(advice)以及对 AspectJ 切点语言的使用,同时仍然使用 Spring AOP 进行注入。
在 Spring 框架中,AOP用于:
- 提供声明式企业服务。最重要的服务之一是声明式事务管理。
- 让用户实现自定义切面,以 AOP 补充他们对 OOP 的使用。
如果你只对通用的声明式服务或其它预打包的声明式中间件服务(如连接池)感兴趣,那么你就不需要直接与 Spring AOP 一起工作。
AOP(面向切面编程)概念
从定义一些核心的 AOP 概念和术语开始。这些术语并不是 Spring 特有的。
- 切面(Aspect):一个关注点(concern)的模块化,该关注点横跨多个类。在企业 Java 应用中,事务管理是一个很好的跨领域关注点示例。在 Spring AOP 中,切面通过使用常规类(基于模式的方法)或使用
@Aspect
注解注释的常规类(@AspectJ
风格)来实现。 - 连接点(Join point):程序执行过程中的一个点,例如方法的执行或异常的处理。在 Spring AOP 中,连接点始终表示方法执行。
- 通知(Advice):切面在特定连接点执行的操作。不同类型的通知包括“环绕(around)”、“前置(before)”和“后置(after)”通知。许多 AOP 框架,包括 Spring,都将通知建模为拦截器,并在连接点周围维护一个拦截器链。
- 切点(Pointcut):一个匹配连接点的断言(predicate )。通知与切点表达式相关联,并在切点匹配的任何连接点(例如,具有特定名称的方法的执行)上运行。切点表达式匹配的连接点概念是 AOP 的核心,Spring 默认使用 AspectJ 切点表达式语言。
- 介绍(Introduction):代表一个类型声明额外的方法或字段。Spring AOP 允许向任何被通知的对象引入新的接口(以及相应的实现)。例如,可以使用介绍来使 bean 实现
IsModified
接口,以简化缓存。(在 AspectJ 社区中,介绍被称为 inter-type declaration(类型间声明))。 - 目标对象(Target object):一个被一个或多个切面通知的对象。也被称为“被通知对象”。由于 Spring AOP 是通过运行时代理实现的,因此该对象始终是一个代理对象。
- AOP 代理(AOP proxy):由 AOP 框架创建的对象,用于实现切面合约(advice 方法执行等)。在 Spring 框架中,AOP 代理是一个 JDK 动态代理或 CGLIB 代理。
- 织入(Weaving):将切面与其它应用程序类型或对象链接起来,以创建被通知的对象。这可以在编译时(例如使用 AspectJ 编译器)、加载时或运行时完成。Spring AOP 与其它纯 Java AOP 框架一样,在运行时执行织入。
Spring AOP 包括以下类型的通知:
- 前置通知(Before advice):在连接点之前运行的通知,但没有能力阻止执行流程继续到连接点(除非它抛出异常)。
- 后置返回通知(After returning advice):在连接点正常完成后运行的通知(例如,如果方法返回而没有抛出异常)。
- 后置异常通知(After throwing advice):如果方法通过抛出异常退出,则运行该通知。
- 后置最终通知(After (finally) advice):无论连接点以何种方式退出(正常返回或异常返回),都会运行该通知。
- 环绕通知(Around advice):围绕连接点(如方法调用)的通知。这是最强大的通知类型。环绕通知可以在方法调用之前和之后执行自定义行为。它还负责选择是否继续执行连接点,或者通过返回自己的返回值或抛出异常来简化被通知方法的执行。
环绕通知是最通用的通知类型。由于 Spring AOP 与 AspectJ 一样,提供了全套的通知类型,因此建议使用能够实现所需行为的最不强大的通知类型。例如,如果只需要使用方法的返回值来更新缓存,那么实现一个后置返回通知而不是环绕通知会更好,尽管环绕通知也可以完成同样的工作。使用最具体的通知类型提供了更简单的编程模型,且出错的可能性更小。例如,不需要在用于环绕通知的 JoinPoint
上调用 proceed()
方法,因此不会忘记调用它。
所有通知参数都是静态类型的,这样就可以使用适当类型的通知参数(例如,方法执行后的返回值的类型),而不是对象数组。
由切点匹配的连接点概念是 AOP 的关键,它将其与其它仅提供拦截的较旧技术区分开来。切点使通知的目标与面向对象层次结构独立。例如,可以将一个环绕通知应用于一组跨越多个对象的方法(如服务层中的所有业务操作),以提供声明式事务管理。
Spring AOP 的能力和目标
Spring AOP 是使用纯 Java 实现的,不需要特殊的编译过程。Spring AOP 不需要控制类加载器层次结构,因此适用于在 servlet 容器或应用服务器中使用。
Spring AOP 目前仅支持方法执行连接点(对 Spring Beans 上的方法执行进行通知)。尽管可以通过不破坏 Spring AOP 核心 API 的方式添加对字段拦截的支持,但当前 Spring AOP 不支持字段拦截。如果你需要通知字段访问和更新连接点,请考虑使用 AspectJ 等语言。
Spring AOP 的 AOP 实现方式与其它大多数 AOP 框架不同。目标不是提供最完整的 AOP 实现(尽管 Spring AOP 相当强大),而是为 AOP 实现和 Spring IoC 之间提供紧密的集成,以帮助解决企业应用中的常见问题。
因此,例如,Spring Framework 的 AOP 功能通常与 Spring IoC 容器一起使用。Aspects通过使用常规的 bean 定义语法进行配置(尽管这允许强大的“自动代理”功能)。这是与其他它AOP 实现的重要区别。你无法轻松或高效地使用 Spring AOP 完成某些操作,例如通知非常细粒度的对象(通常是领域对象)。在这种情况下,AspectJ 是最佳选择。然而,Spring AOP 为大多数适用于 AOP 的企业 Java 应用问题提供了出色的解决方案。
Spring AOP 从未试图与 AspectJ 竞争,以提供全面的 AOP 解决方案。基于代理的框架(如 Spring AOP)和全面成熟的框架(如 AspectJ)都是有价值的,并且它们是互补的,而不是相互竞争的。Spring 将 Spring AOP 和 IoC 与 AspectJ 无缝集成,以在基于 Spring 的一致应用程序架构中启用所有 AOP 的使用。这种集成不会影响 Spring AOP API 或 AOP Alliance API。Spring AOP 保持向后兼容性。
Spring Framework 的核心原则之一是非侵入性。这意味着你不应该被迫将特定于框架的类和接口引入你的业务或领域模型中。然而,在某些地方,Spring Framework 确实提供了将特定于 Spring Framework 的依赖项引入代码库的选择。提供这些选项的原因是,在某些情况下,以某种方式读取或编写某些特定功能可能更为简单。但是,Spring Framework(几乎)总是提供选择:你可以自由地做出明智的决定,以选择最适合你特定用例或场景的选项。
与本章相关的一个选择是选择哪个 AOP 框架(以及哪种 AOP 风格)。可以选择 AspectJ、Spring AOP 或两者都选择。还可以选择使用 @AspectJ
注解风格的方法或 Spring XML 配置风格的方法。
AOP 代理(AOP Proxies)
Spring AOP 默认使用标准的 JDK 动态代理来创建 AOP 代理。这使得任何接口(或一组接口)都可以被代理。
Spring AOP 还可以使用 CGLIB 代理。这对于代理类而不是接口是必要的。如果业务对象没有实现接口,则默认使用 CGLIB。由于面向接口编程而不是面向类编程是一种良好的做法,因此业务类通常实现一个或多个业务接口。在那些需要通知未在接口上声明的方法或需要将代理对象作为具体类型传递给方法的情况下,可以强制使用 CGLIB。
重要的是要理解 Spring AOP 是基于代理的。
@AspectJ 支持
@AspectJ 是指使用注解声明方面的风格,就像常规的 Java 类一样。@AspectJ 风格是由 AspectJ 项目在 AspectJ 5 版本中引入的。Spring 使用 AspectJ 提供的库解析和匹配切入点,解释与 AspectJ 5 相同的注解。但是,AOP 运行时仍然是纯 Spring AOP,没有依赖 AspectJ 编译器或编织器(weaver)。
启用 @AspectJ 支持
要在 Spring 配置中使用 @AspectJ 方面,需要启用 Spring 对基于 @AspectJ 方面的 Spring AOP 配置的支持,并自动根据这些方面是否通知了 bean 来对 bean 进行代理。通过自动代理,指的是,如果 Spring 确定一个 bean 被一个或多个方面通知,它会自动为该 bean 生成一个代理来拦截方法调用,并确保在需要时运行通知。
可以通过 XML 或 Java 风格的配置启用 @AspectJ 支持。在任一情况下,还需要确保 AspectJ 的 aspectjweaver.jar
库位于应用程序的类路径上(版本 1.9 或更高版本)。该库可从 AspectJ 分发的 lib
目录或 Maven 中心存储库中获得。
使用 Java 配置启用 @AspectJ 支持
要使用 Java @Configuration
启用 @AspectJ 支持,请添加 @EnableAspectJAutoProxy
注解,如下所示:
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
使用 XML 配置启用 @AspectJ 支持
要使用基于 XML 的配置启用 @AspectJ 支持,请使用 aop:aspectj-autoproxy
元素,如下所示:
<aop:aspectj-autoproxy/>
声明一个方面(Aspect)
启用 @AspectJ 支持后,任何在你的应用程序上下文中定义并具有 @AspectJ 方面(带有 @Aspect
注解)的类的 bean 都会自动被 Spring 检测到并用于配置 Spring AOP。接下来的两个示例显示了对于不太有用的方面所需的最小步骤。
这两个示例中的第一个显示了应用程序上下文中的常规 bean 定义,该定义指向一个带有 @Aspect
注解的 bean 类:
<bean id="myAspect" class="com.xyz.NotVeryUsefulAspect">
<!-- configure properties of the aspect here -->
</bean>
这两个示例中的第二个显示了 NotVeryUsefulAspect
类定义,该类带有 @Aspect
注解:
package com.xyz;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class NotVeryUsefulAspect {
}
方面(使用 @Aspect
注解的类)可以像任何其它类一样具有方法和字段。它们还可以包含切入点、通知和引入(中间类型)声明。
通过组件扫描自动检测方面
可以在 Spring XML 配置中将方面类注册为常规 bean,通过 @Configuration
类中的 @Bean
方法,或者让 Spring 通过类路径扫描自动检测它们,就像检测任何其它 Spring 托管的 bean 一样。但是,请注意,在类路径中自动检测时,@Aspect
注解是不足以满足要求的。为此,需要添加一个单独的 @Component
注解(或者,根据 Spring 组件扫描器的规则,可以添加一个自定义的元注解)。
用其它方面通知方面?
在 Spring AOP 中,方面本身不能成为来自其它方面的通知的目标。类上的 @Aspect
注解将其标记为方面,因此将其排除在自动代理之外。
声明一个切入点(Pointcut)
切入点决定了我们感兴趣的连接点,从而使我们能够控制通知何时运行。Spring AOP 仅支持 Spring bean 的方法执行连接点,因此可以将切入点视为匹配 Spring bean 上的方法执行。切入点的声明有两个部分:一个由名称和任何参数组成的签名,以及一个确定我们对哪些方法执行感兴趣的切入点表达式。在 @AspectJ 注解风格的 AOP 中,切入点签名由常规方法定义提供,而切入点表达式则通过使用 @Pointcut
注解指示(作为切入点签名的方法必须具有 void
返回类型)。
一个示例可能有助于明确切入点签名和切入点表达式之间的区别。以下示例定义了一个名为 anyOldTransfer
的切入点,它与名为 transfer
的任何方法的执行相匹配:
@Pointcut("execution(* transfer(..))") // the pointcut expression
private void anyOldTransfer() {} // the pointcut signature
形成 @Pointcut
注解值的切入点表达式是一个常规的 AspectJ 切入点表达式。
支持的切入点指示符(Pointcut Designators)
Spring AOP 支持在切入点表达式中使用以下 AspectJ 切入点指示符(PCD):
execution
:用于匹配方法执行连接点。当使用 Spring AOP 时,这是主要的切入点指示符。within
:将匹配限制在特定类型的连接点内(当使用 Spring AOP 时,在匹配类型内声明的方法的执行)。this
:将匹配限制在连接点(当使用 Spring AOP 时,方法的执行),其中 bean 引用(Spring AOP 代理)是给定类型的实例。target
:将匹配限制在连接点(当使用 Spring AOP 时,方法的执行),其中目标对象(被代理的应用程序对象)是给定类型的实例。- args:将匹配限制在连接点(当使用 Spring AOP 时,方法的执行),其中参数是给定类型的实例。
@target
:将匹配限制在连接点(当使用 Spring AOP 时,方法的执行),其中执行对象的类具有给定类型的注解。@args
:将匹配限制在连接点(当使用 Spring AOP 时,方法的执行),其中传递的实际参数的运行时类型具有给定类型的注解。@within
:将匹配限制在具有给定注解的类型的连接点内(当使用 Spring AOP 时,在具有给定注解的类型中声明的方法的执行)。@annotation
:将匹配限制在连接点,其中连接点的主题(在 Spring AOP 中运行的方法)具有给定的注解。
其它切入点类型
完整的 AspectJ 切入点语言支持 Spring 不支持的其它切入点指示符:call
、get
、set
、preinitialization
、staticinitialization
、initialization
、handler
、adviceexecution
、withincode
、cflow
、cflowbelow
、if
、@this
和 @withincode
。在 Spring AOP 解释的切入点表达式中使用这些切入点指示符将导致抛出 IllegalArgumentException
异常。
Spring AOP 支持的切点指示符集合在未来版本中可能会得到扩展,以支持更多的 AspectJ 切点指示符。
由于 Spring AOP 仅将匹配限制在方法执行连接点,因此关于切点指示符的上述讨论给出的定义比 AspectJ 编程指南中的定义更狭窄。此外,AspectJ 本身具有基于类型的语义,并且在执行连接点时,this
和 target
都指向同一对象:执行方法的对象。Spring AOP 是一个基于代理的系统,并区分代理对象本身(绑定到 this
)和代理后面的目标对象(绑定到 target
)。
由于 Spring 的 AOP 框架基于代理,因此在目标对象内部的调用根据定义不会被拦截。对于 JDK 代理,只能拦截代理上的公共接口方法调用。使用 CGLIB,可以拦截代理上的公共和保护方法调用(如有必要,甚至还可以拦截包可见方法)。但是,通过代理进行的常见交互应该始终通过公共签名进行设计。
切点定义通常会与任何被拦截的方法匹配。如果切点仅严格用于公共方法,即使在 CGLIB 代理场景中可能通过代理进行非公共交互,也需要相应地定义它。
如果你的拦截需求包括目标类中的方法调用或甚至构造函数,请考虑使用由 Spring 驱动的原生 AspectJ 织入,而不是使用基于 Spring 代理的 AOP 框架。这是具有不同特性的 AOP 用法的不同模式,因此在做出决定之前,请确保熟悉织入。
Spring AOP 还支持一个名为 bean
的额外切点描述符(PCD)。该切点描述符允许将连接点的匹配限制为特定命名的 Spring bean 或一组命名的 Spring bean(当使用通配符时)。bean 切点描述符具有以下形式:
bean(idOrNameOfBean)
idOrNameOfBean
标记可以是任何 Spring bean 的名称。它提供了有限的通配符支持,该支持使用 *
字符。因此,如果你为 Spring bean 建立了一些命名约定,则可以编写一个 bean PCD 表达式来选择它们。与其它切点指示符一样,bean PCD 也可以与 &&
(和)、||
(或)以及 !
(非)运算符一起使用。
bean
PCD 仅支持 Spring AOP,而不支持原生 AspectJ 织入。它是 Spring 对 AspectJ 所定义的标准 PCD 的特定扩展,因此,对于在 @Aspect
模型中声明的方面来说,它不可用。
bean
PCD 在实例级别(基于 Spring bean 名称概念)上操作,而不仅仅是在类型级别上(这是基于织入的 AOP 所限制的)。基于实例的切点指示符是 Spring 基于代理的 AOP 框架及其与 Spring bean 工厂的紧密集成的一种特殊功能,其中通过名称识别特定的 bean 是自然而直接的。
组合切点表达式
可以使用 &&
、||
和 !
来组合切点表达式。还可以按名称引用切点表达式。以下示例展示了三个切点表达式:
package com.xyz;
public class Pointcuts {
@Pointcut("execution(public * *(..))")
public void publicMethod() {}
@Pointcut("within(com.xyz.trading..*)")
public void inTrading() {}
@Pointcut("publicMethod() && inTrading()")
public void tradingOperation() {}
}
publicMethod
:如果一个方法执行连接点表示任何公共方法的执行,则匹配。inTrading
:如果一个方法执行在trading
模块中,则匹配。tradingOperation
:如果一个方法执行表示trading
模块中的任何公共方法,则匹配。
如上所示,使用较小的命名切点构建更复杂的切点表达式是一种最佳实践。当按名称引用切点时,适用正常的 Java 可见性规则(可以在同一类型中看到私有切点,在层次结构中看到受保护的切点,在任何地方看到公共切点等)。可见性不影响切点匹配。
共享命名切点定义
当处理企业应用程序时,开发人员通常需要从多个方面引用应用程序的模块和特定的操作集。建议为此目的定义一个专门的类,该类捕获常用的命名切点表达式。此类通常类似于以下 CommonPointcuts
示例(尽管你可以自己命名该类):
package com.xyz;
import org.aspectj.lang.annotation.Pointcut;
public class CommonPointcuts {
/**
* A join point is in the web layer if the method is defined
* in a type in the com.xyz.web package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.web..*)")
public void inWebLayer() {}
/**
* A join point is in the service layer if the method is defined
* in a type in the com.xyz.service package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.service..*)")
public void inServiceLayer() {}
/**
* A join point is in the data access layer if the method is defined
* in a type in the com.xyz.dao package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.dao..*)")
public void inDataAccessLayer() {}
/**
* A business service is the execution of any method defined on a service
* interface. This definition assumes that interfaces are placed in the
* "service" package, and that implementation types are in sub-packages.
*
* If you group service interfaces by functional area (for example,
* in packages com.xyz.abc.service and com.xyz.def.service) then
* the pointcut expression "execution(* com.xyz..service.*.*(..))"
* could be used instead.
*
* Alternatively, you can write the expression using the 'bean'
* PCD, like so "bean(*Service)". (This assumes that you have
* named your Spring service beans in a consistent fashion.)
*/
@Pointcut("execution(* com.xyz..service.*.*(..))")
public void businessService() {}
/**
* A data access operation is the execution of any method defined on a
* DAO interface. This definition assumes that interfaces are placed in the
* "dao" package, and that implementation types are in sub-packages.
*/
@Pointcut("execution(* com.xyz.dao.*.*(..))")
public void dataAccessOperation() {}
}
可以通过引用类的完全限定名与 @Pointcut
方法的名称,在任何需要切点表达式的地方引用此类中定义的切点。例如,为了使服务层具有事务性,可以编写以下代码,它引用了 com.xyz.CommonPointcuts.businessService()
命名切点:
<aop:config>
<aop:advisor
pointcut="com.xyz.CommonPointcuts.businessService()"
advice-ref="tx-advice"/>
</aop:config>
<tx:advice id="tx-advice">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
示例
Spring AOP 用户可能最经常使用execution
切入点指示器。执行表达式的格式如下:
execution(modifiers-pattern?
ret-type-pattern
declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)
除了返回类型模式(在前文代码片段中称为ret-type-pattern
)、名称模式和参数模式之外,其它所有部分都是可选的。返回类型模式决定了方法的返回类型必须是什么,以便匹配一个连接点。*
是最常用的返回类型模式,它匹配任何返回类型。完全限定的类型名称只有在方法返回给定类型时才会匹配。名称模式用于匹配方法名。可以使用 *
通配符作为名称模式的全部或一部分。如果指定了声明类型模式,请在后面加上一个 .
以将其与名称模式组件连接起来。参数模式稍微复杂一些:()
匹配没有参数的方法,而 (..)
匹配任意数量(零个或多个)的参数。(*)
模式匹配一个接受任意类型单一参数的方法。(*,String)
匹配一个接受两个参数的方法。第一个参数可以是任何类型,而第二个参数必须是 String
类型。
下面的示例显示了一些常见的切入点表达式:
- 任何公共方法的执行:
execution(public * *(..))
- 执行任何以
set
开头的方法:
execution(* set*(..))
- 执行由
AccountService
接口定义的任何方法:
execution(* com.xyz.service.AccountService.*(..))
- 执行
service
包中定义的任何方法:
execution(* com.xyz.service.*.*(..))
- 执行
service
包或其子包中定义的任何方法:
execution(* com.xyz.service..*.*(..))
- 在
service
包内的任何连接点(Spring AOP 中仅限方法执行):
within(com.xyz.service.*)
- 在
service
包或其子包内的任何连接点(Spring AOP 中仅限方法执行):
within(com.xyz.service..*)
- 代理实现了
AccountService
接口的任何连接点(Spring AOP 中仅限方法执行):
this(com.xyz.service.AccountService)
this
通常在绑定形式中使用。
- 目标对象实现了
AccountService
接口的任何连接点(Spring AOP 中仅限方法执行):
target(com.xyz.service.AccountService)
target
通常在绑定形式中使用。
- 接受单个参数的任何连接点(Spring AOP 中仅限方法执行),并且运行时传递的参数是
Serializable
的:
args(java.io.Serializable)
args
通常在绑定形式中使用。
注意,此示例中给出的切入点与 execution(* *(java.io.Serializable))
不同。args
版本在运行时传递的参数是Serializable
的情况下匹配,而 execution
版本在方法签名声明了单个类型为 Serializable
的参数时匹配。
- 目标对象具有
@Transactional
注解的任何连接点(Spring AOP 中仅限方法执行):
@target(org.springframework.transaction.annotation.Transactional)
也可以在绑定形式中使用 @target
。
- 目标对象的声明类型具有
@Transactional
注解的任何连接点(Spring AOP 中仅限方法执行):
@within(org.springframework.transaction.annotation.Transactional)
也可以在绑定形式中使用 @within
。
- 执行方法具有
@Transactional
注解的任何连接点(Spring AOP 中仅限方法执行):
@annotation(org.springframework.transaction.annotation.Transactional)
也可以在绑定形式中使用 @annotation
。
- 接受单个参数的任何连接点(Spring AOP 中仅限方法执行),并且传递的参数的运行时类型具有
@Classified
注解:
@args(com.xyz.security.Classified)
也可以在绑定形式中使用 @args
。
- 在名为
tradeService
的 Spring bean 上的任何连接点(Spring AOP 中仅限方法执行):
bean(tradeService)
- 在名称匹配通配符表达式
*Service
的 Spring beans 上的任何连接点(Spring AOP 中仅限方法执行):
bean(*Service)
编写好的切入点
在编译期间,AspectJ 处理切入点以优化匹配性能。检查代码并确定每个连接点是否匹配(静态或动态)给定的切入点是一个成本较高的过程。(动态匹配意味着不能仅通过静态分析完全确定匹配,需要在代码中放置一个测试来确定代码运行时是否有实际匹配)。第一次遇到切入点声明时,AspectJ 会将其重写为匹配过程的最佳形式。这意味着什么?基本上,切入点以 DNF(析取范式)重写,并且切入点的组件按照评估成本较低的顺序排序。这意味着你不必担心理解各种切入点指示符的性能,可以在切入点声明中以任何顺序提供它们。
然而,AspectJ 只能按照它被告知的内容工作。为了匹配的最佳性能,你应该思考你试图实现什么,并尽可能在定义中缩小匹配的搜索空间。现有的指示符自然分为三类:类型化、作用域和上下文:
- 类型化指示符选择特定类型的连接点:
execution
、get
、set
、call
和handler
。 - 作用域指示符选择一组感兴趣的连接点(可能有多种类型):
within
和withincode
。 - 上下文指示符基于上下文进行匹配(并可选绑定):
this
、target
和@annotation
。
编写良好的切入点应至少包括前两种类型(类型化和作用域)。你可以包含上下文指示符,以便基于连接点上下文进行匹配或绑定该上下文以供通知使用。仅提供类型化指示符或仅提供上下文指示符也可以工作,但可能会影响织入性能(使用的时间和内存),因为需要进行额外的处理和分析。作用域指示符匹配速度非常快,使用它们意味着 AspectJ 可以非常快速地排除不应进一步处理的连接点组。如果可能,一个好的切入点应始终包括一个。
声明通知(Declaring Advice)
通知与切入点表达式相关联,并在由切入点匹配的方法执行之前、之后或周围运行。切入点表达式可以是内联切入点或是对命名切入点的引用。
Before通知(Advice)
你可以使用 @Before
注解在切面中声明before 通知。
以下示例使用了内联切入点表达式。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("execution(* com.xyz.dao.*.*(..))")
public void doAccessCheck() {
// ...
}
}
如果我们使用命名切入点,可以将前面的例子重写如下:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("com.xyz.CommonPointcuts.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
}
返回后通知(After Returning Advice)
返回后通知匹配的方法执行正常返回时运行。你可以使用 @AfterReturning
注解来声明它。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning("execution(* com.xyz.dao.*.*(..))")
public void doAccessCheck() {
// ...
}
}
你可以在同一个切面中拥有多个通知声明(以及其它成员)。在这些示例中,我们只展示了单个通知声明,以便集中展示每个的效果。
有时,在通知体中你需要访问实际返回的值。你可以使用将返回值绑定的 @AfterReturning
形式来获取访问,如下例所示:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning(
pointcut="execution(* com.xyz.dao.*.*(..))",
returning="retVal")
public void doAccessCheck(Object retVal) {
// ...
}
}
在 returning
属性中使用的名称必须与通知方法中的参数名称相对应。当方法执行返回时,返回值会作为相应的参数值传递给通知方法。returning
子句还将匹配限制为仅那些返回指定类型值(在本例中是 Object
,它匹配任何返回值)的方法执行。
在使用返回后通知时,不可能返回一个完全不同的引用。
抛出后通知(After Throwing Advice)
抛出后通知在匹配的方法执行通过抛出异常退出时运行。你可以使用 @AfterThrowing
注解来声明它,如下例所示:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing("execution(* com.xyz.dao.*.*(..))")
public void doRecoveryActions() {
// ...
}
}
通常,你希望通知仅在抛出给定类型的异常时运行,并且你也经常需要在通知体中访问抛出的异常。你可以使用 throwing
属性来限制匹配(如果需要的话——否则使用 Throwable
作为异常类型)并将抛出的异常绑定到通知参数。以下示例展示了如何做到这一点:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing(
pointcut="execution(* com.xyz.dao.*.*(..))",
throwing="ex")
public void doRecoveryActions(DataAccessException ex) {
// ...
}
}
在 throwing
属性中使用的名称必须与通知方法中的参数名称相对应。当方法执行通过抛出异常退出时,异常会作为相应的参数值传递给通知方法。throwing
子句还将匹配限制为仅那些抛出指定类型(在本例中是 DataAccessException
)的异常的方法执行。
@AfterThrowing
并不表示一般的异常处理回调。具体来说,@AfterThrowing
通知方法仅用于接收来自连接点(用户声明的目标方法)本身的异常,而不是来自伴随的 @After
/@AfterReturning
方法的异常。
最终后通知(After (Finally) Advice)
最终后通知在匹配的方法执行退出时运行。它是通过使用 @After
注解来声明的。最终后通知必须准备好处理正常和异常返回条件。它通常用于释放资源和类似目的。以下示例展示了如何使用最终后通知:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;
@Aspect
public class AfterFinallyExample {
@After("execution(* com.xyz.dao.*.*(..))")
public void doReleaseLock() {
// ...
}
}
AspectJ 中的 @After
通知被定义为“after finally advice”,类似于 try-catch
语句中的 finally
块。无论结果是正常返回还是从连接点(用户声明的目标方法)抛出异常,它都会被调用,与仅适用于成功正常返回的 @AfterReturning
相反。
环绕通知(Around Advice)
环绕通知在匹配方法的执行“周围”运行。它有机会在方法运行前后做工作,并决定何时、如何,甚至是否让方法实际运行。如果需要在方法执行前后以线程安全的方式共享状态,例如启动和停止计时器,通常会使用环绕通知。
总是使用满足你需求的最弱形式通知。
例如,如果before 通知已足够满足你的需求,就不要使用环绕通知。
环绕通知是通过使用 @Around
注解来声明一个方法。该方法应该声明 Object
作为其返回类型,且方法的第一个参数必须是 ProceedingJoinPoint
类型。在通知方法体内,你必须调用 ProceedingJoinPoint
上的 proceed()
方法以运行底层方法。无参调用 proceed()
将导致调用者原始参数在调用底层方法时被提供。对于高级使用情况,proceed()
方法有一个重载变体,它接受一个参数数组(Object[]
)。数组中的值将作为调用底层方法时的参数使用。
当使用 Object[]
调用 proceed
时的行为与由 AspectJ 编译器编译的环绕通知的 proceed
行为略有不同。对于使用传统的 AspectJ 语言编写的环绕通知,传递给 proceed
的参数数量必须与传递给环绕通知的参数数量相匹配(而不是底层连接点接受的参数数量),并且在给定参数位置传递给 proceed
的值将取代连接点处绑定实体的原始值。
Spring 采取的方法更简单,更符合其基于代理的、仅执行语义。只有在你编译为 Spring 编写的 @AspectJ 切面并使用 AspectJ 编译器和编织器时,才需要意识到这一点,并且使用带参数的 proceed
。
环绕通知返回的值是方法调用者看到的返回值。例如,一个简单的缓存切面可能会在有缓存值时返回一个值,或者如果没有则调用 proceed()
(并返回那个值)。请注意,在环绕通知体内,proceed
可能被调用一次、多次或根本不调用。这些都是合法的。
如果你将环绕通知方法的返回类型声明为 void
,那么 null
将始终返回给调用者,有效地忽略了任何对 proceed()
的调用结果。因此,建议环绕通知方法声明一个 Object
类型的返回值。通知方法通常应返回对 proceed()
的调用所返回的值,即使底层方法有 void
返回类型。然而,根据使用情况,通知可以可选地返回缓存值、包装值或其它值。
以下示例展示了如何使用环绕通知:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
@Aspect
public class AroundExample {
@Around("execution(* com.xyz..service.*.*(..))")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
}
通知参数(Advice Parameters)
Spring 提供了完全类型的通知,这意味着你可以在通知签名中声明所需的参数(如我们之前看到的返回和抛出示例),而不是一直使用 Object[]
数组。
访问当前JoinPoint
任何通知方法都可以声明,作为其第一个参数,一个类型为 org.aspectj.lang.JoinPoint
的参数。环绕通知需要声明一个类型为 ProceedingJoinPoint
的第一个参数,这是 JoinPoint
的一个子类。
JoinPoint
接口提供了许多有用的方法:
getArgs()
:返回方法参数。getThis()
:返回代理对象。getTarget()
:返回目标对象。getSignature()
:返回正在被通知的方法的描述。toString()
:打印正在被通知的方法的有用描述。
向通知传递参数
要让参数值可用于通知体,你可以使用 args
的绑定形式。如果你在 args
表达式中使用参数名代替类型名,当通知被调用时,相应参数的值将作为参数值传递。一个例子应该能更清楚地说明这一点。假设你想要通知执行 DAO 操作,这些操作接受 Account
对象作为第一个参数,并且在通知体中你需要访问账户。你可以编写以下代码:
@Before("execution(* com.xyz.dao.*.*(..)) && args(account,..)")
public void validateAccount(Account account) {
// ...
}
切入点表达式中的 args(account,..)
部分有两个目的。首先,它将匹配限制为仅那些至少接受一个参数的方法执行,并且传递给该参数的参数是 Account
类的实例。其次,它通过 account
参数使实际的 Account
对象可用于通知。
另一种编写方式是声明一个切入点,当它匹配连接点时“提供”Account
对象值,然后在通知中引用命名的切入点。这将如下所示:
@Pointcut("execution(* com.xyz.dao.*.*(..)) && args(account,..)")
private void accountDataAccessOperation(Account account) {}
@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
// ...
}
代理对象(this
)、目标对象(target
)和注解(@within
、@target
、@annotation
和 @args
)都可以以类似的方式绑定。下一组示例展示了如何匹配使用 @Auditable
注解的方法执行,并提取审计代码。
以下显示了 @Auditable
注解的定义:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
AuditCode value();
}
以下显示了匹配 @Auditable
方法执行的通知:
@Before("com.xyz.Pointcuts.publicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
AuditCode code = auditable.value();
// ...
}
通知参数和泛型
Spring AOP 可以处理在类声明和方法参数中使用的泛型。假设你有如下的泛型类型:
public interface Sample<T> {
void sampleGenericMethod(T param);
void sampleGenericCollectionMethod(Collection<T> param);
}
你可以通过将通知参数与你希望拦截的方法的参数类型绑定,来限制对某些参数类型的方法类型的拦截:
@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
// Advice implementation
}
这种方法不适用于泛型集合。因此,你不能如下定义切入点:
@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
public void beforeSampleMethod(Collection<MyType> param) {
// Advice implementation
}
要实现这一点,我们必须检查集合中的每个元素,这是不合理的,因为我们也无法决定如何处理 null
值。要实现类似的功能,你必须将参数类型指定为 Collection<?>
并手动检查元素的类型。
确定参数名称
通知调用中的参数绑定依赖于将切入点表达式中使用的名称与在通知和切入点方法签名中声明的参数名称进行匹配。
Spring AOP 使用以下 ParameterNameDiscoverer
实现来确定参数名称。每个discoverer 都有机会发现参数名称,第一个成功发现参数名称的发现器将胜出。如果没有一个注册的discoverer能够确定参数名称,将抛出异常。
AspectJAnnotationParameterNameDiscoverer
:使用用户通过相应的通知或切入点注解中的argNames
属性显式指定的参数名称。KotlinReflectionParameterNameDiscoverer
:使用 Kotlin 反射 API 来确定参数名称。只有当类路径上存在此类 API 时,才会使用此discoverer。StandardReflectionParameterNameDiscoverer
:使用标准的java.lang.reflect.Parameter
API 来确定参数名称。用javac编译代码时需要使用-parameters
标志。在 Java 8+ 上推荐的方法。AspectJAdviceParameterNameDiscoverer
:从切入点表达式、returning
和throwing
子句中推断参数名称。
显式参数名称
@AspectJ 通知和切入点注解具有一个可选的 argNames
属性,你可以使用它来指定被注解的方法的参数名称。
如果 @AspectJ 切面已经被 AspectJ 编译器(ajc
)编译,即使没有调试信息,你也不需要添加 argNames
属性,因为编译器保留了所需的信息。
同样,如果一个 @AspectJ 切面已经使用 javac
和 -parameters
标志编译,你也不需要添加 argNames
属性,因为编译器保留了所需的信息。
以下示例显示了如何使用 argNames
属性:
@Before(
value = "com.xyz.Pointcuts.publicMethod() && target(bean) && @annotation(auditable)",
argNames = "bean,auditable")
public void audit(Object bean, Auditable auditable) {
AuditCode code = auditable.value();
// ... use code and bean
}
如果第一个参数的类型是 JoinPoint
、ProceedingJoinPoint
或 JoinPoint.StaticPart
,你可以从 argNames
属性的值中省略该参数的名称。例如,如果你修改前面的通知以接收连接点对象,argNames
属性不需要包含它:
@Before(
value = "com.xyz.Pointcuts.publicMethod() && target(bean) && @annotation(auditable)",
argNames = "bean,auditable")
public void audit(JoinPoint jp, Object bean, Auditable auditable) {
AuditCode code = auditable.value();
// ... use code, bean, and jp
}
对于不收集任何其它连接点上下文的通知方法,特别处理类型为 JoinPoint
、ProceedingJoinPoint
或 JoinPoint.StaticPart
的第一个参数非常方便。在这种情况下,你可以省略 argNames
属性。例如,以下通知不需要声明 argNames
属性:
@Before("com.xyz.Pointcuts.publicMethod()")
public void audit(JoinPoint jp) {
// ... use jp
}
继续参数讨论
如何在Spring AOP和AspectJ中编写具有参数且工作一致的proceed
调用。解决方案是确保通知签名按顺序绑定每个方法参数。以下示例展示了如何做到这一点:
@Around("execution(List<Account> find*(..)) && " +
"com.xyz.CommonPointcuts.inDataAccessLayer() && " +
"args(accountHolderNamePattern)")
public Object preProcessQueryPattern(ProceedingJoinPoint pjp,
String accountHolderNamePattern) throws Throwable {
String newPattern = preProcess(accountHolderNamePattern);
return pjp.proceed(new Object[] {newPattern});
}
通知排序(Advice Ordering)
当多个通知都想在同一个连接点运行时会发生什么?Spring AOP遵循与AspectJ相同的优先级规则来确定通知执行的顺序。最高优先级的通知首先“在进入时”运行(因此,给定两个前置通知,优先级最高的将首先运行)。“在退出”连接点时,最高优先级的通知最后运行(因此,给定两个后置通知,优先级最高的将会第二个运行)。
当在不同切面中定义的两个通知都需要在同一个连接点运行时,除非另有指定,否则执行顺序是未定义的。你可以通过指定优先级来控制执行顺序。这可以通过在切面类中实现 org.springframework.core.Ordered
接口或使用 @Order
注解以标准的 Spring 方式来完成。给定两个切面,从 Ordered.getOrder()
返回较低值的切面(或注解值)具有更高的优先级。
特定切面的不同通知类型在概念上意味着直接应用于连接点。因此,@AfterThrowing
通知方法不应该从伴随的 @After
/@AfterReturning
方法接收异常。
截至 Spring Framework 5.2.7,定义在同一 @Aspect
类中的通知方法需要在同一个连接点运行时,会根据其通知类型按以下顺序分配优先级,从最高到最低:@Around
、@Before
、@After
、@AfterReturning
、@AfterThrowing
。然而,需要注意的是,同一个切面中的任何 @AfterReturning
或 @AfterThrowing
通知方法之后,@After
通知方法将会被有效地调用,遵循 AspectJ 的 “after finally advice” 语义。
当在同一 @Aspect
类中定义的同类型通知(例如,两个 @After
通知方法)都需要在同一个连接点运行时,执行顺序是未定义的(因为无法通过反射获取 javac
编译类的源代码声明顺序)。请考虑将这样的通知方法合并为每个 @Aspect
类中的每个连接点一个通知方法,或者将这些通知重构为单独的 @Aspect
类,然后通过 Ordered
或 @Order
在切面级别进行排序。
引入(Introductions)
引入(在 AspectJ 中称为 inter-type declarations)允许切面声明被通知对象实现了一个给定的接口,并代表这些对象提供该接口的实现。
你可以通过使用 @DeclareParents
注解来进行引入。这个注解用于声明匹配的类型有了一个新的父类型(因此得名)。例如,给定一个名为 UsageTracked
的接口和一个名为 DefaultUsageTracked
的该接口实现,下面的切面声明所有服务接口的实现者也实现了 UsageTracked
接口(例如,通过 JMX 进行统计):
@Aspect
public class UsageTracking {
@DeclareParents(value="com.xyz.service.*+", defaultImpl=DefaultUsageTracked.class)
public static UsageTracked mixin;
@Before("execution(* com.xyz..service.*.*(..)) && this(usageTracked)")
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
}
要实现的接口由注解字段的类型决定。@DeclareParents
注解的 value
属性是一个 AspectJ 类型模式。任何匹配类型的 bean 都实现了 UsageTracked
接口。注意,在前述示例的前置通知中,服务 bean 可以直接用作 UsageTracked
接口的实现。如果以编程方式访问一个 bean,你会写下如下代码:
UsageTracked usageTracked = context.getBean("myService", UsageTracked.class);
切面实例化模型(Aspect Instantiation Models)
默认情况下,在应用程序上下文中每个切面都有一个实例。AspectJ 称这为单例实例化模型。可以定义具有替代生命周期的切面。Spring 支持 AspectJ 的 perthis
、pertarget
和 pertypewithin
实例化模型;目前不支持 percflow
和 percflowbelow
。
你可以通过在 @Aspect
注解中指定 perthis
子句来声明一个 perthis
切面。考虑下面的例子:
@Aspect("perthis(execution(* com.xyz..service.*.*(..)))")
public class MyAspect {
private int someState;
@Before("execution(* com.xyz..service.*.*(..))")
public void recordServiceUsage() {
// ...
}
}
在前述示例中,perthis
子句的效果是为每个执行业务服务的唯一服务对象(在切入点表达式匹配的连接点上绑定到 this
的唯一对象)创建一个切面实例。当第一次在服务对象上调用方法时,会创建切面实例。当服务对象超出作用域时,切面也随之超出作用域。在创建切面实例之前,其内的任何通知都不会运行。一旦创建了切面实例,只要服务对象是与该切面关联的对象,其内声明的通知就会在匹配的连接点运行。
pertarget
实例化模型的工作原理与 perthis
完全相同,但它为匹配连接点上的每个唯一目标对象创建一个切面实例。
一个AOP示例
业务服务的执行有时会因为并发问题(例如,死锁失败者)而失败。如果操作被重试,很可能在下一次尝试时成功。对于那些在这种情况下适合重试的业务服务(不需要回到用户那里解决冲突的幂等操作),我们希望透明地重试操作,以避免客户端看到PessimisticLockingFailureException
。这是一个明显跨越服务层中多个服务的需求,因此,通过切面实现是理想的选择。
因为我们想要重试操作,我们需要使用环绕通知,这样我们可以多次调用 proceed
。下面的列表显示了基本的切面实现:
@Aspect
public class ConcurrentOperationExecutor implements Ordered {
private static final int DEFAULT_MAX_RETRIES = 2;
private int maxRetries = DEFAULT_MAX_RETRIES;
private int order = 1;
public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
@Around("com.xyz.CommonPointcuts.businessService()")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
int numAttempts = 0;
PessimisticLockingFailureException lockFailureException;
do {
numAttempts++;
try {
return pjp.proceed();
}
catch(PessimisticLockingFailureException ex) {
lockFailureException = ex;
}
} while(numAttempts <= this.maxRetries);
throw lockFailureException;
}
}
切面实现了 Ordered
接口,以便我们可以将切面的优先级设置为高于事务通知(我们希望每次重试时都有一个新事务)。maxRetries
和 order
属性都由 Spring 配置。主要操作发生在 doConcurrentOperation
环绕通知中。注意,目前将重试逻辑应用于每个 businessService
。尝试进行操作,如果因 PessimisticLockingFailureException
失败,会再次尝试,除非已经用尽了所有重试尝试。
相应的 Spring 配置如下:
<aop:aspectj-autoproxy/>
<bean id="concurrentOperationExecutor"
class="com.xyz.service.impl.ConcurrentOperationExecutor">
<property name="maxRetries" value="3"/>
<property name="order" value="100"/>
</bean>
为了完善切面,使其只重试幂等操作,我们可以定义以下 Idempotent
注解:
@Retention(RetentionPolicy.RUNTIME)
// marker annotation
public @interface Idempotent {
}
然后我们可以使用注解来标注服务操作的实现。修改切面以仅重试幂等操作涉及完善切入点表达式,以便只有 @Idempotent
操作匹配,如下所示:
@Around("execution(* com.xyz..service.*.*(..)) && " +
"@annotation(com.xyz.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
// ...
}
基于模式的 AOP 支持
如果你更喜欢基于 XML 的格式,Spring 也支持使用 aop
命名空间标签定义切面。支持与使用 @AspectJ 风格时完全相同的切入点表达式和通知类型。
要使用本节中描述的 aop
命名空间标签,需要导入 spring-aop
模式。
在你的 Spring 配置中,所有的 aspect 和 advisor 元素必须放置在 <aop:config>
元素内(在一个应用上下文配置中可以有多个 <aop:config>
元素)。<aop:config>
元素可以包含 pointcut、advisor 和 aspect 元素(注意,这些必须按该顺序声明)。
<aop:config>
风格的配置大量使用了 Spring 的自动代理机制。如果你已经通过使用 BeanNameAutoProxyCreator
或类似的东西显式使用自动代理,这可能会导致问题(例如通知没有被编织)。推荐使用的模式是仅使用<aop:config>
风格或仅使用 AutoProxyCreator
风格,并且永远不要混合使用它们。
声明切面(Declaring an Aspect)
当使用模式支持时,切面是在你的 Spring 应用上下文中定义为 bean 的常规 Java 对象。状态和行为被捕获在对象的字段和方法中,切入点和通知信息被捕获在 XML 中。
可以使用 <aop:aspect>
元素声明切面,并使用 ref
属性引用支持 bean,如下例所示:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
...
</aop:aspect>
</aop:config>
<bean id="aBean" class="...">
...
</bean>
支持切面的 bean(本例中的 aBean
)当然可以像任何其它 Spring bean 一样进行配置和依赖注入。
声明切入点(Declaring a Pointcut)
可以在 <aop:config>
元素内部声明一个命名切入点,让切入点定义在多个切面和通知器之间共享。
可以如下定义一个切入点,它表示服务层中任何业务服务的执行:
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..))" />
</aop:config>
切入点表达式本身使用了与 @AspectJ 支持中描述的相同的 AspectJ 切入点表达式语言。如果使用基于模式的声明风格,还可以在切入点表达式中引用 @Aspect
类型中定义的命名切入点。因此,定义上述切入点的另一种方式如下:
<aop:config>
<aop:pointcut id="businessService"
expression="com.xyz.CommonPointcuts.businessService()" />
</aop:config>
在切面内部声明切入点与声明顶级切入点非常相似,如下例所示:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..))"/>
...
</aop:aspect>
</aop:config>
与 @AspectJ 切面类似,使用基于模式的定义风格声明的切入点可以收集连接点上下文。例如,以下切入点收集 this
对象作为连接点上下文并将其传递给通知:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..)) && this(service)"/>
<aop:before pointcut-ref="businessService" method="monitor"/>
...
</aop:aspect>
</aop:config>
通知必须声明为接收收集的连接点上下文,包括具有匹配名称的参数,如下所示:
public void monitor(Object service) {
// ...
}
组合切入点子表达式时,&&
符号在 XML 文档中显得笨拙,因此可以使用 and
、or
和 not
关键字分别代替 &&
、||
和 !
。例如,前面的切入点可以更好地写成如下形式:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..)) and this(service)"/>
<aop:before pointcut-ref="businessService" method="monitor"/>
...
</aop:aspect>
</aop:config>
以这种方式定义的切入点是通过它们的 XML id
引用的,不能用作命名切入点来形成复合切入点。因此,基于模式的定义风格中的命名切入点支持比 @AspectJ 风格提供的更为有限。
声明通知(Declaring Advice)
基于模式的 AOP 支持使用与 @AspectJ 风格相同的五种通知类型,并且它们具有完全相同的语义。
前置通知(Before Advice)
前置通知在匹配的方法执行之前运行。它通过使用 <aop:before>
元素声明在 <aop:aspect>
内部,如下例所示:
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut-ref="dataAccessOperation"
method="doAccessCheck"/>
...
</aop:aspect>
在上面的示例中,dataAccessOperation
是在顶层(<aop:config>
)定义的命名切入点的 id
。
使用命名切入点可以显著提高代码的可读性。
要内联定义切入点,只需用 pointcut
属性替换 pointcut-ref
属性,如下所示:
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut="execution(* com.xyz.dao.*.*(..))"
method="doAccessCheck"/>
...
</aop:aspect>
method
属性标识了一个方法(doAccessCheck
),该方法提供了通知的主体。这个方法必须为包含通知的切面元素所引用的 bean 定义。在执行数据访问操作之前(由切入点表达式匹配的方法执行连接点),会调用切面 bean 上的 doAccessCheck
方法。
返回后通知(After Returning Advice)
返回后通知在匹配的方法执行正常完成时运行。它在 <aop:aspect>
内部的声明方式与前置通知相同。以下示例展示了如何声明它:
<aop:aspect id="afterReturningExample" ref="aBean">
<aop:after-returning
pointcut="execution(* com.xyz.dao.*.*(..))"
method="doAccessCheck"/>
...
</aop:aspect>
与 @AspectJ 风格一样,你可以在通知体内部获取返回值。为此,使用 returning
属性指定应将返回值传递给的参数的名称,如下例所示:
<aop:aspect id="afterReturningExample" ref="aBean">
<aop:after-returning
pointcut="execution(* com.xyz.dao.*.*(..))"
returning="retVal"
method="doAccessCheck"/>
...
</aop:aspect>
doAccessCheck
方法必须声明一个名为 retVal
的参数。此参数的类型以与 @AfterReturning
描述的方式相同的方式约束匹配。例如,可以按如下方式声明方法签名:
public void doAccessCheck(Object retVal) {...
抛出异常后通知(After Throwing Advice)
抛出异常后通知在匹配的方法执行通过抛出异常退出时运行。它通过使用 after-throwing
元素声明在 <aop:aspect>
内部,如下例所示:
<aop:aspect id="afterThrowingExample" ref="aBean">
<aop:after-throwing
pointcut="execution(* com.xyz.dao.*.*(..))"
method="doRecoveryActions"/>
...
</aop:aspect>
与 @AspectJ 风格一样,你可以在通知体内部获取抛出的异常。为此,使用 throwing
属性指定应将异常传递给的参数的名称,如下例所示:
<aop:aspect id="afterThrowingExample" ref="aBean">
<aop:after-throwing
pointcut="execution(* com.xyz.dao.*.*(..))"
throwing="dataAccessEx"
method="doRecoveryActions"/>
...
</aop:aspect>
doRecoveryActions
方法必须声明一个名为 dataAccessEx
的参数。此参数的类型以与 @AfterThrowing
描述的方式相同的方式约束匹配。例如,可以按如下方式声明方法签名:
public void doRecoveryActions(DataAccessException dataAccessEx) {...
最终(最后)通知(After (Finally) Advice)
最终(最后)通知无论匹配的方法执行如何退出都会运行。你可以使用 after
元素声明它,如下例所示:
<aop:aspect id="afterFinallyExample" ref="aBean">
<aop:after
pointcut="execution(* com.xyz.dao.*.*(..))"
method="doReleaseLock"/>
...
</aop:aspect>
环绕通知(Around Advice)
环绕通知在匹配的方法执行的“周围”运行。它有机会在方法运行之前和之后执行工作,并确定何时、如何,甚至是否允许该方法实际运行。如果需要在方法执行之前和之后以线程安全的方式共享状态,例如启动和停止计时器,通常会使用环绕通知。
始终使用满足你需求的最简单形式的通知。
例如,如果前置通知已足够满足你的需求,就不要使用环绕通知。
可以通过使用 aop:around
元素声明环绕通知。通知方法应声明 Object
作为其返回类型,且该方法的第一个参数必须是 ProceedingJoinPoint
类型。在通知方法体内,必须调用 ProceedingJoinPoint
上的 proceed()
方法以便底层方法运行。不带参数地调用 proceed()
将导致调用者原始参数在底层方法被调用时提供给它。对于高级情况,proceed()
方法有一个接受参数数组(Object[]
)的重载变体。数组中的值将在底层方法被调用时用作参数。
以下示例展示了如何在 XML 中声明环绕通知:
<aop:aspect id="aroundExample" ref="aBean">
<aop:around
pointcut="execution(* com.xyz.service.*.*(..))"
method="doBasicProfiling"/>
...
</aop:aspect>
doBasicProfiling
通知的实现可以如下例所示:
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
通知参数(Advice Parameters)
基于模式的声明风格支持全类型通知,就像为 @AspectJ 支持所描述的那样——通过将切入点参数的名称与通知方法参数进行匹配。如果希望明确指定通知方法的参数名称(不依赖于之前描述的检测策略),可以通过使用通知元素的 arg-names
属性来实现,其处理方式与通知注解中的 argNames
属性相同。以下示例展示了如何在 XML 中指定参数名称:
<aop:before
pointcut="com.xyz.Pointcuts.publicMethod() and @annotation(auditable)"
method="audit"
arg-names="auditable" />
arg-names
属性接受逗号分隔的参数名称列表。
以下稍微复杂一些的基于 XSD 的方法示例展示了一些与多个强类型参数结合使用的环绕通知:
package com.xyz.service;
public interface PersonService {
Person getPerson(String personName, int age);
}
public class DefaultPersonService implements PersonService {
public Person getPerson(String name, int age) {
return new Person(name, age);
}
}
接下来是切面。请注意,profile(..)
方法接受多个强类型参数,其中第一个恰好是用于继续进行方法调用的连接点。这个参数的存在表明 profile(..)
将作为环绕通知使用,如下例所示:
package com.xyz;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;
public class SimpleProfiler {
public Object profile(ProceedingJoinPoint call, String name, int age) throws Throwable {
StopWatch clock = new StopWatch("Profiling for '" + name + "' and '" + age + "'");
try {
clock.start(call.toShortString());
return call.proceed();
} finally {
clock.stop();
System.out.println(clock.prettyPrint());
}
}
}
最后,以下示例 XML 配置实现了对特定连接点执行前面通知的效果:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- this is the object that will be proxied by Spring's AOP infrastructure -->
<bean id="personService" class="com.xyz.service.DefaultPersonService"/>
<!-- this is the actual advice itself -->
<bean id="profiler" class="com.xyz.SimpleProfiler"/>
<aop:config>
<aop:aspect ref="profiler">
<aop:pointcut id="theExecutionOfSomePersonServiceMethod"
expression="execution(* com.xyz.service.PersonService.getPerson(String,int))
and args(name, age)"/>
<aop:around pointcut-ref="theExecutionOfSomePersonServiceMethod"
method="profile"/>
</aop:aspect>
</aop:config>
</beans>
考虑以下驱动脚本:
public class Boot {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
PersonService person = ctx.getBean(PersonService.class);
person.getPerson("Pengo", 12);
}
}
有了这样的Boot
类,我们可以得到类似如下的标准输出:
StopWatch 'Profiling for 'Pengo' and '12': running time (millis) = 0
-----------------------------------------
ms % Task name
-----------------------------------------
00000 ? execution(getFoo)
通知排序(Advice Ordering)
方面之间的优先级通过<aop:aspect>
元素中的order
属性确定,或者通过在支持方面的bean中添加@Order注
解,或让bean实现Ordered
接口来确定。
与同一@Aspect
类中定义的通知方法的优先级规则相反,当同一<aop:aspect>
元素中定义的两个通知都需要在同一连接点上运行时,优先级由通知元素在包围的<aop:aspect>
元素内声明的顺序决定,从最高到最低优先级。
例如,给定一个环绕通知和一个前置通知,它们定义在同一<aop:aspect>
元素中并应用于同一连接点,要确保环绕通知的优先级高于前置通知,必须在<aop:before>
元素之前声明<aop:around>
元素。
作为一般经验法则,如果你发现在同一<aop:aspect>
元素中定义了多个适用于同一连接点的通知,请考虑将这样的通知方法合并为每个<aop:aspect>
元素中每个连接点一个通知方法,或者将通知重构为单独的<aop:aspect>
元素,这样你可以在方面级别对它们进行排序。
引入(Introductions)
引入(在AspectJ中称为inter-type声明)允许一个切面声明被通知对象实现一个给定的接口,并为这些对象提供该接口的一个实现。
你可以通过在aop: aspect
内部使用aop: declare-parents
元素来做一个引入。你可以使用aop: declare-parents
元素来声明匹配类型有一个新的父类型(因此得名)。例如,假设有一个名为UsageTracked
的接口和一个名为DefaultUsageTracked
的实现类,下面的切面声明所有service接口的实现者也实现了UsageTracked
接口。(比如,为了通过JMX公开统计信息)
<aop:aspect id=“usageTrackerAspect” ref=“usageTracking”>
<aop:declare-parents
types-matching="com.xyz.service.*+"
implement-interface="com.xyz.service.tracking.UsageTracked"
default-impl="com.xyz.service.tracking.DefaultUsageTracked"/>
<aop:before
pointcut="execution(* com.xyz..service.*.*(..))
and this(usageTracked)"
method="recordUsage"/>
</aop:aspect>
支持usageTracking
bean的类将包含以下方法:
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
要实现的接口由implement-interface
属性决定。types-matching
属性的值是一个AspectJ类型模式。任何匹配类型的bean都实现了UsageTracked
接口。注意,在之前示例的before advice中,service beans可以直接作为UsageTracked
接口的实现类使用。要以编程方式访问一个bean,你可以编写以下代码:
UsageTracked usageTracked = context.getBean("myService", UsageTracked.class);
切面实例化模型
对于模式定义的切面,唯一支持的实例化模型是单例模型。其它实例化模型可能会在未来的版本中得到支持。
顾问(Advisors)
“顾问(Advisors)”概念源自Spring中定义的AOP支持,并没有在AspectJ中直接等价的概念。顾问就像一个小型自包含的切面,只包含一条通知。通知本身由一个bean表示,并且必须实现Spring中的Advice Types所描述的通知接口之一。顾问可以利用AspectJ切入点表达式。
Spring通过aop:advisor
元素支持顾问概念。你最常见的是它与事务性通知一起使用,事务性通知在Spring中也有自己的命名空间支持。以下示例展示了一个顾问:
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..))"/>
<aop:advisor
pointcut-ref="businessService"
advice-ref="tx-advice" />
</aop:config>
<tx:advice id="tx-advice">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
除了前面示例中使用的pointcut-ref
属性,你也可以使用pointcut
属性来内联定义一个切入点表达式。
要定义顾问的优先级,以便通知可以参与排序,使用order
属性来定义顾问的Ordered
值。
一个AOP模式示例
业务服务的执行有时会因为并发问题(例如死锁失败者)而失败。如果操作被重试,很可能在下一次尝试中成功。对于在这些条件下适合重试的业务服务(不需要返回给用户解决冲突的幂等操作),我们希望透明地重试操作,以避免客户端看到PessimisticLockingFailureException
。这是一个明显跨越服务层多个服务的要求,因此,通过切面实现是理想的选择。
因为我们想要重试操作,我们需要使用环绕通知,这样我们可以多次调用proceed
。下面的列表显示了基本的切面实现(这是一个使用模式支持的常规Java类):
public class ConcurrentOperationExecutor implements Ordered {
private static final int DEFAULT_MAX_RETRIES = 2;
private int maxRetries = DEFAULT_MAX_RETRIES;
private int order = 1;
public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
int numAttempts = 0;
PessimisticLockingFailureException lockFailureException;
do {
numAttempts++;
try {
return pjp.proceed();
}
catch(PessimisticLockingFailureException ex) {
lockFailureException = ex;
}
} while(numAttempts <= this.maxRetries);
throw lockFailureException;
}
}
注意,切面实现了Ordered
接口,这样我们就可以将切面的优先级设置得比事务通知更高(我们希望每次重试时都是一个新的事务)。maxRetries
和order
属性都由Spring配置。主要操作发生在doConcurrentOperation
环绕通知方法中。我们尝试继续进行。如果我们遇到了PessimisticLockingFailureException
失败,我们会再次尝试,除非我们已经用尽了所有的重试尝试。
这个类与@AspectJ示例中使用的类相同,只是去掉了注解。
相应的Spring配置如下:
<aop:config>
<aop:aspect id="concurrentOperationRetry" ref="concurrentOperationExecutor">
<aop:pointcut id="idempotentOperation"
expression="execution(* com.xyz.service.*.*(..))"/>
<aop:around
pointcut-ref="idempotentOperation"
method="doConcurrentOperation"/>
</aop:aspect>
</aop:config>
<bean id="concurrentOperationExecutor"
class="com.xyz.service.impl.ConcurrentOperationExecutor">
<property name="maxRetries" value="3"/>
<property name="order" value="100"/>
</bean>
目前我们假设所有业务服务都是幂等的。如果不是这样,我们可以完善切面,使其只重试真正幂等的操作,通过引入Idempotent
注解并使用该注解来标注服务操作的实现,如下例所示:
@Retention(RetentionPolicy.RUNTIME)
// marker annotation
public @interface Idempotent {
}
要修改切面以便只重试幂等操作,需要完善切入点表达式,使其只匹配@Idempotent
操作,如下所示:
<aop:pointcut id="idempotentOperation"
expression="execution(* com.xyz.service.*.*(..)) and
@annotation(com.xyz.service.Idempotent)"/>
选择使用哪种AOP声明风格
一旦你决定切面是实现某个需求的最佳方法,你如何在Spring AOP和AspectJ之间以及在Aspect语言(代码)风格、@AspectJ注解风格或Spring XML风格之间做出选择?这些决策受到多种因素的影响,包括应用程序需求、开发工具以及团队对AOP的熟悉程度。
Spring AOP还是完整的AspectJ?
使用最简单的可行方案。Spring AOP比使用完整的AspectJ简单,因为它不需要在你的开发和构建过程中引入AspectJ编译器/编织器。如果你只需要对Spring beans的操作提供通知,Spring AOP是正确的选择。如果你需要对非Spring容器管理的对象(例如通常的领域对象)提供通知,你需要使用AspectJ。如果你希望对除了简单方法执行之外的连接点(例如,字段获取或设置连接点等)提供通知,你也需要使用AspectJ。
当你使用AspectJ时,你可以选择AspectJ语言语法(也称为“代码风格”)或@AspectJ注解风格。如果切面在你的设计中扮演重要角色,并且你可以使用Eclipse的AspectJ Development Tools(AJDT)插件,那么AspectJ语言语法是首选选项。它更简洁、更简单,因为这种语言是专门为编写切面而设计的。如果你不使用Eclipse,或者只有少数几个在应用程序中不起主要作用的切面,你可能想考虑使用@AspectJ风格,坚持在你的IDE中使用常规的Java编译,并在你的构建脚本中添加一个切面编织阶段。
@AspectJ还是Spring AOP的XML?
如果你选择了使用Spring AOP,你可以选择@AspectJ风格或XML风格。有各种权衡需要考虑。
XML风格可能是现有Spring用户最熟悉的,它由真正的POJO支持。当将AOP作为配置企业服务的工具时,XML可能是一个不错的选择(一个好的测试是你是否认为切入点表达式是你可能想要独立更改的配置的一部分)。使用XML风格,从你的配置中可以更清楚地看出系统中存在哪些切面。
XML风格有两个缺点。首先,它没有将所处理需求的实现完全封装在一个地方。DRY原则(Don’t Repeat Yourself,不要重复自己)指出,系统中任何知识都应该有一个单一、明确、权威的表示。使用XML风格时,关于需求如何实现的知识被分散在后台bean类的声明和配置文件中的XML之间。当你使用@AspectJ风格时,这些信息被封装在一个模块中:切面。其次,XML风格在表达上稍微有些限制,不如@AspectJ风格那么灵活:只支持“单例”切面实例化模型,并且无法组合在XML中声明的命名切入点。例如,在使用@AspectJ风格时,你可以编写如下内容:
@Pointcut("execution(* get*())")
public void propertyAccess() {}
@Pointcut("execution(com.xyz.Account+ *(..))")
public void operationReturningAnAccount() {}
@Pointcut("propertyAccess() && operationReturningAnAccount()")
public void accountPropertyAccess() {}
在XML风格中,你可以声明前两个切入点:
<aop:pointcut id="propertyAccess"
expression="execution(* get*())"/>
<aop:pointcut id="operationReturningAnAccount"
expression="execution(com.xyz.Account+ *(..))"/>
XML方法的缺点是你不能通过组合这些定义来定义accountPropertyAccess
切入点。
@AspectJ风格支持额外的实例化模型和更丰富的切入点组合。它的优势在于保持切面作为一个模块化单元。它还具有@AspectJ切面可以被Spring AOP和AspectJ理解(因此也可以被消费)的优势。所以,如果你后来决定你需要AspectJ的功能来实现额外的需求,你可以很容易地迁移到经典的AspectJ设置。总的来说,Spring团队更倾向于使用@AspectJ风格来自定义切面,而不是简单的配置企业服务。
混合使用切面类型
在同一配置中混合使用@AspectJ风格的切面、自动代理支持、模式定义的aop:aspect
切面、声明的aop:advisor
顾问,甚至是其它风格的代理和拦截器是完全可能的。所有这些都是通过使用相同的底层支持机制实现的,可以毫无困难地共存。
代理机制
Spring AOP使用JDK动态代理或CGLIB来为目标对象创建代理。JDK动态代理是内置于JDK中的,而CGLIB是一个通用的开源类定义库(重新打包进spring-core
)。
如果要代理的目标对象实现了至少一个接口,就会使用JDK动态代理。目标类型实现的所有接口都会被代理。如果目标对象没有实现任何接口,就会创建一个CGLIB代理。
如果你想强制使用CGLIB代理(例如,为了代理目标对象定义的每个方法,而不仅仅是其接口实现的方法),你可以这样做。但是,你应该考虑以下几个问题:
- 使用CGLIB时,
final
方法不能被增强,因为它们不能在运行时生成的子类中被覆盖。 - 从Spring 4.0开始,被代理对象的构造函数不再被调用两次,因为CGLIB代理实例是通过Objenesis创建的。只有当你的JVM不允许绕过构造函数时,你可能会看到双重调用和来自Spring AOP支持的相应调试日志条目。
要强制使用CGLIB代理,请将<aop:config>
元素的proxy-target-class
属性值设置为true
,如下所示:
<aop:config proxy-target-class="true">
<!-- other beans defined here... -->
</aop:config>
当你使用@AspectJ自动代理支持时,要强制使用CGLIB代理,请将<aop:aspectj-autoproxy>
元素的proxy-target-class
属性设置为true
,如下所示:
<aop:aspectj-autoproxy proxy-target-class="true"/>
多个<aop:config/>
部分在运行时会合并成一个统一的自动代理创建器,它应用任何一个<aop:config/>
部分(通常来自不同的XML bean定义文件)指定的最强代理设置。这同样适用于<tx:annotation-driven/>
和<aop:aspectj-autoproxy/>
元素。
明确地说,在<tx:annotation-driven/>
、<aop:aspectj-autoproxy/>
或<aop:config/>元
素上使用proxy-target-class="true"
会强制对它们三个都使用CGLIB代理。
理解AOP代理
Spring AOP是基于代理的。
首先考虑这样一个场景,你有一个普通的对象引用,没有被代理,没有什么特别之处,就像下面的代码片段所示:
public class SimplePojo implements Pojo {
public void foo() {
// this next method invocation is a direct call on the 'this' reference
this.bar();
}
public void bar() {
// some logic...
}
}
如果你在对象引用上调用一个方法,该方法会直接在该对象引用上被调用,如下图所示和列表所示:
public class Main {
public static void main(String[] args) {
Pojo pojo = new SimplePojo();
// this is a direct method call on the 'pojo' reference
pojo.foo();
}
}
当客户端代码持有的引用是一个代理时,情况会略有变化。考虑下面的图表和代码片段:
public class Main {
public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory(new SimplePojo());
factory.addInterface(Pojo.class);
factory.addAdvice(new RetryAdvice());
Pojo pojo = (Pojo) factory.getProxy();
// this is a method call on the proxy!
pojo.foo();
}
}
这里需要理解的关键点是,Main
类中的main(..)
方法内的客户端代码持有对代理的引用。这意味着对该对象引用的方法调用实际上是在代理上进行的。因此,代理可以将调用委派给与该特定方法调用相关的所有拦截器(通知(advice))。然而,一旦调用最终到达目标对象(在这个例子中是SimplePojo
引用),它可能对自己进行的任何方法调用,比如this.bar()
或this.foo()
,都将针对this
引用进行调用,而不是代理。这有重要的影响。这意味着自我调用不会导致与方法调用相关联的通知有机会运行。
那么,对此我们该怎么办呢?好的方法是重构你的代码,避免自我调用的发生。这需要你做一些工作,但这是最好、最不侵入性的方法。另一种不好的方法是你可以将类内的逻辑完全与Spring AOP绑定,如下例所示:
public class SimplePojo implements Pojo {
public void foo() {
// this works, but... gah!
((Pojo) AopContext.currentProxy()).bar();
}
public void bar() {
// some logic...
}
}
这完全将你的代码与Spring AOP耦合,并且使得类本身意识到它正在被用于AOP上下文中,这与AOP的初衷背道而驰。它还需要在创建代理时进行一些额外的配置,如下例所示:
public class Main {
public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory(new SimplePojo());
factory.addInterface(Pojo.class);
factory.addAdvice(new RetryAdvice());
factory.setExposeProxy(true);
Pojo pojo = (Pojo) factory.getProxy();
// this is a method call on the proxy!
pojo.foo();
}
}
最后,必须指出的是,AspectJ没有这种自我调用问题,因为它不是一个基于代理的AOP框架。
编程方式创建@AspectJ代理
除了在配置中使用<aop:config>
或<aop:aspectj-autoproxy>
声明切面之外,还可以通过编程方式创建代理来通知目标对象。在这里,我们想要关注使用@AspectJ切面自动创建代理的能力。
你可以使用org.springframework.aop.aspectj.annotation.AspectJProxyFactory
类来为目标对象创建一个代理,该对象由一个或多个@AspectJ切面通知。这个类的基本用法非常简单,如下例所示:
// create a factory that can generate a proxy for the given target object
AspectJProxyFactory factory = new AspectJProxyFactory(targetObject);
// add an aspect, the class must be an @AspectJ aspect
// you can call this as many times as you need with different aspects
factory.addAspect(SecurityManager.class);
// you can also add existing aspect instances, the type of the object supplied
// must be an @AspectJ aspect
factory.addAspect(usageTracker);
// now get the proxy object...
MyInterfaceType proxy = factory.getProxy();
在Spring应用程序中使用AspectJ
到目前为止,我们在本章中介绍的内容都是纯粹的Spring AOP。在这一节中,我们将探讨如何使用AspectJ编译器或编织器来替代或补充Spring AOP,如果你的需求超出了Spring AOP本身所提供的功能。
Spring附带了一个小型的AspectJ切面库,它作为spring-aspects.jar
在分发版中独立提供。你需要将其添加到类路径中才能使用其中的切面。
使用AspectJ与Spring依赖注入领域对象
Spring容器实例化并配置在应用程序上下文中定义的bean。也可以要求bean工厂配置一个预先存在的对象,给定一个包含要应用的配置的bean定义的名称。spring-aspects.jar
包含了一个注解驱动的切面,利用这一能力来实现任何对象的依赖注入。该支持旨在用于在任何容器控制之外创建的对象。领域对象通常属于这一类,因为它们通常是通过new
操作符编程创建的,或者是由ORM工具作为数据库查询的结果创建的。
@Configurable
注解标记一个类,使其适用于Spring驱动的配置。在最简单的情况下,你可以将其仅用作标记注解,如下例所示:
package com.xyz.domain;
import org.springframework.beans.factory.annotation.Configurable;
@Configurable
public 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定义的名称,可以直接在注解中进行指定,如下例所示:
package com.xyz.domain;
import org.springframework.beans.factory.annotation.Configurable;
@Configurable("account")
public 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
声明中定义这一点,如下所示:
@Configurable(preConstruction = true)
要使这一功能正常工作,必须使用AspectJ编织器对注解类型进行编织。你可以使用Ant或Maven的构建时任务来完成这一点,或者使用加载时编织。AnnotationBeanConfigurerAspect
本身需要由Spring配置(以获取用于配置新对象的bean工厂的引用)。如果你使用基于Java的配置,可以在任何@Configuration
类中添加@EnableSpringConfigured
,如下所示:
@Configuration
@EnableSpringConfigured
public class AppConfig {
}
如果你更喜欢基于XML的配置,Spring上下文命名空间定义了一个方便的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编织,那么在单元测试期间注解不会有任何影响。你可以在被测试对象中设置模拟或桩(stub)属性引用,并像往常一样继续进行。如果@Configurable
类型已经被AspectJ编织,你仍然可以像往常一样在容器外部进行单元测试,但每次构造@Configurable
对象时,你会看到一个警告消息,表明它尚未由Spring配置。
使用多个应用程序上下文
用于实现@Configurable
支持的AnnotationBeanConfigurerAspect
是一个AspectJ单例切面。单例切面的范围与静态成员的范围相同:每个定义类型的ClassLoader
都有一个切面实例。这意味着,如果你在同一个ClassLoader
层次结构中定义了多个应用程序上下文,你需要考虑在哪里定义@EnableSpringConfigured
bean以及在类路径上放置spring-aspects.jar
的位置。
考虑一个典型的Spring Web应用程序配置,它有一个共享的父应用程序上下文,定义了通用的业务服务、支持这些服务所需的一切,以及每个servlet的一个子应用程序上下文(包含特定于该servlet的定义)。所有这些上下文都存在于相同的ClassLoader
层次结构中,因此AnnotationBeanConfigurerAspect
只能持有其中一个的引用。在这种情况下,建议在共享的(父)应用程序上下文中定义@EnableSpringConfigured
bean。这定义了你可能想要注入到领域对象中的服务。一个后果是,你不能使用@Configurable
机制配置领域对象,使其引用在子(特定于servlet的)上下文中定义的bean(这可能也不是你想要做的事情)。
在同一容器中部署多个Web应用程序时,请确保每个Web应用程序通过使用自己的ClassLoader
(例如,将spring-aspects.jar
放置在WEB-INF/lib
中)加载spring-aspects.jar
中的类型。如果spring-aspects.jar
仅添加到容器范围的类路径中(因此由共享的父ClassLoader
加载),所有Web应用程序将共享相同的切面实例(这可能不是你想要的)。
AspectJ的其它Spring切面(aspects )
除了@Configurable
切面之外,spring-aspects.jar
还包含一个AspectJ
切面,你可以使用它来驱动Spring的事务管理,用于被@Transactional
注解标记的类型和方法。这主要是为那些想在Spring容器外部使用Spring框架事务支持的用户设计的。
解释@Transactional
注解的切面是AnnotationTransactionAspect
。当你使用这个切面时,你必须对实现类(或该类中的方法或两者)进行注解,而不是该类实现的接口(如果有的话)。AspectJ遵循Java的规则,即接口上的注解不会被继承。
在类上使用@Transactional
注解指定了该类中任何公共操作执行的默认事务语义。
在类内部的方法上使用@Transactional
注解会覆盖类注解给出的默认事务语义(如果存在的话)。任何可见性的方法都可以被注解,包括私有方法。直接注解非公共方法是获得这些方法执行的事务界定的唯一方式。
自Spring框架4.2起,spring-aspects
提供了一个类似的切面,为标准的jakarta.transaction.Transactional
注解提供了完全相同的功能。
对于想要使用Spring配置和事务管理支持但不想(或不能)使用注解的AspectJ程序员,spring-aspects.jar
还包含了你可以扩展的抽象切面,以提供你自己的切入点定义。作为示例,以下摘录显示了如何编写一个切面,通过使用与完全限定类名匹配的原型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">
<property name="profilingStrategy" ref="jamonProfilingStrategy"/>
</bean>
非单例切面更难配置。然而,通过创建原型bean定义并使用spring-aspects.jar
中的@Configurable
支持,一旦AspectJ运行时创建了切面实例,就可以对它们进行配置。
如果你有一些@AspectJ切面想要用AspectJ编织(例如,使用加载时编织来处理领域模型类型),以及其它一些你想要与Spring AOP一起使用的@AspectJ切面,并且这些切面都在Spring中配置,你需要告诉Spring AOP @AspectJ自动代理支持,在配置中定义的@AspectJ切面的确切子集中,哪些应该用于自动代理。你可以通过在<aop:aspectj-autoproxy/>
声明内部使用一个或多个<include/>
元素来实现这一点。每个元素指定一个名称模式,只有那些至少匹配其中一个模式的bean才会被用于Spring AOP自动代理配置。以下示例显示了如何使用元素:
<aop:aspectj-autoproxy>
<aop:include name="thisBean"/>
<aop:include name="thatBean"/>
</aop:aspectj-autoproxy>
不要被<aop:aspectj-autoproxy/>
元素的名称所误导。使用它会导致创建Spring AOP代理。这里使用的是@AspectJ风格的切面声明,但并不涉及AspectJ运行时。
在Spring框架中使用AspectJ进行加载时编织
加载时编织(LTW)是指在将应用程序的类文件加载到Java虚拟机(JVM)时,将AspectJ切面编织进去的过程。本节的重点是在Spring框架的特定上下文中配置和使用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
。开发人员配置应用程序上下文以启用加载时编织,而不是依赖于通常负责部署配置的管理员,例如启动脚本。
第一个示例
假设你是一名应用程序开发人员,负责诊断系统性能问题的原因。我们不打算使用性能分析工具,而是打算开启一个简单的性能分析切面,以便我们快速获取一些性能指标。然后我们可以立即对特定区域应用更细粒度的性能分析工具。
这里展示的例子使用了XML配置。你也可以使用Java配置来配置和使用@AspectJ。具体来说,你可以使用@EnableLoadTimeWeaving
注解作为<context:load-time-weaver/>
的替代方案。
以下示例展示了一个并不复杂的性能分析切面。它是一个基于时间的剖析器,使用了@AspectJ风格的切面声明:
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(){}
}
我们还需要创建一个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 转储(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的实际运行情况:
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();
}
}
我们还有最后一件事要做。对于这个例子,我们使用一个Java代理(随Spring提供)来开启LTW。我们使用以下命令来运行前面展示的Main
类:
java -javaagent:C:/projects/xyz/lib/spring-instrument.jar com.xyz.Main
-javaagent
是一个标志,用于指定和启用代理以检测在JVM上运行的程序。Spring框架附带了一个这样的代理,即InstrumentationSavingAgent
,它被打包在spring-instrument.jar
中,该jar文件是前面示例中-javaagent
参数的值。
主程序的执行输出看起来类似于以下示例。(在calculateEntitlement()
实现中引入了一个Thread.sleep(..)
语句,以便分析器实际捕获到除了0毫秒之外的其它内容(01234
毫秒不是由AOP引入的开销)。以下列表显示了运行分析器时得到的输出:
Calculating entitlement
StopWatch 'ProfilingAspect': running time (millis) = 1234
------ ----- ----------------------------
ms % Task name
------ ----- ----------------------------
01234 100% calculateEntitlement
由于这个轻量级事务织入(LTW)是通过使用完整的AspectJ来实现的,因此我们不仅限于通知Spring bean。对主程序的以下稍微修改也能得到相同的结果:
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();
}
}
请注意,在前一个程序中,我们启动Spring容器,然后在Spring上下文之外完全创建一个StubEntitlementCalculationService
的新实例。分析通知仍然会被织入。
诚然,这个示例很简单。但是,前面示例中已经介绍了Spring中LTW支持的基础知识,本节的其余部分将详细解释每个配置和用法背后的“原因”。
本示例中使用的ProfilingAspect
可能很基础,但非常有用。这是一个很好的开发时方面的示例,开发人员可以在开发过程中使用它,然后很容易地从部署到UAT或生产环境的应用程序的构建中排除它。
方面(Aspects)
在LTW中使用的方面必须是AspectJ方面。可以使用AspectJ语言本身编写它们,也可以使用@AspectJ风格的方面。然后,你的方面既是有效的AspectJ方面,也是Spring AOP方面。此外,编译后的方面类需要在类路径上可用。
META-INF/aop.xml
AspectJ LTW基础设施是通过使用一个或多个位于Java类路径(直接或更常见的是在jar文件中)上的META-INF/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>
</aspectj>
建议仅对特定的类(通常是上述aop.xml
示例中的应用程序包中的类)进行织入,以避免出现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
负责在运行时向ClassLoader
添加一个或多个java.lang.instrument.ClassFileTransformers
,这为各种有趣的应用打开了大门,其中之一恰好是切面的LTW。
为特定ApplicationContext
配置LoadTimeWeaver
可以简单到只需添加一行代码。(请注意,几乎肯定需要使用ApplicationContext
作为你的Spring容器——通常,仅使用BeanFactory
是不够的,因为LTW支持使用了BeanFactoryPostProcessors
。)
要启用Spring Framework的LTW支持,你需要配置一个LoadTimeWeaver
,这通常通过使用@EnableLoadTimeWeaving
注解来完成,如下所示:
@Configuration
@EnableLoadTimeWeaving
public 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
实现:
请注意,表格仅列出了在使用DefaultContextLoadTimeWeaver
时自动检测到的LoadTimeWeavers
。你可以指定要使用哪个LoadTimeWeaver
实现。
要使用Java配置指定特定的LoadTimeWeaver
,请实现LoadTimeWeavingConfigurer
接口并覆盖getLoadTimeWeaver()
方法。以下示例指定了一个ReflectiveLoadTimeWeaver
:
@Configuration
@EnableLoadTimeWeaving
public class AppConfig implements LoadTimeWeavingConfigurer {
@Override
public LoadTimeWeaver getLoadTimeWeaver() {
return new 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的ClassFileTransformer
是ClassPreProcessorAgentAdapter
类(来自org.aspectj.weaver.loadtime
包)。
还有一个配置属性需要讨论:aspectjWeaving
属性(如果你使用XML,则为aspectj-weaving
)。该属性控制是否启用LTW。它接受三个可能的值之一,如果属性不存在,则默认值为autodetect
。下表总结了这三个可能的值:
特定环境的配置
最后一节包含了在应用程序服务器和Web容器等环境中使用Spring的LTW支持时所需的任何额外设置和配置。
Tomcat, JBoss, WildFly
Tomcat和JBoss/WildFly提供了一个通用的app 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设置。