面向切面编程 (AOP) 通过提供另一种思考程序结构的方式来补充面向对象编程 (OOP)。 OOP 中模块化的关键单位是类,而 AOP 中模块化的单位是切面。切面是能够实现跨越多种类型和对象的关注点(例如事务管理)的模块化。 (这种关注点在 AOP 文献中通常被称为“横切”关注点。)
Spring 通过使用schema-based approach或 @AspectJ annotation style, 提供编写自定义切面的简单而强大的方法。
AOP 在 Spring 框架中用于:
- 提供声明式企业服务。最重要是声明式事务管理。
- 让用户实现自定义切面,用 AOP 补充他们对 OOP 的使用。
AOP 概念
AOP 术语:
- 切面
aspect:横切关注点的模块化。事务管理是企业 Java 应用程序中常见的横切关注点。在 Spring AOP 中,切面通过使用常规类(schema-based approach)或使用@Aspect注解(@AspectJstyle)注解 的常规类来实现的。 - 连接点
Join point:在程序执行过程中的一点,例如方法的执行或异常的处理。在 Spring AOP 中,连接点始终代表方法的执行。 - 通知
Advice:切面在特定的连接点处采取的操作。通知的类型包括“around”、“before”和“after”通知。许多 AOP 框架,包括 Spring,将通知建模为拦截器,并在连接点周围维护一个拦截器链。 - 切入点
Pointcut:匹配连接点的谓词。Advice与Pointcut表达式相关联,并在与切入点匹配的任何连接点处运行(例如,执行具有特定名称的方法)。由切入点表达式匹配的连接点的概念是 AOP 的核心,Spring 默认使用 AspectJ 切入点表达式语言。 - 简介
Introduction:代表类型声明其他方法或字段。 Spring AOP 允许您向任何建议的对象引入新的接口(和相应的实现)。例如,可以使用Introduction使 Bean 实现IsModified接口,以简化缓存。 - 目标对象
Target object:一个或多个切面通知的对象,也称为“通知对象”。由于 Spring AOP 是使用运行时代理实现的,因此该对象始终是代理对象。 - AOP 代理
AOP proxy:由 AOP 框架为实现切面协定所创建的对象(通知方法执行等)。在 Spring Framework 中,AOP 代理是 JDK 动态代理或 CGLIB 代理。 - 编织
Weaving: linking aspects with other application types or objects to create an advised object. 这可以在编译时(例如,使用 AspectJ 编译器),加载时或在运行时完成。Spring AOP 在运行时执行编织。
Spring AOP 包括以下类型的通知:
- Before advice:在连接点之前运行的通知,但是它不能阻止执行流程前进到连接点(除非它引发异常)。
- After returning advice:在连接点正常完成后要运行的通知(例如,如果方法返回而没有引发异常)。
- After throwing advice:如果方法因抛出异常而退出,则执行建议。
- after (finally) advice:无论连接点退出的方式如何(正常或特殊返回),均应执行建议。
- Around advice:环绕连接点的通知,例如方法调用。这是最有力的建议。This is the most powerful kind of advice. Around advice can perform custom behavior before and after the method invocation. It is also responsible for choosing whether to proceed to the join point or to shortcut the advised method execution by returning its own return value or throwing an exception.
Around advice is the most general kind of advice. Since Spring AOP, like AspectJ, provides a full range of advice types, we recommend that you use the least powerful advice type that can implement the required behavior. For example, if you need only to update a cache with the return value of a method, you are better off implementing an after returning advice than an around advice, although an around advice can accomplish the same thing. Using the most specific advice type provides a simpler programming model with less potential for errors. For example, you do not need to invoke the proceed() method on the JoinPoint used for around advice, and, hence, you cannot fail to invoke it.
pointcut 匹配 joinpoint 概念是 AOP 的关键,它区别于仅提供拦截的旧技术。切入点使通知能够独立于面向对象的层次结构而成为目标。例如,可以将提供声明式事务管理的环绕通知应用于一组跨越多个对象的方法(例如服务层中的所有业务操作)。
SpringAOP 能力和目标
Spring AOP 是用纯 Java 实现的。不需要特殊的编译过程。 Spring AOP 不需要控制类加载器的层次结构,因此适合在 Servlet 容器或应用程序服务器中使用。
Spring AOP 当前仅支持方法执行连接点(建议在 Spring Bean 上执行方法)。
Spring AOP 的 AOP 方法目的不是提供最完整的 AOP 实现(尽管 Spring AOP 相当强大)。相反,其目的是在 AOP 实现和 Spring IoC 之间提供紧密的集成,以帮助解决企业应用程序中的常见问题。
因此,通常将 Spring Framework 的 AOP 功能与 Spring IoC 容器结合使用。通过使用常规 bean 定义语法来配置切面。这是与其他 AOP 实现的关键区别。
Spring AOP 从未努力与 AspectJ 竞争以提供全面的 AOP 解决方案。我们认为,基于代理的框架(如 Spring AOP)和成熟的框架(如 AspectJ)都是有价值的,它们是互补的,而不是竞争。 Spring 将 AspectsJ 无缝集成了 Spring AOP 和 IoC,以在基于 Spring 的一致应用程序架构中支持 AOP 的所有使用。这种集成不会影响 Spring AOP API 或 AOP Alliance API。
AOP 代理
Spring AOP 默认将标准 JDK 动态代理用于 AOP 代理。这使得可以代理任何接口(或一组接口)。
Spring AOP 也可以使用 CGLIB 代理。这对于代理类而不是接口是必需的。默认情况下,如果业务对象未实现接口,则使用 CGLIB。由于对接口而不是对类进行编程是一种好习惯,因此业务类通常实现一个或多个业务接口。在某些情况下(可能极少发生),您需要通知未在接口上声明的方法,或者需要将代理对象作为具体类型传递给方法,则可以使用强制使用 CGLIB。
@AspectJ 支持
@AspectJ 是一种将切面声明为带有注解的常规 Java 类的样式。 Spring 使用 AspectJ 提供的用于切入点解析和匹配的库来解释与 AspectJ 5 相同的 注解。但是,AOP 运行时仍然是纯 Spring AOP,并且不依赖于 AspectJ 编译器或编织器。
启用@AspectJ 支持
要在 Spring 配置中使用@AspectJ 切面,需要启用 Spring 支持以基于@AspectJ 切面配置 Spring AOP,并根据这些切面是否建议对它们进行自动代理。通过自动代理,我们的意思是,如果 Spring 确定一个或多个切面建议一个 bean,它会自动为该 bean 生成一个代理来拦截方法调用并确保按需执行建议。
可以使用 XML 或 Java 样式的配置来启用@AspectJ 支持。无论哪种情况,都需要确保 AspectJ 的aspectjweaver.jar库位于应用程序的 Classpath(版本 1.8 或更高版本)上。该库在 AspectJ 发行版的lib目录中或从 Maven Central 存储库中可用。
通过 Java 配置启用@AspectJ 支持
要使用 Java @Configuration启用@AspectJ 支持,请添加@EnableAspectJAutoProxy注解,如以下示例所示:
1 |
|
通过 XML 配置启用@AspectJ 支持
要通过基于 XML 的配置启用@AspectJ 支持,请使用aop:aspectj-autoproxy元素,如以下示例所示:
1 | <aop:aspectj-autoproxy/> |
声明一个切面
启用@AspectJ 支持后,Spring 会自动检测到在应用程序上下文中使用@AspectJ 切面(具有@Aspect注解)的类定义的任何 bean,并用于配置 Spring AOP。
第一个示例显示了应用程序上下文中的常规 bean 定义,该定义指向具有@Aspect注解的 bean 类:
1 | <bean id="myAspect" class="org.xyz.NotVeryUsefulAspect"> |
第二个示例显示了NotVeryUsefulAspect类定义,该类定义带有org.aspectj.lang.annotation.Aspect注解;
1 | package org.xyz; |
切面(带有@Aspect注解 的类)可以具有方法和字段,与任何其他类相同。它们还可以包含切入点,通知和引入(类型间)声明。
通过扫描自动探测切面
可以将切面类注册为 Spring XML 配置中的常规 bean,也可以通过 Classpath 扫描来自动检测它们-与其他任何 Spring管理 的 bean 一样。但是,请注意,
@Aspect注解不足以在 Classpath 中进行自动检测。为此,您需要添加一个单独的@Component注解(或者,按照 Spring 的组件扫描程序的规则,有条件的自定义构造型注解)。
使用切面对另一个切面进行通知?
在 Spring AOP 中,切面本身不能成为其他切面的通知目标。类上的
@Aspect注解 将其标记为一个切面,因此将其从自动代理中排除。
声明切入点
pointcut确定了感兴趣的join point,从而使我们能够控制执行通知的时间。 Spring AOP 仅支持 Spring Bean 的方法执行join point,因此可以将pointcut视为与 Spring Bean 上的方法执行相匹配。pointcut声明由两部分组成:一个包含名称和任何参数的签名,以及一个切入点表达式,该切入点表达式确定方法执行。在 AOP 的@AspectJ 注解样式中,常规方法定义提供了切入点签名。并通过使用@Pointcut注解 指示切入点表达式(用作切入点签名的方法必须具有void返回类型)。
下面的示例定义一个名为anyOldTransfer的切入点,该切入点与任何名为transfer的方法的执行相匹配:
1 | // the pointcut expressionprivate |
形成@Pointcut注解的值的切入点表达式是一个常规的 AspectJ 5 切入点表达式。
支持的切入点指示符
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 中正在执行的方法)具有给定注解的连接点。
组合切入点表达式
您可以组合切入点表达式,可以使用&&, ||和!进行组合。您也可以按名称引用切入点表达式。以下示例显示了三个切入点表达式:
1 |
|
- (1)
anyPublicOperation匹配方法执行联接点是否表示任何公共方法的执行。 - (2)
inTrading如果 Transaction 模块中有方法执行则匹配。 - (3)
tradingOperation匹配,如果方法执行代表 Transaction 模块中的任何公共方法。
最佳实践是从较小的命名组件中构建更复杂的切入点表达式,如先前所示。按名称引用切入点时,将应用常规的 Java 可见性规则(您可以看到相同类型的私有切入点,层次结构中受保护的切入点,任何位置的公共切入点,等等)。可见性不影响切入点匹配。
共享通用切入点定义
在使用企业应用程序时,开发人员通常希望从多个切面引用应用程序的模块和特定的操作集。我们建议为此定义一个“ SystemArchitecture”切面,以捕获常见的切入点表达式。这样的切面通常类似于以下示例:
1 | package com.xyz.someapp; |
您可以在需要切入点表达式的任何地方引用在这样的切面中定义的切入点。例如,要使服务层具有事务性,您可以编写以下内容:
1 | <aop:config> |
Examples
Spring AOP 用户可能最常使用execution切入点指示符。执行表达式的格式如下:
1 | execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) |
除了返回类型模式(前面的代码段中为ret-type-pattern),name-pattern和param-pattern以外的所有其他部分都是可选的。返回类型模式确定该方法的返回类型必须是什么才能使连接点匹配, *最常用作返回类型模式,匹配任何返回类型。仅当方法返回给定类型时,完全合格的类型名称才匹配。name-pattern与方法名称匹配。您可以将*通配符用作名称模式的全部或一部分。如果指定了声明类型模式,请在其末尾添加.并将其连接到名称模式组件。param-pattern稍微复杂一些:()匹配不带参数的方法,而(..)匹配任意数量(零个或多个)的参数。 (*)模式与采用任何类型的一个参数的方法匹配。 (*,String)与采用两个参数的方法匹配。第一个可以是任何类型,而第二个必须是String。
以下示例显示了一些常用的切入点表达式:
- 任何公共方法的执行:
1 | execution(public * *(..)) |
- 名称以
set开头的任何方法的执行:
1 | execution(* set*(..)) |
AccountService接口定义的任何方法的执行:
1 | execution(* com.xyz.service.AccountService.*(..)) |
service软件包中定义的任何方法的执行:
1 | execution(* com.xyz.service.*.*(..)) |
- 服务包或其子包之一中定义的任何方法的执行:
1 | execution(* com.xyz.service..*.*(..)) |
- 服务包中的任何连接点(仅在 Spring AOP 中执行方法):
1 | within(com.xyz.service.*) |
- 服务包或其子包之一中的任何连接点(仅在 Spring AOP 中执行方法):
1 | within(com.xyz.service..*) |
- 代理实现
AccountService接口的任何连接点(仅在 Spring AOP 中执行方法):
1 | this(com.xyz.service.AccountService) |
- 目标对象实现
AccountService接口的任何连接点(仅在 Spring AOP 中执行方法):
1 | target(com.xyz.service.AccountService) |
- 任何采用单个参数且运行时传递的参数为
Serializable的连接点(仅在 Spring AOP 中是方法执行):
1 | args(java.io.Serializable) |
请注意,此示例中给出的切入点不同于execution(* *(java.io.Serializable))。如果在运行时传递的参数为Serializable,则 args 版本匹配,如果方法签名声明单个类型为Serializable的参数,则执行版本匹配。
- 目标对象带有
@Transactional注解的任何连接点(仅在 Spring AOP 中执行方法):
1 |
- 目标对象的声明类型具有
@Transactional注解 的任何连接点(仅在 Spring AOP 中是方法执行):
1 |
- 执行方法带有
@Transactional注解的任何连接点(仅在 Spring AOP 中是方法执行):
1 |
- 任何采用单个参数且传递的参数的运行时类型具有
@Classified注解 的连接点(仅在 Spring AOP 中是方法执行)。
1 |
- 名为
tradeService的 Spring bean 上的任何连接点(仅在 Spring AOP 中执行方法):
1 | bean(tradeService) |
- Spring Bean 上具有与通配符表达式
*Service匹配的名称的任何连接点(仅在 Spring AOP 中是方法执行):
1 | bean(*Service) |
编写好的切入点
在编译期间,AspectJ 处理切入点以优化匹配性能。检查代码并确定每个连接点是否(静态或动态)匹配给定的切入点是一个昂贵的过程。 (动态匹配意味着无法从静态分析中完全确定匹配,并且在代码中进行测试以确定在运行代码时是否存在实际匹配)。首次遇到切入点声明时,AspectJ 将其重写为匹配过程的最佳形式。这是什么意思?基本上,切入点以 DNF(析取范式)重写,并且对切入点的组件进行排序,以便首先检查那些较便宜的组件。这意味着您不必担心理解各种切入点指示符的性能,并且可以在切入点声明中以任何 Sequences 提供它们。
但是,AspectJ 只能使用所告诉的内容。为了获得最佳的匹配性能,您应该考虑他们试图达到的目标,并在定义中尽可能缩小匹配的搜索空间。现有的指示符自然分为三类之一:同类,作用域和上下文:
- 亲切的指示者选择一种特定的连接点:
execution,get,set,call和handler。 - 作用域指定者选择一组感兴趣的连接点(可能是多种):
within和withincode - 上下文指示符根据以下上下文进行匹配(并可选地绑定):
this,target和@annotation
编写正确的切入点至少应包括前两种类型(种类和作用域)。您可以包括上下文指示符以根据连接点上下文进行匹配,也可以绑定该上下文以在建议中使用。仅提供同类的标识符或仅提供上下文的标识符是可行的,但是由于额外的处理和分析,可能会影响编织性能(使用的时间和内存)。范围指定者的匹配非常快,使用它们的使用意味着 AspectJ 可以非常迅速地消除不应进一步处理的连接点组。一个好的切入点应该始终包括一个切入点。
声明通知
Advice 与切入点表达式相关联,并在与切入点匹配的方法执行之前、之后或周围运行。切入点表达式可以是对命名切入点的简单引用,也可以是就地声明的切入点表达式。
Before Advice
可以使用@Before注解 在切面中在建议之前声明:
1 | import org.aspectj.lang.annotation.Aspect; |
如果使用就地切入点表达式,则可以将前面的示例重写为以下示例:
1 | import org.aspectj.lang.annotation.Aspect; |
After Returning Advice
当匹配的方法执行正常返回时,运行After Returning Advice。可以使用@AfterReturning注解进行声明:
1 | import org.aspectj.lang.annotation.Aspect; |
有时需要在通知正文中访问返回的实际值。可以使用@AfterReturning的形式绑定返回值以获取该访问权限,如以下示例所示:
1 | import org.aspectj.lang.annotation.Aspect; |
returning属性中使用的名称必须与 advice 方法中的参数名称相对应。当方法执行返回时,该返回值将作为相应的参数值传递到通知方法。 returning子句还将匹配仅限制为返回指定类型值(在这种情况下为Object,该值与任何返回值匹配)的那些方法执行。
After Throwing Advice
当匹配的方法执行通过抛出异常退出时,After Throwing Advice运行。可以使用@AfterThrowing注解进行声明,如以下示例所示:
1 | import org.aspectj.lang.annotation.Aspect; |
希望通知仅在引发给定类型的异常时才运行,并且还需要访问通知正文中的异常,可以使用throwing属性来限制匹配(如果需要)(否则,请使用Throwable作为异常类型),并将抛出的异常绑定到 advice 参数。以下示例显示了如何执行此操作:
1 | import org.aspectj.lang.annotation.Aspect; |
throwing属性中使用的名称必须与 advice 方法中的参数名称相对应。当通过抛出异常退出方法执行时,该异常将作为相应的参数值传递给通知方法。 throwing子句还将匹配仅限制为抛出指定类型(在本例中为DataAccessException)的异常的方法执行。
After (Finally) Advice
当匹配的方法执行退出时,After (Finally) Advice运行。通过使用@After注解进行声明。之后必须准备处理正常和异常返回条件的建议。它通常用于释放资源和类似目的。以下示例显示了最终建议后的用法:
1 | import org.aspectj.lang.annotation.Aspect; |
Around Advice
Around Advice“环绕”匹配方法的执行。它有机会在方法运行之前和之后进行工作,并确定方法何时、如何、甚至是否真正开始运行。如果您需要以线程安全的方式(例如,启动和停止计时器)在方法执行前后共享状态,则通常使用环绕通知。始终使用满足您要求的最不强大的通知形式(即,如果 before 通知可以,请不要使用 around 通知)。。
周围的建议通过使用@Around注解 来声明。咨询方法的第一个参数必须为ProceedingJoinPoint类型。在建议的正文中,在ProceedingJoinPoint上调用proceed()会使底层方法执行。 proceed方法也可以传入Object[]。数组中的值用作方法执行时的参数。
以下示例显示了如何使用周围建议:
1 | import org.aspectj.lang.annotation.Aspect; |
Around Advice返回的值是该方法的调用者看到的返回值。例如,如果一个简单的缓存切面有一个值,则可以从缓存中返回一个值;如果没有,则调用proceed()。请注意,proceed可能在周围建议的正文中被调用一次,多次或完全不被调用。所有这些都是合法的。
Advice Parameters
Spring 提供了完全类型化的建议,这意味着您可以在建议签名中声明所需的参数(如我们先前在返回和抛出示例中所见),而不是一直使用Object[]数组。我们将在本节的后面部分介绍如何使参数和其他上下文值可用于建议主体。首先,我们看一下如何编写通用建议,以了解该建议当前建议的方法。
访问当前的 JoinPoint
任何通知方法都可以将类型org.aspectj.lang.JoinPoint的参数声明为第一个参数(请注意,在周围的通知中必须声明类型JoinPoint的子类ProceedingJoinPoint的第一个参数。JoinPoint接口提供了许多有用的方法:
getArgs():返回方法参数。getThis():返回代理对象。getTarget():返回目标对象。getSignature():返回建议使用的方法的描述。toString():打印有关所建议方法的有用描述。
将参数传递给Advice
我们已经看到了如何绑定返回的值或异常值(在返回之后和引发建议之后使用)。要使参数值可用于建议正文,可以使用args的绑定形式。如果在 args 表达式中使用参数名称代替类型名称,则在调用建议时会将相应参数的值作为参数值传递。一个例子应该使这一点更清楚。假设您要建议执行以Account对象作为第一个参数的 DAO 操作,并且您需要访问建议正文中的帐户。您可以编写以下内容:
1 |
|
切入点表达式的args(account,..)部分有两个作用。首先,它将匹配限制为仅方法采用至少一个参数并且传递给该参数的参数是Account的实例的方法执行。其次,它通过account参数使实际的Account对象可用于建议。
编写此文件的另一种方法是声明一个切入点,当切入点Account对象值与连接点匹配时,该切入点“提供”,然后从建议中引用命名切入点。如下所示:
1 |
|
代理对象(this),目标对象(target)和 注解(@within,@target,@annotation和@args)都可以以类似的方式绑定。接下来的两个示例显示如何匹配使用@Auditable注解注解 的方法的执行并提取审核代码:
这两个示例中的第一个显示了@Auditable注解的定义:
1 |
|
两个示例中的第二个示例显示与@Auditable方法的执行相匹配的建议:
1 |
|
建议参数和泛型
Spring AOP 可以处理类声明和方法参数中使用的泛型。假设您具有如下通用类型:
1 | public interface Sample<T> { void sampleGenericMethod(T param); void sampleGenericCollectionMethod(Collection<T> param);} |
您可以通过在要拦截方法的参数类型中键入 advice 参数,将方法类型的拦截限制为某些参数类型:
1 | public void beforeSampleMethod(MyType param) { // Advice implementation} |
这种方法不适用于通用集合。因此,您不能按以下方式定义切入点:
1 | public void beforeSampleMethod(Collection<MyType> param) { // Advice implementation} |
为了使这项工作有效,我们将不得不检查集合中的每个元素,这是不合理的,因为我们也无法决定通常如何处理null值。要实现类似目的,您必须将参数键入Collection<?>并手动检查元素的类型。
确定参数名称
通知调用中的参数绑定依赖于切入点表达式中使用的名称与通知和切入点方法签名中声明的参数名称的匹配。通过 Java 反射无法获得参数名称,因此 Spring AOP 使用以下策略来确定参数名称:
- 如果用户已明确指定参数名称,则使用指定的参数名称。建议和切入点注解都具有可选的
argNames属性,您可以使用该属性来指定带注解方法的参数名称。这些参数名称在运行时可用。下面的示例演示如何使用argNames属性:
1 | public void audit(Object bean, Auditable auditable) { AuditCode code = auditable.value(); // ... use code and bean} |
如果第一个参数是JoinPoint,ProceedingJoinPoint或JoinPoint.StaticPart类型,则可以从argNames属性的值中省略参数的名称。例如,如果您修改前面的建议以接收连接点对象,则argNames属性不需要包括它:
1 | public void audit(JoinPoint jp, Object bean, Auditable auditable) { AuditCode code = auditable.value(); // ... use code, bean, and jp} |
对于不收集任何其他连接点上下文的建议实例,对JoinPoint,ProceedingJoinPoint和JoinPoint.StaticPart类型的第一个参数进行特殊处理特别方便。在这种情况下,您可以省略argNames属性。例如,以下建议无需声明argNames属性:
1 | public void audit(JoinPoint jp) { // ... use jp} |
- 使用
'argNames'属性有点笨拙,因此,如果未指定'argNames'属性,Spring AOP 将查看该类的调试信息,并尝试从局部变量表中确定参数名称。只要已使用调试信息(至少'-g:vars')编译了类,就存在此信息。启用此标志时进行编译的结果是:(1)您的代码稍微易于理解(逆向工程),(2)类文件的大小略大(通常无关紧要),(3)删除未使用的本地代码的优化变量不适用于您的编译器。换句话说,通过启用该标志,您应该不会遇到任何困难。
Note
如果即使没有调试信息,AspectJ 编译器(ajc)都已编译@AspectJ 切面,则无需添加argNames属性,因为编译器会保留所需的信息。
- 如果在没有必要调试信息的情况下编译了代码,Spring AOP 会尝试推断绑定变量与参数的配对(例如,如果切入点表达式中仅绑定了一个变量,并且 advice 方法仅接受一个参数,则配对很明显)。如果在给定可用信息的情况下变量的绑定不明确,则会抛出
AmbiguousBindingException。 - 如果以上所有策略均失败,则抛出
IllegalArgumentException。
处理参数
前面我们提到过,我们将描述如何使用在 Spring AOP 和 AspectJ 上始终有效的参数编写proceed调用。解决方案是确保建议签名按 Sequences 绑定每个方法参数。以下示例显示了如何执行此操作:
1 | public Object preProcessQueryPattern(ProceedingJoinPoint pjp, String accountHolderNamePattern) throws Throwable { String newPattern = preProcess(accountHolderNamePattern); return pjp.proceed(new Object[] {newPattern});} |
在许多情况下,无论如何都要进行此绑定(如上例所示)。
Advice Ordering
当多条建议都希望在同一连接点上运行时会发生什么? Spring AOP 遵循与 AspectJ 相同的优先级规则来确定建议执行的 Sequences。优先级最高的建议首先“在途中”运行(因此,给定两条优先建议,则优先级最高的建议首先运行)。从连接点“出路”中,优先级最高的建议将最后运行(因此,给定两条后置通知,优先级最高的建议将第二次运行)。
当在不同切面定义的两条建议都需要在同一连接点上运行时,除非另行指定,否则执行 Sequences 是不确定的。您可以通过指定优先级来控制执行 Sequences。通过在 Aspect 类中实现org.springframework.core.Ordered接口或使用Order注解对其进行 注解,可以通过普通的 Spring 方法来完成。给定两个切面,从Ordered.getValue()返回较低值(或注解值)的切面具有较高的优先级。
当在相同切面定义的两条建议都需要在同一连接点上运行时,其 Sequences 是未定义的(因为无法通过反射来获取 javac 编译类的声明 Sequences)。考虑将这些建议方法折叠成每个切面类中每个连接点的一个建议方法,或将建议重构为单独的切面类,您可以在切面级别进行 Order。
Introductions
简介(在 AspectJ 中称为类型间声明)使切面可以声明建议对象实现给定的接口,并代表那些对象提供该接口的实现。
您可以使用@DeclareParents注解 进行介绍。此注解用于声明匹配类型具有新的父代(因此而得名)。例如,在给定名为UsageTracked的接口和该接口名为DefaultUsageTracked的实现的情况下,以下切面声明服务接口的所有实现者也都实现UsageTracked接口(例如,通过 JMX 公开统计信息):
1 | class UsageTracking { public static UsageTracked mixin; public void recordUsage(UsageTracked usageTracked) { usageTracked.incrementUseCount(); }} |
要实现的接口由带注解的字段的类型确定。 @DeclareParents注解的value属性是 AspectJ 类型的模式。任何匹配类型的 bean 都实现UsageTracked接口。请注意,在前面示例的建议中,服务 Bean 可以直接用作UsageTracked接口的实现。如果以编程方式访问 bean,则应编写以下内容:
1 | UsageTracked usageTracked = (UsageTracked) context.getBean("myService"); |
切面实例化模型
Note
这是一个高级主题。如果您刚开始使用 AOP,则可以放心地跳过它,直到以后。
默认情况下,应用程序上下文中每个切面都有一个实例。 AspectJ 将此称为单例实例化模型。可以使用备用生命周期来定义切面。 Spring 支持 AspectJ 的perthis和pertarget实例化模型(当前不支持percflow, percflowbelow,和pertypewithin)。
您可以通过在@Aspect注解中指定perthis子句来声明perthis切面。考虑以下示例:
1 | public class MyAspect { private int someState; public void recordServiceUsage() { // ... }} |
在前面的示例中,'perthis'子句的作用是为每个执行业务服务的唯一服务对象(每个与切入点表达式匹配的联接点绑定到“ this”的唯一对象)创建一个切面实例。切面实例是在服务对象上首次调用方法时创建的。当服务对象超出范围时,切面将超出范围。在创建切面实例之前,其中的任何建议都不会执行。创建切面实例后,在其中声明的建议将在匹配的连接点处执行,但仅当服务对象是与此切面相关联的对象时才执行。有关per子句的更多信息,请参见 AspectJ 编程指南。
pertarget实例化模型的工作方式与perthis完全相同,但是它为匹配的连接点处的每个唯一目标对象创建一个切面实例。
AOP 示例
既然您已经了解了所有组成部分是如何工作的,那么我们可以将它们放在一起做一些有用的事情。
有时由于并发问题(例如,死锁失败者),业务服务的执行可能会失败。如果重试该操作,则很可能在下一次尝试中成功。对于适合在这种情况下重试的业务服务(不需要为解决冲突而需要返回给用户的幂等操作),我们希望透明地重试该操作以避免 Client 端看到PessimisticLockingFailureException。这项要求清楚地跨越了服务层中的多个服务,因此非常适合通过一个切面实施。
因为我们想重试该操作,所以我们需要使用“周围”建议,以便我们可以多次调用proceed。以下清单显示了基本切面的实现:
1 | 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周围建议中。请注意,目前,我们将重试逻辑应用于每个businessService()。我们尝试 continue,如果失败并失败了PessimisticLockingFailureException,我们将重试,除非我们用尽了所有的重试尝试。
相应的 Spring 配置如下:
1 | <aop:aspectj-autoproxy/><bean id="concurrentOperationExecutor" class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor"> <property name="maxRetries" value="3"/> <property name="order" value="100"/></bean> |
为了优化切面,使其仅重试幂等操作,我们可以定义以下Idempotent注解:
1 | @Retention(RetentionPolicy.RUNTIME)public @interface Idempotent { // marker annotation} |
然后,我们可以使用注解来注解服务操作的实现。切面的更改仅重试幂等操作涉及精简切入点表达式,以便只有@Idempotent个操作匹配,如下所示:
1 | public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable { ...} |
基于架构的 AOP 支持
如果您更喜欢基于 XML 的格式,Spring 还支持使用新的aop名称空间标签定义切面。支持与使用@AspectJ 样式时完全相同的切入点表达式和建议类型。因此,在本节中,我们将重点放在新语法上,并使 Reader 参考上一节(@AspectJ support)中的讨论,以了解编写切入点表达式和建议参数的绑定。
要使用本节中描述的 aop 名称空间标签,您需要导入spring-aop模式,如基于 XML 模式的配置中所述。有关如何在aop名称空间中导入标签的信息,请参见AOP 模式。
在您的 Spring 配置中,所有切面和顾问元素都必须放在<aop:config>元素内(在应用程序上下文配置中可以有多个<aop:config>元素)。 <aop:config>元素可以包含切入点,顾问和切面元素(请注意,这些元素必须按此 Sequences 声明)。
Warning
<aop:config>样式的配置大量使用了 Spring 的auto-proxying机制。如果您已经通过使用BeanNameAutoProxyCreator或类似方法来使用显式自动代理,则可能会导致问题(例如未编制建议)。推荐的用法模式是仅使用<aop:config>样式或仅AutoProxyCreator样式,并且不要混合使用。
声明一个切面
使用模式支持时,切面是在 Spring 应用程序上下文中定义为 Bean 的常规 Java 对象。状态和行为在对象的字段和方法中捕获,切入点和建议信息在 XML 中捕获。
您可以使用\ <>元素声明一个切面,并使用ref属性引用该支持 bean,如以下示例所示:
1 | <aop:config> <aop:aspect id="myAspect" ref="aBean"> ... </aop:aspect></aop:config><bean id="aBean" class="..."> ...</bean> |
支持切面(在这种情况下为aBean)的 bean 当然可以像配置任何其他 Spring bean 一样进行配置并注入依赖项。
声明切入点
您可以在<aop:config>元素中声明命名的切入点,从而使切入点定义在多个切面和顾问程序之间共享。
可以定义代表服务层中任何业务服务的执行的切入点:
1 | <aop:config> <aop:pointcut id="businessService" expression="execution(* com.xyz.myapp.service.*.*(..))"/></aop:config> |
注意,切入点表达式本身使用的是与@AspectJ support中所述的 AspectJ 切入点表达式语言。如果使用基于架构的声明样式,则可以引用在切入点表达式中的类型(@Aspects)中定义的命名切入点。定义上述切入点的另一种方法如下:
1 | <aop:config> <aop:pointcut id="businessService" expression="com.xyz.myapp.SystemArchitecture.businessService()"/></aop:config> |
假定您具有共享通用切入点定义中所述的SystemArchitecture外观。
然后,在切面中声明切入点与声明顶级切入点非常相似,如以下示例所示:
1 | <aop:config> <aop:aspect id="myAspect" ref="aBean"> <aop:pointcut id="businessService" expression="execution(* com.xyz.myapp.service.*.*(..))"/> ... </aop:aspect></aop:config> |
与@AspectJ 切面几乎相同,使用基于架构的定义样式声明的切入点可以收集连接点上下文。例如,以下切入点收集this对象作为连接点上下文,并将其传递给建议:
1 | <aop:config> <aop:aspect id="myAspect" ref="aBean"> <aop:pointcut id="businessService" expression="execution(* com.xyz.myapp.service.*.*(..)) && this(service)"/> <aop:before pointcut-ref="businessService" method="monitor"/> ... </aop:aspect></aop:config> |
必须声明通知,以通过包含匹配名称的参数来接收收集的连接点上下文,如下所示:
1 | public void monitor(Object service) { ...} |
组合切入点子表达式时,XML 文档中的&&很尴尬,因此可以分别使用and,or和not关键字代替&&,||和!。例如,上一个切入点可以更好地编写如下:
1 | <aop:config> <aop:aspect id="myAspect" ref="aBean"> <aop:pointcut id="businessService" expression="execution(* com.xyz.myapp.service..(..)) and this(service)"/> <aop:before pointcut-ref="businessService" method="monitor"/> ... </aop:aspect></aop:config> |
请注意,以这种方式定义的切入点由其 XML id引用,并且不能用作命名切入点以形成复合切入点。因此,基于架构的定义样式中的命名切入点支持比@AspectJ 样式所提供的更受限制。
声明Advice
基于模式的 AOP 支持使用与@AspectJ 样式相同的五种建议,并且它们具有完全相同的语义。
Before Advice
在运行匹配的方法之前,建议运行之前。使用\ <>元素在<aop:aspect>内部声明它,如以下示例所示:
1 | <aop:aspect id="beforeExample" ref="aBean"> <aop:before pointcut-ref="dataAccessOperation" method="doAccessCheck"/> ...</aop:aspect> |
在这里,dataAccessOperation是在最高(<aop:config>)级别定义的切入点的id。要定义切入点内联,请用pointcut属性替换pointcut-ref属性,如下所示:
1 | <aop:aspect id="beforeExample" ref="aBean"> <aop:before pointcut="execution(* com.xyz.myapp.dao.*.*(..))" method="doAccessCheck"/> ...</aop:aspect> |
正如我们在@AspectJ 样式的讨论中所指出的那样,使用命名的切入点可以显着提高代码的可读性。
method属性标识提供建议正文的方法(doAccessCheck)。必须为包含建议的 Aspect 元素所引用的 bean 定义此方法。在执行数据访问操作(与切入点表达式匹配的方法执行连接点)之前,将调用 Aspect Bean 上的doAccessCheck方法。
返回建议后
返回的建议在匹配的方法执行正常完成时运行。在<aop:aspect>内部以与建议之前相同的方式声明它。以下示例显示了如何声明它:
1 | <aop:aspect id="afterReturningExample" ref="aBean"> <aop:after-returning pointcut-ref="dataAccessOperation" method="doAccessCheck"/> ...</aop:aspect> |
与@AspectJ 样式一样,您可以在建议正文中获取返回值。为此,使用 returning 属性指定返回值应传递到的参数的名称,如以下示例所示:
1 | <aop:aspect id="afterReturningExample" ref="aBean"> <aop:after-returning pointcut-ref="dataAccessOperation" returning="retVal" method="doAccessCheck"/> ...</aop:aspect> |
doAccessCheck方法必须声明一个名为retVal的参数。该参数的类型以与@AfterReturning相同的方式约束匹配。例如,您可以声明方法签名,如下所示:
1 | public void doAccessCheck(Object retVal) {... |
提出建议后
抛出建议后,当匹配的方法执行通过抛出异常退出时执行建议。通过使用掷后元素在<aop:aspect>内部声明它,如以下示例所示:
1 | <aop:aspect id="afterThrowingExample" ref="aBean"> <aop:after-throwing pointcut-ref="dataAccessOperation" method="doRecoveryActions"/> ...</aop:aspect> |
与@AspectJ 样式一样,您可以在通知正文中获取引发的异常。为此,请使用 throwing 属性指定异常应传递到的参数的名称,如以下示例所示:
1 | <aop:aspect id="afterThrowingExample" ref="aBean"> <aop:after-throwing pointcut-ref="dataAccessOperation" throwing="dataAccessEx" method="doRecoveryActions"/> ...</aop:aspect> |
doRecoveryActions方法必须声明一个名为dataAccessEx的参数。该参数的类型以与@AfterThrowing相同的方式约束匹配。例如,方法签名可以声明如下:
1 | public void doRecoveryActions(DataAccessException dataAccessEx) {... |
(最后)建议后
无论最终如何执行匹配的方法,建议(最终)都会运行。您可以使用after元素对其进行声明,如以下示例所示:
1 | <aop:aspect id="afterFinallyExample" ref="aBean"> <aop:after pointcut-ref="dataAccessOperation" method="doReleaseLock"/> ...</aop:aspect> |
Around Advice
最后一种建议是围绕建议。围绕建议在匹配的方法执行过程中“围绕”运行。它有机会在方法执行之前和之后进行工作,并确定何时,如何以及什至根本不执行该方法。周围建议通常用于以线程安全的方式(例如,启动和停止计时器)在方法执行之前和之后共享状态。始终使用最不强大的建议形式,以满足您的要求。如果建议可以完成这项工作,请不要在建议周围使用。
您可以使用aop:around元素在建议周围进行声明。咨询方法的第一个参数必须为ProceedingJoinPoint类型。在建议的正文中,在ProceedingJoinPoint上调用proceed()会导致基础方法执行。 proceed方法也可以用Object[]调用。数组中的值用作方法执行时的参数。有关使用Object[]调用proceed的说明,请参见Around Advice。以下示例显示了如何在 XML 中围绕建议进行声明:
1 | <aop:aspect id="aroundExample" ref="aBean"> <aop:around pointcut-ref="businessService" method="doBasicProfiling"/> ...</aop:aspect> |
doBasicProfiling通知的实现可以与@AspectJ 示例完全相同(当然要减去 注解),如以下示例所示:
1 | public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable { // start stopwatch Object retVal = pjp.proceed(); // stop stopwatch return retVal;} |
Advice Parameters
基于架构的声明样式以与@AspectJ 支持相同的方式支持完全类型的建议,即通过名称与建议方法参数匹配切入点参数。有关详情,请参见Advice Parameters。如果您希望显式指定建议方法的参数名称(不依赖于先前描述的检测策略),则可以通过使用建议元素的arg-names属性来实现,该属性与argNames属性的处理方式相同。建议 注解(如确定参数名称中所述)。以下示例显示如何在 XML 中指定参数名称:
1 | <aop:before pointcut="com.xyz.lib.Pointcuts.anyPublicMethod() and @annotation(auditable)" method="audit" arg-names="auditable"/> |
arg-names属性接受逗号分隔的参数名称列表。
以下基于 XSD 的方法中涉及程度稍高的示例显示了一些与一些强类型参数结合使用的建议:
1 | package x.y.service;public interface PersonService { Person getPerson(String personName, int age);}public class DefaultFooService implements FooService { public Person getPerson(String name, int age) { return new Person(name, age); }} |
接下来是切面。请注意,profile(..)方法接受许多强类型的参数,第一个恰好是用于进行方法调用的连接点。此参数的存在表明profile(..)用作around建议,如以下示例所示:
1 | package x.y;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 配置影响了特定连接点的上述建议的执行:
1 | <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 http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://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="x.y.service.DefaultPersonService"/> <!-- this is the actual advice itself --> <bean id="profiler" class="x.y.SimpleProfiler"/> <aop:config> <aop:aspect ref="profiler"> <aop:pointcut id="theExecutionOfSomePersonServiceMethod" expression="execution(* x.y.service.PersonService.getPerson(String,int)) and args(name, age)"/> <aop:around pointcut-ref="theExecutionOfSomePersonServiceMethod" method="profile"/> </aop:aspect> </aop:config></beans> |
考虑以下驱动程序脚本:
1 | import org.springframework.beans.factory.BeanFactory;import org.springframework.context.support.ClassPathXmlApplicationContext;import x.y.service.PersonService;public final class Boot { public static void main(final String[] args) throws Exception { BeanFactory ctx = new ClassPathXmlApplicationContext("x/y/plain.xml"); PersonService person = (PersonService) ctx.getBean("personService"); person.getPerson("Pengo", 12); }} |
有了这样的 Boot 类,我们将在标准输出上获得类似于以下内容的输出:
1 | StopWatch 'Profiling for 'Pengo' and '12'': running time (millis) = 0-----------------------------------------ms % Task name-----------------------------------------00000 ? execution(getFoo) |
Advice Ordering
当需要在同一连接点(执行方法)上执行多个建议时,排序规则如Advice Ordering中所述。切面之间的优先级是通过将Order注解添加到支持切面的 Bean 或通过使 Bean 实现Ordered接口来确定的。
Introductions
简介(在 AspectJ 中称为类型间声明)使切面可以声明建议的对象实现给定的接口,并代表那些对象提供该接口的实现。
您可以使用aop:aspect内的aop:declare-parents元素进行介绍。您可以使用aop:declare-parents元素来声明匹配类型具有新的父代(因此而得名)。例如,给定名为UsageTracked的接口和该名为DefaultUsageTracked的接口的实现,以下切面声明服务接口的所有实现者也都实现UsageTracked接口。 (例如,为了通过 JMX 公开统计信息.)
1 | <aop:aspect id="usageTrackerAspect" ref="usageTracking"> <aop:declare-parents types-matching="com.xzy.myapp.service.*+" implement-interface="com.xyz.myapp.service.tracking.UsageTracked" default-impl="com.xyz.myapp.service.tracking.DefaultUsageTracked"/> <aop:before pointcut="com.xyz.myapp.SystemArchitecture.businessService() and this(usageTracked)" method="recordUsage"/></aop:aspect> |
支持usageTracking bean 的类将包含以下方法:
1 | public void recordUsage(UsageTracked usageTracked) { usageTracked.incrementUseCount();} |
要实现的接口由implement-interface属性确定。 types-matching属性的值是 AspectJ 类型的模式。任何匹配类型的 bean 都实现UsageTracked接口。请注意,在前面示例的建议中,服务 Bean 可以直接用作UsageTracked接口的实现。要以编程方式访问 bean,可以编写以下代码:
1 | UsageTracked usageTracked = (UsageTracked) context.getBean("myService"); |
5.5.5. 切面实例化模型
模式定义切面唯一受支持的实例化模型是单例模型。在将来的版本中可能会支持其他实例化模型。
5.5.6. Advisors
“顾问”的概念来自 Spring 中定义的 AOP 支持,并且在 AspectJ 中没有直接等效的概念。顾问就像一个独立的小切面,只有一条建议。通知本身由 bean 表示,并且必须实现Spring 的建议类型中描述的建议接口之一。顾问可以利用 AspectJ 切入点表达式。
Spring 通过<aop:advisor>元素支持顾问程序概念。您通常会看到它与事务建议结合使用,事务建议在 Spring 中也有自己的名称空间支持。以下示例显示顾问程序:
1 | <aop:config> <aop:pointcut id="businessService" expression="execution(* com.xyz.myapp.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,请使用order属性来定义顾问程序的Ordered值。
5.5.7. AOP 模式示例
本节显示了使用模式支持重写时来自AOP 示例的并发锁定失败重试示例的外观。
有时由于并发问题(例如,死锁失败者),业务服务的执行可能会失败。如果重试该操作,则很可能在下一次尝试中成功。对于适合在这种情况下重试的业务服务(不需要为解决冲突而需要返回给用户的幂等操作),我们希望透明地重试该操作以避免 Client 端看到PessimisticLockingFailureException。这项要求清楚地跨越了服务层中的多个服务,因此非常适合通过一个切面实施。
因为我们想重试该操作,所以我们需要使用“周围”建议,以便我们可以多次调用proceed。以下清单显示了基本切面的实现(这是使用模式支持的常规 Java 类):
1 | 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周围建议方法中。我们尝试 continue。如果我们失败了PessimisticLockingFailureException,我们将重试,除非我们用尽了所有的重试尝试。
Note
该类与@AspectJ 示例中使用的类相同,但是除去了 注解。
相应的 Spring 配置如下:
1 | <aop:config> <aop:aspect id="concurrentOperationRetry" ref="concurrentOperationExecutor"> <aop:pointcut id="idempotentOperation" expression="execution(* com.xyz.myapp.service.*.*(..))"/> <aop:around pointcut-ref="idempotentOperation" method="doConcurrentOperation"/> </aop:aspect></aop:config><bean id="concurrentOperationExecutor" class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor"> <property name="maxRetries" value="3"/> <property name="order" value="100"/></bean> |
请注意,目前我们假设所有业务服务都是幂等的。如果不是这种情况,我们可以通过引入Idempotent注解 并使用该注解来注解服务操作的实现,来改进切面,使其仅重试 true 的幂等操作,如以下示例所示:
1 | @Retention(RetentionPolicy.RUNTIME)public @interface Idempotent { // marker annotation} |
切面的更改仅重试幂等操作涉及精简切入点表达式,以便只有@Idempotent个操作匹配,如下所示:
1 | <aop:pointcut id="idempotentOperation" expression="execution(* com.xyz.myapp.service.*.*(..)) and @annotation(com.xyz.myapp.service.Idempotent)"/> |
选择要使用的 AOP 声明样式
一旦确定切面是实现给定需求的最佳方法,您如何在使用 Spring AOP 或 AspectJ 以及在 Aspect 语言(代码)样式,@ AspectJ 注解样式或 Spring XML 样式之间做出选择?这些决定受许多因素影响,包括应用程序需求,开发工具以及团队对 AOP 的熟悉程度。
5.6.1. Spring AOP 还是 Full AspectJ?
使用最简单的方法即可。 Spring AOP 比使用完整的 AspectJ 更简单,因为不需要在开发和构建过程中引入 AspectJ 编译器/编织器。如果您只需要建议在 Spring bean 上执行操作,则 Spring AOP 是正确的选择。如果您需要建议不受 Spring 容器 Management 的对象(通常是域对象),则需要使用 AspectJ。如果您希望建议除简单方法执行以外的连接点(例如,字段 get 或设置连接点等),则还需要使用 AspectJ。
使用 AspectJ 时,可以选择 AspectJ 语言语法(也称为“代码样式”)或@AspectJ注解 样式。显然,如果您不使用 Java 5,那么将为您做出选择:使用代码样式。如果切面在您的设计中起着重要作用,并且您能够将AspectJ 开发工具(AJDT)插件用于 Eclipse,则 AspectJ 语言语法是首选。它更干净,更简单,因为该语言是专为编写切面而设计的。如果您不使用 Eclipse 或只有少数几个切面在您的应用程序中不起作用,那么您可能要考虑使用@AspectJ 样式,在 IDE 中坚持常规 Java 编译,并向其中添加切面编织阶段您的构建脚本。
5.6.2. @AspectJ 或 Spring AOP 的 XML?
如果选择使用 Spring AOP,则可以选择@AspectJ 或 XML 样式。有各种折衷考虑。
XML 样式可能是现有 Spring 用户最熟悉的,并且得到了 true 的 POJO 的支持。当使用 AOP 作为配置企业服务的工具时,XML 可能是一个不错的选择(一个很好的测试是您是否将切入点表达式视为配置的一部分,您可能希望独立更改)。使用 XML 样式,可以说从您的配置中可以更清楚地了解系统中存在哪些切面。
XML 样式有两个缺点。首先,它没有完全将要解决的需求的实现封装在一个地方。 DRY 原则说,系统中的任何知识都应该有单一,明确,Authority 的表示形式。当使用 XML 样式时,关于如何实现需求的知识会在配置文件中的后备 bean 类的声明和 XML 中分散。当您使用@AspectJ 样式时,此信息将封装在一个单独的模块中:切面。其次,与@AspectJ 样式相比,XML 样式在表达能力上有更多限制:仅支持“单例”切面实例化模型,并且无法组合以 XML 声明的命名切入点。例如,使用@AspectJ 样式,您可以编写如下内容:
1 | @Pointcut("execution(* get*())")public void propertyAccess() {}@Pointcut("execution(org.xyz.Account+ *(..))")public void operationReturningAnAccount() {}@Pointcut("propertyAccess() && operationReturningAnAccount()")public void accountPropertyAccess() {} |
在 XML 样式中,您可以声明前两个切入点:
1 | <aop:pointcut id="propertyAccess" expression="execution(* get*())"/><aop:pointcut id="operationReturningAnAccount" expression="execution(org.xyz.Account+ *(..))"/> |
XML 方法的缺点是无法通过组合这些定义来定义accountPropertyAccess切入点。
@AspectJ 样式支持其他实例化模型和更丰富的切入点组合。它具有将切面保持为模块化单元的优势。它还具有的优点是,Spring AOP 和 AspectJ 都可以理解@AspectJ 切面。因此,如果您以后决定需要 AspectJ 的功能来实现其他要求,则可以轻松地迁移到基于 AspectJ 的方法。总而言之,只要您拥有比简单地配置企业服务更多的功能,Spring 团队就会喜欢@AspectJ 样式。
5.7. 混合切面类型
通过使用自动代理支持,模式定义的<aop:aspect>切面,<aop:advisor>声明的顾问程序,甚至是在同一配置中使用 Spring 1.2 样式定义的代理和拦截器,完全可以混合使用@AspectJ 样式的切面。所有这些都是通过使用相同的基础支持机制实现的,并且可以毫无困难地共存。
5.8. 代理机制
Spring AOP 使用 JDK 动态代理或 CGLIB 创建给定目标对象的代理。 (只要有选择,首选 JDK 动态代理)。
如果要代理的目标对象实现至少一个接口,则使用 JDK 动态代理。代理了由目标类型实现的所有接口。如果目标对象未实现任何接口,则将创建 CGLIB 代理。
如果要强制使用 CGLIB 代理(例如,代理为目标对象定义的每个方法,而不仅是由其接口实现的方法),都可以这样做。但是,您应该考虑以下问题:
- 不能建议
final方法,因为它们不能被覆盖。 - 从 Spring 3.2 开始,不再需要将 CGLIB 添加到您的项目 Classpath 中,因为 CGLIB 类在
org.springframework下重新打包并直接包含在 spring-core JAR 中。这意味着基于 CGLIB 的代理支持“有效”,就像 JDK 动态代理始终具有的一样。 - 从 Spring 4.0 开始,由于 CGLIB 代理实例是通过 Objenesis 创建的,因此不再调用代理对象的构造函数两次。仅当您的 JVM 不允许绕过构造函数时,您才可能从 Spring 的 AOP 支持中看到两次调用和相应的调试日志条目。
要强制使用 CGLIB 代理,请将<aop:config>元素的proxy-target-class属性的值设置为 true,如下所示:
1 | <aop:config proxy-target-class="true"> <!-- other beans defined here... --></aop:config> |
要在使用@AspectJ 自动代理支持时强制 CGLIB 代理,请将<aop:aspectj-autoproxy>元素的proxy-target-class属性设置为true,如下所示:
1 | <aop:aspectj-autoproxy proxy-target-class="true"/> |
Note
多个<aop:config/>节在运行时折叠到一个统一的自动代理创建器中,该创建器将应用<aop:config/>节中的任何(通常来自不同 XML bean 定义文件)指定的* strong *代理设置。这也适用于<tx:annotation-driven/>和<aop:aspectj-autoproxy/>元素。
明确地说,在<tx:annotation-driven/>,<aop:aspectj-autoproxy/>或<aop:config/>元素上使用proxy-target-class="true"会强制对所有三个元素*使用 CGLIB 代理。
5.8.1. 了解 AOP 代理
Spring AOP 是基于代理的。在编写自己的切面或使用 Spring Framework 随附的任何基于 Spring AOP 的切面之前,掌握最后一条语句实际含义的语义至关重要。
首先考虑您有一个普通的,未经代理的,没有什么特别的,直接的对象引用的情况,如以下代码片段所示:
1 | 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... }} |
如果在对象引用上调用方法,则直接在该对象引用上调用该方法,如下图和清单所示:

1 | 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(); }} |
当 Client 端代码具有的引用是代理时,情况会稍有变化。考虑以下图表和代码片段:

1 | 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(..)方法内部的 Client 端代码具有对代理的引用。这意味着该对象引用上的方法调用是代理上的调用。结果,代理可以委派给与该特定方法调用相关的所有拦截器(建议)。但是,一旦调用最终到达目标对象(在这种情况下为SimplePojo,则为this.bar()或this.foo()),则将针对this引用而不是对this引用调用它可能对其自身进行的任何方法调用。代理。这具有重要的意义。这意味着自调用不会导致与方法调用相关的建议得到执行的机会。
好吧,那么该怎么办?最好的方法(此处宽松地使用术语“最好”)是重构代码,以免发生自调用。这确实需要您做一些工作,但这是最好的,侵入性最小的方法。下一种方法绝对可怕,我们正要指出这一点,恰恰是因为它是如此可怕。您可以(对我们来说是痛苦的)完全将类中的逻辑与 Spring AOP 绑定在一起,如以下示例所示:
1 | 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 却是事实。创建代理时,还需要一些其他配置,如以下示例所示:
1 | public class Main { public static void main(String[] args) { ProxyFactory factory = new ProxyFactory(new SimplePojo()); factory.adddInterface(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 框架。
5.9. 以编程方式创建@AspectJ 代理
除了使用<aop:config>或<aop:aspectj-autoproxy>声明配置中的各个切面外,还可以通过编程方式创建建议目标对象的代理。有关 Spring 的 AOP API 的完整详细信息,请参见next chapter。在这里,我们要重点介绍通过使用@AspectJ 切面自动创建代理的功能。
您可以使用org.springframework.aop.aspectj.annotation.AspectJProxyFactory类为一个或多个@AspectJ 切面建议的目标对象创建代理。此类的基本用法非常简单,如以下示例所示:
1 | // create a factory that can generate a proxy for the given target objectAspectJProxyFactory 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 aspectsfactory.addAspect(SecurityManager.class);// you can also add existing aspect instances, the type of the object supplied must be an @AspectJ aspectfactory.addAspect(usageTracker);// now get the proxy object...MyInterfaceType proxy = factory.getProxy(); |
有关更多信息,请参见javadoc。
5.10. 在 Spring 应用程序中使用 AspectJ
到目前为止,本章介绍的所有内容都是纯 Spring AOP。在本节中,我们将研究如果您的需求超出了 Spring AOP 所提供的功能,那么如何使用 AspectJ 编译器或 weaver 代替 Spring AOP 或除 Spring AOP 之外使用。
Spring 附带了一个小的 AspectJ 切面库,该库在您的发行版中可以作为spring-aspects.jar独立使用。您需要将其添加到 Classpath 中才能使用其中的切面。 使用 AspectJ 通过 Spring 依赖注入域对象和AspectJ 的其他 Spring 切面讨论了该库的内容以及如何使用它。 使用 Spring IoC 配置 AspectJ Aspects讨论如何依赖注入使用 AspectJ 编译器编织的 AspectJ 切面。最后,在 Spring Framework 中使用 AspectJ 进行加载时编织介绍了使用 AspectJ 的 Spring 应用程序的加载时编织。
5.10.1. 使用 AspectJ 通过 Spring 依赖注入域对象
Spring 容器实例化并配置在您的应用程序上下文中定义的 bean。给定包含要应用的配置的 Bean 定义的名称,也可以要求 Bean 工厂配置预先存在的对象。 spring-aspects.jar包含注解驱动的切面,该切面利用此功能允许任何对象的依赖项注入。该支架旨在用于在任何容器的控制范围之外创建的对象。域对象通常属于此类,因为它们通常是通过new运算符或通过 ORM 工具以数据库查询的方式通过程序创建的。
@Configurable注解 将一个类标记为符合 Spring 驱动的配置。在最简单的情况下,您可以将其纯粹用作标记 注解,如以下示例所示:
1 | package com.xyz.myapp.domain;import org.springframework.beans.factory.annotation.Configurable; class Account { // ...} |
当以这种方式用作标记接口时,Spring 通过使用具有与完全限定类型名称(com.xyz.myapp.domain.Account)同名的 bean 定义(通常为原型作用域)来配置带注解类型的新实例(在本例中为Account)。 。由于 Bean 的默认名称是其类型的完全限定名称,因此声明原型定义的便捷方法是省略id属性,如以下示例所示:
1 | <bean class="com.xyz.myapp.domain.Account" scope="prototype"> <property name="fundsTransferService" ref="fundsTransferService"/></bean> |
如果要显式指定要使用的原型 bean 定义的名称,则可以直接在注解中这样做,如以下示例所示:
1 | package com.xyz.myapp.domain;import org.springframework.beans.factory.annotation.Configurable;public class Account { // ...} |
Spring 现在查找名为account的 bean 定义,并将其用作配置新Account实例的定义。
您也可以使用自动装配来避免完全指定专用的 bean 定义。要让 Spring 应用自动装配,请使用@Configurable注解的autowire属性。您可以分别按类型或名称指定@Configurable(autowire=Autowire.BY_TYPE)或@Configurable(autowire=Autowire.BY_NAME自动布线。或者,从 Spring 2.5 开始,最好在字段或方法级别使用@Autowired或@Inject为@Configurable bean 指定显式的,注解 驱动的依赖项注入(有关更多详细信息,请参见基于注解的容器配置)。
最后,您可以使用dependencyCheck属性(例如@Configurable(autowire=Autowire.BY_NAME,dependencyCheck=true))为新创建和配置的对象中的对象引用启用 Spring 依赖项检查。如果此属性设置为true,则 Spring 在配置后验证是否已设置所有属性(不是基元或集合)。
请注意,单独使用注解不会执行任何操作。注解 中存在的是spring-aspects.jar中的AnnotationBeanConfigurerAspect。从本质上讲,切面说:“在从带有@Configurable注解 的类型的新对象的初始化返回之后,根据注解的属性使用 Spring 配置新创建的对象”。在这种情况下,“初始化”是指新实例化的对象(例如,用new运算符实例化的对象)以及正在反序列化(例如,通过readResolve())的Serializable对象。
Note
上段中的关键短语之一是“本质上”。对于大多数情况,“从新对象的初始化返回后”的确切语义是可以的。在这种情况下,“初始化之后”是指在构造对象之后注入依赖项。这意味着该依赖项不可在类的构造函数体中使用。如果要在构造函数主体执行之前注入依赖项,从而可以在构造函数主体中使用这些依赖项,则需要在@Configurable声明中定义此变量,如下所示:
1 |
您可以在AspectJ 编程指南的 AspectJ 在本附录中中找到有关各种切入点类型的语言语义的更多信息。
为此,必须将带注解的类型与 AspectJ 编织器编织在一起。您可以使用构建时 Ant 或 Maven 任务来执行此操作(例如,参见__),也可以使用加载时编织(请参见在 Spring Framework 中使用 AspectJ 进行加载时编织)。 AnnotationBeanConfigurerAspect本身需要由 Spring 配置(以获取对将用于配置新对象的 Bean 工厂的引用)。如果使用基于 Java 的配置,则可以将@EnableSpringConfigured添加到任何@Configuration类中,如下所示:
1 | @Configuration@EnableSpringConfiguredpublic class AppConfig {} |
如果您喜欢基于 XML 的配置,Spring context namespace定义了一个方便的context:spring-configured元素,您可以按以下方式使用它:
1 | <context:spring-configured/> |
在配置切面之前创建的@Configurable个对象的实例导致向调试日志发出一条消息,并且未进行任何对象配置。一个示例可能是 Spring 配置中的 bean,当它由 Spring 初始化时会创建域对象。在这种情况下,可以使用depends-on bean 属性来手动指定该 bean 取决于配置切面。下面的示例演示如何使用depends-on属性:
1 | <bean id="myService" class="com.xzy.myapp.service.MyService" depends-on="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect"> <!-- ... --></bean> |
Note
除非您真的想在运行时依赖它的语义,否则不要通过 bean configurer 切面激活@Configurable处理。特别是,请确保不要在已通过容器注册为常规 Spring bean 的 bean 类上使用@Configurable。这样做将导致两次初始化,一次是通过容器,一次是通过切面。
单元测试@Configurable 对象
@Configurable支持的目标之一是实现域对象的独立单元测试,而不会遇到与硬编码查找相关的困难。如果 AspectJ 尚未编织@Configurable类型,则注解在单元测试期间不起作用。您可以在被测对象中设置模拟或存根属性引用,然后照常进行。如果@Configurable类型是 AspectJ 编织的,您仍然可以像往常一样在容器外部进行单元测试,但是每次构造@Configurable对象时,都会看到一条警告消息,指示该对象尚未由 Spring 配置。
处理多个应用程序上下文
用于实现@Configurable支持的AnnotationBeanConfigurerAspect是 AspectJ 单例切面。单例切面的范围与static成员的范围相同:每个类加载器都有一个切面实例来定义类型。这意味着,如果您在同一个类加载器层次结构中定义多个应用程序上下文,则需要考虑在哪里定义@EnableSpringConfigured bean 以及在哪里将spring-aspects.jar放在 Classpath 上。
考虑一个典型的 Spring Web 应用程序配置,该配置具有一个共享的父应用程序上下文,该上下文定义了通用的业务服务,支持那些服务所需的一切,以及每个 Servlet 的一个子应用程序上下文(其中包含该 Servlet 的特定定义)。所有这些上下文共存于相同的类加载器层次结构中,因此AnnotationBeanConfigurerAspect只能保留对其中一个的引用。在这种情况下,我们建议在共享(父)应用程序上下文中定义@EnableSpringConfigured bean。这定义了您可能想注入域对象的服务。结果是,您无法使用@Configurable 机制来配置域对象,该域对象引用的是在子(特定于 servlet 的)上下文中定义的 bean 的引用(无论如何,这可能不是您想做的事情)。
在同一容器中部署多个 Web 应用程序时,请确保每个 Web 应用程序使用自己的类加载器(例如,将spring-aspects.jar放在'WEB-INF/lib'中)加载spring-aspects.jar中的类型。如果spring-aspects.jar仅添加到容器级的 Classpath 中(并因此由共享的父类加载器加载),则所有 Web 应用程序都共享相同的切面实例(这可能不是您想要的)。
5.10.2. AspectJ 的其他 Spring 切面
除了@Configurable切面之外,spring-aspects.jar还包含一个 AspectJ 切面,您可以使用它来驱动 Spring 的事务 Management,该事务 Management 使用@Transactional注解 进行注解的类型和方法。这主要适用于希望在 Spring 容器之外使用 Spring Framework 的事务支持的用户。
解释@Transactional注解 的切面是AnnotationTransactionAspect。使用此切面时,必须注解实现类(或该类中的方法或两者),而不是注解该类所实现的接口(如果有)。 AspectJ 遵循 Java 的规则,即不继承接口上的 注解。
类上的@Transactional注解 指定用于执行该类中任何公共操作的默认事务语义。
类内方法上的@Transactional注解 将覆盖类 注解(如果存在)给出的默认事务语义。可以标注任何可见性的方法,包括私有方法。直接注解非公共方法是执行此类方法而获得事务划分的唯一方法。
Tip
从 Spring Framework 4.2 开始,spring-aspects提供了类似的切面,为标准javax.transaction.Transactional注解 提供了完全相同的功能。检查JtaAnnotationTransactionAspect了解更多详细信息。
对于希望使用 Spring 配置和事务 Management 支持但又不想(或不能)使用注解的 AspectJ 程序员,spring-aspects.jar还包含abstract个切面,您可以扩展它们以提供自己的切入点定义。有关更多信息,请参见AbstractBeanConfigurerAspect和AbstractTransactionAspect切面的资源。例如,以下摘录显示了如何编写切面来使用与完全限定的类名匹配的原型 Bean 定义来配置域模型中定义的对象的所有实例:
1 | 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(..)) && SystemArchitecture.inDomainModel() && this(beanInstance);} |
5.10.3. 使用 Spring IoC 配置 AspectJ Aspects
当您将 AspectJ 切面与 Spring 应用程序一起使用时,既自然又希望能够使用 Spring 配置这些切面。 AspectJ 运行时本身负责切面的创建,并且通过 Spring 配置 AspectJ 创建的切面的方法取决于切面所使用的 AspectJ 实例化模型(per-xxx子句)。
AspectJ 的大多数切面都是单例切面。这些切面的配置很容易。您可以创建一个正常引用外观类型并包含factory-method="aspectOf" bean 属性的 bean 定义。这可以确保 Spring 通过向 AspectJ 索要长宽比实例,而不是尝试自己创建实例来获得长宽比实例。下面的示例演示如何使用factory-method="aspectOf"属性:
1 | <bean id="profiler" class="com.xyz.profiler.Profiler" factory-method="aspectOf"> (1) <property name="profilingStrategy" ref="jamonProfilingStrategy"/></bean> |
- (1) 请注意
factory-method="aspectOf"属性
非单一切面很难配置。但是,可以通过创建原型 bean 定义并使用spring-aspects.jar的@Configurable支持来配置切面实例(一旦 AspectJ 运行时创建了 bean)来实现。
如果您有一些要与 AspectJ 编织的@AspectJ 切面(例如,对域模型类型使用加载时编织)以及要与 Spring AOP 一起使用的其他@AspectJ 切面,那么这些切面都已在 Spring 中配置,您需要告诉 Spring AOP @AspectJ 自动代理支持,应使用配置中定义的@AspectJ 切面的确切子集进行自动代理。您可以使用<aop:aspectj-autoproxy/>声明中的一个或多个<include/>元素来完成此操作。每个<include/>元素都指定一个名称模式,并且只有名称与至少一个模式匹配的 bean 才可用于 Spring AOP 自动代理配置。以下示例显示了如何使用<include/>元素:
1 | <aop:aspectj-autoproxy> <aop:include name="thisBean"/> <aop:include name="thatBean"/></aop:aspectj-autoproxy> |
Note
不要被<aop:aspectj-autoproxy/>元素的名称所迷惑。使用它可以创建 Spring AOP 代理。此处使用了@AspectJ 样式的声明,但是不涉及 AspectJ 运行时。
5.10.4. 在 Spring Framework 中使用 AspectJ 进行加载时编织
加载时编织(LTW)是指在将 AspectJ 切面加载到应用程序的类文件中时将它们编织到 Java 虚拟机(JVM)中的过程。本部分的重点是在 Spring 框架的特定上下文中配置和使用 LTW。本节不是 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/org.springframework.instrument-{version}.jar(以前称为spring-agent.jar)所需的应用服务器的启动脚本进行任何修改。开发人员可以修改构成应用程序上下文的一个或多个文件,以实现加载时编织,而不必依赖通常负责部署配置(例如启动脚本)的 Management 员。
现在,销售工作已经结束,让我们首先浏览一个使用 Spring 的 AspectJ LTW 的快速示例,然后详细介绍示例中引入的元素。有关完整示例,请参见Petclinicsample 申请。
第一个例子
假设您是一位负责诊断系统中某些性能问题的原因的应用程序开发人员。与其使用分析工具,不如使用一个简单的分析切面,使我们能够快速获得一些性能 Metrics。然后,我们可以立即在该特定区域应用更细粒度的分析工具。
Note
此处提供的示例使用 XML 配置。您还可以将Java configuration配置和使用@AspectJ。具体来说,您可以使用@EnableLoadTimeWeaving注解 替代<context:load-time-weaver/>(有关详细信息,请参见below)。
下面的示例显示了配置切面,它不是花哨的-它是基于时间的探查器,它使用@AspectJ 样式的切面声明:
1 | package foo;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; class ProfilingAspect { 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()); } } public void methodsToBeProfiled(){}} |
我们还需要创建一个META-INF/aop.xml文件,以通知 AspectJ 编织者我们要将ProfilingAspect编织到类中。此文件约定,即在 JavaClasspath 上名为META-INF/aop.xml的文件,是标准 AspectJ。下面的示例显示aop.xml文件:
1 | <aspectj> <weaver> <!-- only weave classes in our application-specific packages --> <include within="foo.*"/> </weaver> <aspects> <!-- weave in just this aspect --> <aspect name="foo.ProfilingAspect"/> </aspects></aspectj> |
现在,我们可以 continue 进行配置中特定于 Spring 的部分。我们需要配置一个LoadTimeWeaver(稍后说明)。此加载时织布器是必不可少的组件,负责将一个或多个META-INF/aop.xml文件中的切面配置编织到应用程序的类中。好处是,它不需要很多配置(您可以指定一些其他选项,但是稍后会详细介绍),如以下示例所示:
1 | <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 http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!-- a service object; we will be profiling its methods --> <bean id="entitlementCalculationService" class="foo.StubEntitlementCalculationService"/> <!-- this switches on the load-time weaving --> <context:load-time-weaver/></beans> |
现在,所有必需的构件(切面,META-INF/aop.xml文件和 Spring 配置)都就位了,我们可以使用main(..)方法创建以下驱动程序类,以演示实际的 LTW:
1 | package foo;import org.springframework.context.support.ClassPathXmlApplicationContext;public final class Main { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml", Main.class); EntitlementCalculationService entitlementCalculationService = (EntitlementCalculationService) ctx.getBean("entitlementCalculationService"); // the profiling aspect is 'woven' around this method execution entitlementCalculationService.calculateEntitlement(); }} |
我们还有最后一件事要做。本节的引言确实说过,可以使用 Spring 以ClassLoader为基础选择性地打开 LTW,这是事实。但是,在此示例中,我们使用 Java 代理(Spring 随附)打开 LTW。我们使用以下命令运行前面显示的Main类:
1 | java -javaagent:C:/projects/foo/lib/global/spring-instrument.jar foo.Main |
-javaagent是用于指定和启用代理来检测在 JVM 上运行的程序的标志。 Spring 框架附带了这样的代理InstrumentationSavingAgent,该代理打包在spring-instrument.jar中,在上一示例中,该代理作为-javaagent自变量的值提供。
Main程序的执行输出类似于下一个示例。 (我在calculateEntitlement()实现中引入了Thread.sleep(..)语句,以便探查器实际上捕获的不是 0 毫秒(01234毫秒不是 AOP 引入的开销)。以下清单显示了运行探查器时得到的输出:
1 | Calculating entitlementStopWatch 'ProfilingAspect': running time (millis) = 1234------ ----- ----------------------------ms % Task name------ ----- ----------------------------01234 100% calculateEntitlement |
由于此 LTW 是通过使用成熟的 AspectJ 来实现的,因此我们不仅限于建议 Spring Bean。 Main程序的以下细微变化会产生相同的结果:
1 | package foo;import org.springframework.context.support.ClassPathXmlApplicationContext;public final class Main { public static void main(String[] args) { new ClassPathXmlApplicationContext("beans.xml", Main.class); EntitlementCalculationService entitlementCalculationService = new StubEntitlementCalculationService(); // the profiling aspect will be 'woven' around this method execution entitlementCalculationService.calculateEntitlement(); }} |
注意,在前面的程序中,我们如何引导 Spring 容器,然后完全在 Spring 上下文之外创建StubEntitlementCalculationService的新实例。剖析建议仍会被应用。
诚然,这个例子很简单。但是,在前面的示例中已经介绍了 Spring 对 LTW 支持的基础,本节的其余部分详细解释了每一位配置和用法的“原因”。
Note
在此示例中使用的ProfilingAspect可能是基本的,但很有用。这是开发时切面的一个很好的示例,开发人员可以在开发过程中使用它,然后轻松地将其从部署到 UAT 或 Producing 的应用程序构建中排除。
Aspects
您在 LTW 中使用的切面必须是 AspectJ 切面。您可以使用 AspectJ 语言本身来编写它们,也可以使用@AspectJ 风格来编写切面。这样,您的切面就是有效的 AspectJ 和 Spring AOP 切面。此外,编译的切面类需要在 Classpath 上可用。
‘META-INF/aop.xml’
通过使用 JavaClasspath 上的一个或多个META-INF/aop.xml文件(直接或通常在 jar 文件中)来配置 AspectJ LTW 基础结构。
该文件的结构和内容在 LTW 部分AspectJ 参考文档中详细介绍。由于 aop.xml 文件是 100%AspectJ,因此在此不再赘述。
必需的库(JARS)
至少,您需要以下库来使用 Spring Framework 对 AspectJ LTW 的支持:
spring-aop.jar(2.5 版或更高版本,以及所有强制性依赖项)aspectjweaver.jar(1.6.8 版或更高版本)
如果您使用Spring 提供的代理程序可实现检测,则还需要:
spring-instrument.jar
Spring Configuration
Spring 的 LTW 支持中的关键组件是LoadTimeWeaver接口(在org.springframework.instrument.classloading包中),以及 Spring 发行版附带的众多实现。 LoadTimeWeaver负责在运行时将一个或多个java.lang.instrument.ClassFileTransformers添加到ClassLoader,这为各种有趣的应用程序打开了大门,其中之一恰好是切面的 LTW。
Tip
如果您不熟悉运行时类文件转换的概念,请在 continue 之前先查看java.lang.instrument软件包的 javadoc API 文档。虽然该文档并不全面,但是至少您可以看到关键的接口和类(在您阅读本节时作为参考)。
为特定的ApplicationContext配置LoadTimeWeaver就像添加一行一样容易。 (请注意,您几乎肯定需要使用ApplicationContext作为您的 Spring 容器—通常,BeanFactory是不够的,因为 LTW 支持使用BeanFactoryPostProcessors.)
要启用 Spring Framework 的 LTW 支持,您需要配置LoadTimeWeaver,通常通过使用@EnableLoadTimeWeaving注解来完成,如下所示:
1 | @Configuration@EnableLoadTimeWeavingpublic class AppConfig {} |
另外,如果您更喜欢基于 XML 的配置,请使用<context:load-time-weaver/>元素。请注意,该元素是在context名称空间中定义的。以下示例显示了如何使用<context:load-time-weaver/>:
1 | <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 http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:load-time-weaver/></beans> |
前面的配置会自动为您定义并注册许多 LTW 特定的基础结构 Bean,例如LoadTimeWeaver和AspectJWeavingEnabler。缺省LoadTimeWeaver是DefaultContextLoadTimeWeaver类,它将尝试装饰自动检测到的LoadTimeWeaver。 “自动检测到”的LoadTimeWeaver的确切类型取决于您的运行时环境。下表总结了各种LoadTimeWeaver实现:
*表 13. DefaultContextLoadTimeWeaver LoadTimeWeavers *
| Runtime Environment | LoadTimeWeaver实施 |
|---|---|
| 在 Oracle 的WebLogic中运行 | WebLogicLoadTimeWeaver |
| 在 Oracle 的GlassFish中运行 | GlassFishLoadTimeWeaver |
| 在Apache Tomcat中运行 | TomcatLoadTimeWeaver |
| 在 Red Hat 的JBoss AS或WildFly中运行 | JBossLoadTimeWeaver |
| 在 IBM 的WebSphere中运行 | WebSphereLoadTimeWeaver |
JVM 从 Spring InstrumentationSavingAgent(java -javaagent:path/to/spring-instrument.jar)开始 |
InstrumentationLoadTimeWeaver |
回退,期望基础 ClassLoader 遵循通用约定(例如适用于TomcatInstrumentableClassLoader和Resin) |
ReflectiveLoadTimeWeaver |
请注意,该表仅列出了使用DefaultContextLoadTimeWeaver时自动检测到的LoadTimeWeavers。您可以确切指定要使用的LoadTimeWeaver实现。
要使用 Java 配置指定特定的LoadTimeWeaver,请实现LoadTimeWeavingConfigurer接口并覆盖getLoadTimeWeaver()方法。以下示例指定ReflectiveLoadTimeWeaver:
1 | @Configuration@EnableLoadTimeWeavingpublic class AppConfig implements LoadTimeWeavingConfigurer { @Override public LoadTimeWeaver getLoadTimeWeaver() { return new ReflectiveLoadTimeWeaver(); }} |
如果使用基于 XML 的配置,则可以将完全限定的类名指定为<context:load-time-weaver/>元素上的weaver-class属性的值。同样,以下示例指定ReflectiveLoadTimeWeaver:
1 | <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 http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:load-time-weaver weaver-class="org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver"/></beans> |
以后可以使用众所周知的名称loadTimeWeaver从 Spring 容器中检索由配置定义和注册的LoadTimeWeaver。请记住,LoadTimeWeaver仅作为 Spring 的 LTW 基础结构添加一个或多个ClassFileTransformers的机制而存在。执行 LTW 的实际ClassFileTransformer是ClassPreProcessorAgentAdapter(来自org.aspectj.weaver.loadtime程序包)类。有关更多详细信息,请参见ClassPreProcessorAgentAdapter类的类级 javadoc,因为实际上如何实现编织的细节不在本文档的讨论范围之内。
还需要讨论配置的最后一个属性:aspectjWeaving属性(如果使用 XML,则为aspectj-weaving)。此属性控制是否启用 LTW。它接受三个可能的值之一,如果属性不存在,则默认值为autodetect。下表总结了三个可能的值:
表 14. AspectJ 编织属性值
| Annotation Value | XML Value | Explanation |
|---|---|---|
ENABLED |
on |
AspectJ 正在编织,并且在加载时适当地编织了切面。 |
DISABLED |
off |
LTW 已关闭。加载时不会编织任何切面。 |
AUTODETECT |
autodetect |
如果 Spring LTW 基础结构可以找到至少一个META-INF/aop.xml文件,则表示 AspectJ 编织已开始。否则,它关闭。这是默认值。 |
Environment-specific Configuration
最后一部分包含在应用程序服务器和 Web 容器等环境中使用 Spring 的 LTW 支持时所需的所有其他设置和配置。
Tomcat
从历史上看,Apache Tomcat的默认类加载器不支持类转换,因此 Spring 提供了增强的实现来满足此需求。名为TomcatInstrumentableClassLoader的加载程序可在 Tomcat 6.0 及更高版本上运行。
Tip
不要在 Tomcat 8.0 及更高版本上定义TomcatInstrumentableClassLoader。相反,让 Spring 通过TomcatLoadTimeWeaver策略自动使用 Tomcat 的新本机InstrumentableClassLoader工具。
如果仍然需要使用TomcatInstrumentableClassLoader,则可以为每个 Web 应用程序分别进行注册,如下所示:
- 将
org.springframework.instrument.tomcat.jar复制到$CATALINA_HOME/lib,其中$CATALINA_HOME代表 Tomcat 安装的根目录 - 通过编辑 Web 应用程序上下文文件,指示 Tomcat 使用自定义类加载器(而不是默认值),如以下示例所示:
1 | <Context path="/myWebApp" docBase="/my/webApp/location"> <Loader loaderClass="org.springframework.instrument.classloading.tomcat.TomcatInstrumentableClassLoader"/></Context> |
Apache Tomcat 6.0 支持多个上下文位置:
- 服务器配置文件:
$CATALINA_HOME/conf/server.xml - 默认上下文配置:
$CATALINA_HOME/conf/context.xml,这会影响所有已部署的 Web 应用程序 - 每个 Web 应用程序配置,可以在
$CATALINA_HOME/conf/[enginename]/[hostname]/[webapp]-context.xml部署在服务器端,也可以在META-INF/context.xml嵌入在 Web 应用程序存档中
为了提高效率,我们建议使用嵌入式的逐个 Web 应用程序配置样式,因为它只影响使用自定义类加载器的应用程序,并且不需要对服务器配置进行任何更改。有关可用上下文位置的更多详细信息,请参见 Tomcat 6.0.x documentation。
或者,考虑使用 Spring 提供的通用 VM 代理,该代理在 Tomcat 的启动脚本中指定(本节前面已描述)。这使得检测对所有已部署的 Web 应用程序均可用,无论它们运行在ClassLoader上。
WebLogic,WebSphere,Resin,GlassFish 和 JBoss
最新版本的 WebLogic Server(版本 10 和更高版本),IBM WebSphere Application Server(版本 7 和更高版本),Resin(版本 3.1 和更高版本)以及 JBoss(版本 6.x 或更高版本)提供了ClassLoader并能够进行本地检测。 Spring 的本地 LTW 利用此类 ClassLoader 实现来实现 AspectJ 编织。您可以通过激活加载时编织来启用 LTW,如described earlier。具体来说,您无需修改启动脚本即可添加-javaagent:path/to/spring-instrument.jar。
请注意,具有 GlassFish 工具功能的ClassLoader仅在其 EAR 环境中可用。对于 GlassFish Web 应用程序,请遵循 Tomcat 设置说明outlined earlier。
请注意,在 JBoss 6.x 上,您需要禁用应用服务器扫描,以防止它在应用程序实际启动之前加载类。一个快速的解决方法是将名为WEB-INF/jboss-scanning.xml的文件添加到您的工件中,其中包含以下内容:
1 | <scanning xmlns="urn:jboss:scanning:1.0"/> |
通用 Java 应用程序
在不支持或现有LoadTimeWeaver实现不支持的环境中需要类检测时,JDK 代理可以是唯一的解决方案。对于这种情况,Spring 提供了InstrumentationLoadTimeWeaver,这需要一个 Spring 特定(但非常通用)的 VM 代理org.springframework.instrument-{version}.jar(以前称为spring-agent.jar)。
要使用它,您必须通过提供以下 JVM 选项来使用 Spring 代理启动虚拟机:
1 | -javaagent:/path/to/org.springframework.instrument-{version}.jar |
请注意,这需要修改 VM 启动脚本,这可能会阻止您在应用程序服务器环境中使用它(取决于您的操作策略)。此外,JDK 代理会检测整个 VM,这可能会很昂贵。
出于性能原因,我们建议仅在目标环境(例如Jetty)没有(或不支持)专用 LTW 的情况下才使用此配置。
5.11. 更多资源
可以在AspectJ website上找到有关 AspectJ 的更多信息。
- Adrian Colyer 等人的《 Eclipse AspectJ *》。等(Addison-Wesley,2005 年)为 AspectJ 语言提供了全面的介绍和参考。
强烈推荐 Ramnivas Laddad(Manning,2009)出版的《 AspectJ in Action *》第二版。本书的重点是 AspectJ,但在一定程度上探讨了许多通用的 AOP 主题。
6. Spring AOP API
上一章使用@AspectJ 和基于模式的切面定义描述了 Spring 对 AOP 的支持。在本章中,我们讨论较低级别的 Spring AOP API 和通常在 Spring 1.2 应用程序中使用的 AOP 支持。对于新应用程序,我们建议使用上一章中介绍的 Spring 2.0 和更高版本的 AOP 支持。但是,当您使用现有应用程序(或阅读书籍和文章)时,可能会遇到 Spring 1.2 样式的示例。 Spring 5 仍然与 Spring 1.2 向后兼容,Spring 5 完全支持本章中描述的所有内容。
6.1. Spring 中的 Pointcut API
本节描述了 Spring 如何处理关键切入点概念。
6.1.1. Concepts
Spring 的切入点模型使切入点重用不受建议类型的影响。您可以使用相同的切入点来定位不同的建议。
org.springframework.aop.Pointcut界面是中央界面,用于将建议定向到特定的类和方法。完整的界面如下:
1 | public interface Pointcut { ClassFilter getClassFilter(); MethodMatcher getMethodMatcher();} |
将Pointcut接口分为两部分,可以重用类和方法匹配的部分以及细粒度的合成操作(例如与另一个方法匹配器执行“联合”)。
ClassFilter接口用于将切入点限制为给定的一组目标类。如果matches()方法始终返回 true,则匹配所有目标类。以下清单显示了ClassFilter接口定义:
1 | public interface ClassFilter { boolean matches(Class clazz);} |
MethodMatcher界面通常更重要。完整的界面如下:
1 | public interface MethodMatcher { boolean matches(Method m, Class targetClass); boolean isRuntime(); boolean matches(Method m, Class targetClass, Object[] args);} |
matches(Method, Class)方法用于测试此切入点是否与目标类上的给定方法匹配。创建 AOP 代理时可以执行此评估,以避免需要对每个方法调用进行测试。如果给定方法的两个参数的matches方法返回true,而用于 MethodMatcher 的isRuntime()的方法返回true,则在每次方法调用时都会调用三个参数的 match 方法。这样,切入点就可以在执行目标建议之前立即查看传递给方法调用的参数。
大多数MethodMatcher实现是静态的,这意味着它们的isRuntime()方法返回false。在这种情况下,永远不会调用三参数matches方法。
Tip
如果可能,请尝试使切入点成为静态,从而在创建 AOP 代理时允许 AOP 框架缓存切入点评估的结果。
6.1.2. 切入点的操作
Spring 支持切入点上的操作(特别是联合和相交)。
联合表示两个切入点都匹配的方法。交集是指两个切入点都匹配的方法。联合通常更有用。您可以使用org.springframework.aop.support.Pointcuts类中的静态方法或同一包中的ComposablePointcut类来编写切入点。但是,使用 AspectJ 切入点表达式通常是一种更简单的方法。
6.1.3. AspectJ 表达切入点
从 2.0 开始,Spring 使用的最重要的切入点类型是org.springframework.aop.aspectj.AspectJExpressionPointcut。这是一个切入点,该切入点使用 AspectJ 提供的库来解析 AspectJ 切入点表达式字符串。
有关支持的 AspectJ 切入点 Primitives 的讨论,请参见previous chapter。
6.1.4. 便捷切入点实现
Spring 提供了几种方便的切入点实现。您可以直接使用其中一些。其他的则应归入特定于应用程序的切入点中。
Static Pointcuts
静态切入点是基于方法和目标类的,不能考虑方法的参数。静态切入点足以满足大多数用途,并且最好。首次调用方法时,Spring 只能评估一次静态切入点。之后,无需在每次方法调用时再次评估切入点。
本节的其余部分描述了 Spring 附带的一些静态切入点实现。
正则表达式切入点
指定静态切入点的一种明显方法是正则表达式。除了 Spring 之外,还有几个 AOP 框架使之成为可能。 org.springframework.aop.support.JdkRegexpMethodPointcut是通用正则表达式切入点,它使用 JDK 中的正则表达式支持。
使用JdkRegexpMethodPointcut类,可以提供模式字符串列表。如果其中任何一个匹配,则切入点的值为true。 (因此,结果实际上是这些切入点的并集.)
以下示例显示了如何使用JdkRegexpMethodPointcut:
1 | <bean id="settersAndAbsquatulatePointcut" class="org.springframework.aop.support.JdkRegexpMethodPointcut"> <property name="patterns"> <list> <value>.*set.*</value> <value>.*absquatulate</value> </list> </property></bean> |
Spring 提供了一个名为RegexpMethodPointcutAdvisor的便捷类,它使我们也可以引用Advice(请记住Advice可以是拦截器,而不是建议,引发建议等)。在后台,Spring 使用JdkRegexpMethodPointcut。使用RegexpMethodPointcutAdvisor简化了接线,因为一个 bean 封装了切入点和建议,如以下示例所示:
1 | <bean id="settersAndAbsquatulateAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"> <property name="advice"> <ref bean="beanNameOfAopAllianceInterceptor"/> </property> <property name="patterns"> <list> <value>.*set.*</value> <value>.*absquatulate</value> </list> </property></bean> |
您可以将RegexpMethodPointcutAdvisor与任何Advice类型一起使用。
Attribute-driven Pointcuts
静态切入点的一种重要类型是元数据驱动的切入点。这使用元数据属性的值(通常是源级别的元数据)。
Dynamic pointcuts
动态切入点比静态切入点更昂贵。它们考虑了方法参数以及静态信息。这意味着必须在每次方法调用时对它们进行评估,并且由于参数会有所不同,因此无法缓存结果。
主要示例是control flow切入点。
控制流切入点
Spring 控制流切入点在概念上类似于 AspectJ cflow切入点,但功能较弱。 (当前无法指定一个切入点在与另一个切入点匹配的连接点下执行.)控制流切入点与当前调用堆栈匹配。例如,如果连接点是由com.mycompany.web包中的方法或SomeCaller类调用的,则可能会触发。通过使用org.springframework.aop.support.ControlFlowPointcut类指定控制流切入点。
Note
与其他动态切入点相比,控制流切入点在运行时进行评估要昂贵得多。在 Java 1.4 中,成本大约是其他动态切入点的五倍。
6.1.5. 切入点超类
Spring 提供了有用的切入点超类,以帮助您实现自己的切入点。
因为静态切入点最有用,所以您可能应该子类StaticMethodMatcherPointcut。这仅需要实现一个抽象方法(尽管您可以覆盖其他方法以自定义行为)。以下示例显示了如何对StaticMethodMatcherPointcut进行子类化:
1 | class TestStaticPointcut extends StaticMethodMatcherPointcut { public boolean matches(Method m, Class targetClass) { // return true if custom criteria match }} |
动态切入点也有超类。
在 Spring 1.0 RC2 及更高版本中,您可以将自定义切入点与任何建议类型一起使用。
6.1.6. 自定义切入点
由于 Spring AOP 中的切入点是 Java 类,而不是语言功能(如 AspectJ),因此可以声明自定义切入点,无论是静态还是动态。 Spring 中的自定义切入点可以任意复杂。但是,如果可以的话,我们建议使用 AspectJ 切入点表达语言。
Note
更高版本的 Spring 可能提供对 JAC offered 提供的“语义切入点”的支持,例如,“更改目标对象中实例变量的所有方法”。
6.2. Spring 咨询 API
现在,我们可以检查 Spring AOP 如何处理建议。
6.2.1. 咨询生命周期
每个建议都是一个 Spring bean。建议实例可以在所有建议对象之间共享,或者对于每个建议对象都是唯一的。这对应于每个类或每个实例的建议。
每班建议最常用。适用于一般建议,例如 Transaction 顾问。这些不依赖于代理对象的状态或添加新状态。它们仅作用于方法和参数。
每个实例的建议都适合引入,以支持混合。在这种情况下,建议将状态添加到代理对象。
您可以在同一 AOP 代理中混合使用共享建议和基于实例的建议。
6.2.2. Spring 的建议类型
Spring 提供了几种建议类型,并且可以扩展以支持任意建议类型。本节介绍基本概念和标准建议类型。
围绕建议进行拦截
Spring 中最基本的建议类型是围绕建议的拦截。
对于使用方法拦截的建议,Spring 符合 AOP Alliance接口。实现MethodInterceptor并围绕建议实现的类也应实现以下接口:
1 | public interface MethodInterceptor extends Interceptor { Object invoke(MethodInvocation invocation) throws Throwable;} |
invoke()方法的MethodInvocation参数公开了正在调用的方法,目标连接点,AOP 代理以及该方法的参数。 invoke()方法应返回调用的结果:连接点的返回值。
以下示例显示了一个简单的MethodInterceptor实现:
1 | public class DebugInterceptor implements MethodInterceptor { public Object invoke(MethodInvocation invocation) throws Throwable { System.out.println("Before: invocation=[" + invocation + "]"); Object rval = invocation.proceed(); System.out.println("Invocation returned"); return rval; }} |
请注意对MethodInvocation的proceed()方法的调用。这沿着拦截器链向下到达连接点。大多数拦截器都调用此方法并返回其返回值。但是,MethodInterceptor就像任何周围的建议一样,可以返回不同的值或引发异常,而不是调用 proceed 方法。但是,您没有充分的理由就不想这样做。
Note
MethodInterceptor实现提供与其他符合 AOP Alliance 要求的 AOP 实现的互操作性。本节其余部分讨论的其他建议类型将实现常见的 AOP 概念,但以特定于 Spring 的方式。尽管使用最具体的建议类型有一个优势,但是如果您可能想在另一个 AOP 框架中运行切面,请坚持使用MethodInterceptor。请注意,切入点当前无法在框架之间互操作,并且 AOP Alliance 当前未定义切入点接口。
Before Advice
一种更简单的建议类型是事前建议。不需要MethodInvocation对象,因为它仅在进入方法之前被调用。
先行建议的主要优点是无需调用proceed()方法,因此,不会无意中未能沿拦截器链 continue 前进。
以下清单显示了MethodBeforeAdvice界面:
1 | public interface MethodBeforeAdvice extends BeforeAdvice { void before(Method m, Object[] args, Object target) throws Throwable;} |
(尽管通常的对象适用于字段拦截,并且 Spring 不太可能实现它,但 Spring 的 API 设计允许先于字段咨询.)
请注意,返回类型为void。通知可以在联接点执行之前插入自定义行为,但不能更改返回值。如果之前的建议引发异常,它将中止拦截器链的进一步执行。异常会传播回拦截器链。如果未选中它或在调用的方法的签名上,则将其直接传递给 Client 端。否则,它将被 AOP 代理包装在未经检查的异常中。
以下示例显示了 Spring 中的 before 建议,该建议计算所有方法调用:
1 | public class CountingBeforeAdvice implements MethodBeforeAdvice { private int count; public void before(Method m, Object[] args, Object target) throws Throwable { ++count; } public int getCount() { return count; }} |
Tip
在将建议与任何切入点一起使用之前。
Throws Advice
如果联接点引发异常,则在联接点返回之后调用引发通知。 Spring 提供类型化的抛出建议。请注意,这意味着org.springframework.aop.ThrowsAdvice接口不包含任何方法。它是一个标签接口,用于标识给定对象实现了一个或多个类型化的 throws 通知方法。这些应采用以下形式:
1 | afterThrowing([Method, args, target], subclassOfThrowable) |
仅最后一个参数是必需的。方法签名可以具有一个或四个参数,具体取决于建议方法是否对该方法和参数感兴趣。接下来的两个清单显示了类,它们是引发建议的示例。
如果抛出RemoteException(包括来自子类),则调用以下建议:
1 | public class RemoteThrowsAdvice implements ThrowsAdvice { public void afterThrowing(RemoteException ex) throws Throwable { // Do something with remote exception }} |
与前面的建议不同,下一个示例声明四个参数,以便可以访问被调用的方法,方法参数和目标对象。如果抛出ServletException,则调用以下建议:
1 | public class ServletThrowsAdviceWithArguments implements ThrowsAdvice { public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) { // Do something with all arguments }} |
最后一个示例说明如何在处理RemoteException和ServletException的单个类中使用这两种方法。可以将任意数量的引发建议方法组合到一个类中。以下清单显示了最后一个示例:
1 | public static class CombinedThrowsAdvice implements ThrowsAdvice { public void afterThrowing(RemoteException ex) throws Throwable { // Do something with remote exception } public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) { // Do something with all arguments }} |
Note
如果 throws-advice 方法本身引发异常,则它将覆盖原始异常(也就是说,它将更改引发给用户的异常)。重写异常通常是 RuntimeException,它与任何方法签名都兼容。但是,如果 throws-advice 方法抛出一个已检查的异常,则它必须与目标方法的已声明异常匹配,因此在某种程度上与特定的目标方法签名耦合。 请勿抛出与目标方法签名不兼容的未声明检查异常!
Tip
抛出建议可以与任何切入点一起使用。
返回建议后
在 Spring 中返回通知后,必须实现org.springframework.aop.AfterReturningAdvice接口,以下清单显示了该接口:
1 | public interface AfterReturningAdvice extends Advice { void afterReturning(Object returnValue, Method m, Object[] args, Object target) throws Throwable;} |
After After Returning 建议可以访问返回值(它不能修改),调用的方法,方法的参数和目标。
返回建议后的以下内容将计数所有未引发异常的成功方法调用:
1 | public class CountingAfterReturningAdvice implements AfterReturningAdvice { private int count; public void afterReturning(Object returnValue, Method m, Object[] args, Object target) throws Throwable { ++count; } public int getCount() { return count; }} |
该建议不会更改执行路径。如果抛出异常,则会将其抛出拦截器链,而不是返回值。
Tip
返回后,建议可以与任何切入点一起使用。
Introduction Advice
Spring 将介绍建议视为一种特殊的拦截建议。
简介需要IntroductionAdvisor和IntroductionInterceptor来实现以下接口:
1 | public interface IntroductionInterceptor extends MethodInterceptor { boolean implementsInterface(Class intf);} |
从 AOP Alliance MethodInterceptor接口继承的invoke()方法必须实现介绍。也就是说,如果被调用的方法在引入的接口上,则引入拦截器负责处理方法调用-它不能调用proceed()。
简介建议不能与任何切入点一起使用,因为它仅适用于类,而不适用于方法级别。您只能通过IntroductionAdvisor使用介绍建议,该建议具有以下方法:
1 | public interface IntroductionAdvisor extends Advisor, IntroductionInfo { ClassFilter getClassFilter(); void validateInterfaces() throws IllegalArgumentException;}public interface IntroductionInfo { Class[] getInterfaces();} |
没有MethodMatcher,因此也没有Pointcut与介绍建议相关联。只有类过滤是合乎逻辑的。
getInterfaces()方法返回此顾问程序引入的接口。
在内部使用validateInterfaces()方法来查看引入的接口是否可以由配置的IntroductionInterceptor实现。
考虑一下 Spring 测试套件中的一个示例,并假设我们想为一个或多个对象引入以下接口:
1 | public interface Lockable { void lock(); void unlock(); boolean locked();} |
这说明了混合。我们希望能够将建议对象强制转换为Lockable,无论它们的类型如何,并调用锁定和解锁方法。如果调用lock()方法,则希望所有的 setter 方法都抛出LockedException。因此,我们可以添加一个切面,使对象在不了解对象的情况下不可变:AOP 的一个很好的例子。
首先,我们需要一个IntroductionInterceptor来完成繁重的工作。在这种情况下,我们扩展了org.springframework.aop.support.DelegatingIntroductionInterceptor便利类。我们可以直接实现IntroductionInterceptor,但是在大多数情况下最好使用DelegatingIntroductionInterceptor。
DelegatingIntroductionInterceptor的设计宗旨是将引入的接口委派给引入接口的实际实现,从而隐藏使用拦截的方式。您可以使用构造函数参数将委托设置为任何对象。默认委托(使用无参数构造函数时)为this。因此,在下一个示例中,委托是DelegatingIntroductionInterceptor的LockMixin子类。给定一个委托(默认情况下为自身),DelegatingIntroductionInterceptor实例将查找由委托实现的所有接口(IntroductionInterceptor除外),并支持针对其中任何接口的介绍。诸如LockMixin之类的子类可以调用suppressInterface(Class intf)方法来禁止不应公开的接口。但是,无论IntroductionInterceptor准备支持多少个接口,IntroductionAdvisor使用的控件都会控制实际公开哪些接口。引入的接口隐藏了目标对同一接口的任何实现。
因此,LockMixin扩展了DelegatingIntroductionInterceptor并实现了Lockable本身。超类会自动选择支持Lockable进行介绍,因此我们无需指定。我们可以通过这种方式引入任意数量的接口。
请注意locked实例变量的使用。这有效地将附加状态添加到目标对象中保存的状态。
下面的示例显示示例LockMixin类:
1 | public class LockMixin extends DelegatingIntroductionInterceptor implements Lockable { private boolean locked; public void lock() { this.locked = true; } public void unlock() { this.locked = false; } public boolean locked() { return this.locked; } public Object invoke(MethodInvocation invocation) throws Throwable { if (locked() && invocation.getMethod().getName().indexOf("set") == 0) { throw new LockedException(); } return super.invoke(invocation); }} |
通常,您无需覆盖invoke()方法。通常,DelegatingIntroductionInterceptor实现(如果引入了delegate方法,则调用delegate方法,否则 continue 向连接点前进)。在当前情况下,我们需要添加一个检查:如果处于锁定模式,则不能调用任何 setter 方法。
所需的简介仅需要保存一个不同的LockMixin实例并指定所引入的接口(在这种情况下,只需Lockable)。一个更复杂的示例可能引用了引入拦截器(将被定义为原型)。在这种情况下,没有与LockMixin相关的配置,因此我们使用new创建它。以下示例显示了LockMixinAdvisor类:
1 | public class LockMixinAdvisor extends DefaultIntroductionAdvisor { public LockMixinAdvisor() { super(new LockMixin(), Lockable.class); }} |
我们可以非常简单地应用此顾问程序,因为它不需要配置。 (但是,如果没有IntroductionAdvisor,就不可能使用IntroductionInterceptor.)像通常的介绍一样,顾问程序必须是按实例的,因为它是有状态的。对于每个建议对象,我们需要LockMixinAdvisor的不同实例,因此需要LockMixin。顾问程序包含建议对象状态的一部分。
我们可以像其他任何顾问一样,通过使用Advised.addAdvisor()方法或 XML 配置中的(推荐方式)以编程方式应用此顾问。下面讨论的所有代理创建选择,包括“自动代理创建器”,都可以正确处理介绍和有状态的混合。
6.3. Spring 的 Advisor API
在 Spring 中,顾问程序是一个切面,其中仅包含与切入点表达式关联的单个建议对象。
除了介绍的特殊情况外,任何顾问都可以与任何建议一起使用。 org.springframework.aop.support.DefaultPointcutAdvisor是最常用的顾问类。可以与MethodInterceptor,BeforeAdvice或ThrowsAdvice一起使用。
可以在同一 AOP 代理中的 Spring 中混合使用顾问和建议类型。例如,您可以在一个代理配置中使用围绕建议的拦截,抛出建议以及在建议之前。 Spring 自动创建必要的拦截器链。
6.4. 使用 ProxyFactoryBean 创建 AOP 代理
如果将 Spring IoC 容器(ApplicationContext或BeanFactory)用于业务对象(应该是!),则要使用 Spring 的 AOP FactoryBean实现之一。 (请记住,工厂 bean 引入了一个间接层,允许它创建不同类型的对象.)
Note
Spring AOP 支持还在后台使用了工厂 bean。
在 Spring 中创建 AOP 代理的基本方法是使用org.springframework.aop.framework.ProxyFactoryBean。这样可以完全控制切入点,任何适用的建议及其 Sequences。但是,如果您不需要这样的控制,则有一些更简单的选项是可取的。
6.4.1. Basics
像其他 Spring FactoryBean实现一样,ProxyFactoryBean引入了一个间接级别。如果定义一个名为foo的ProxyFactoryBean,则引用foo的对象看不到ProxyFactoryBean实例本身,而是看到通过ProxyFactoryBean中的getObject()方法的实现创建的对象。此方法创建一个包装目标对象的 AOP 代理。
使用ProxyFactoryBean或另一个 IoC 感知类创建 AOP 代理的最重要好处之一是,IoC 也可以 Management 建议和切入点。这是一项强大的功能,可实现某些其他 AOP 框架难以实现的方法。例如,受益于依赖注入提供的所有可插入性,建议本身可以引用应用程序对象(目标之外,目标应该在任何 AOP 框架中可用)。
6.4.2. JavaBean 属性
与 Spring 提供的大多数FactoryBean实现一样,ProxyFactoryBean类本身就是 JavaBean。其属性用于:
- 指定您要代理的目标。
- 指定是否使用 CGLIB(稍后介绍,另请参见基于 JDK 和 CGLIB 的代理)。
一些关键属性是从org.springframework.aop.framework.ProxyConfig(Spring 中所有 AOP 代理工厂的超类)继承的。这些关键属性包括:
proxyTargetClass:true(如果要代理目标类,而不是目标类的接口)。如果此属性值设置为true,那么将创建 CGLIB 代理(但也请参见基于 JDK 和 CGLIB 的代理)。optimize:控制是否将积极的优化应用于通过 CGLIB 创建的代理。除非您完全了解相关的 AOP 代理如何处理优化,否则不要随意使用此设置。当前仅用于 CGLIB 代理。它对 JDK 动态代理无效。frozen:如果代理配置为frozen,则不再允许更改配置。这是一个轻微的优化,对于在您不希望调用者在创建代理后能够(通过Advised接口)操纵代理的情况下很有用。此属性的默认值为false,因此允许进行更改(例如添加其他建议)。exposeProxy:确定是否应在ThreadLocal中公开当前代理,以便目标可以访问它。如果目标需要获取代理并且exposeProxy属性设置为true,则目标可以使用AopContext.currentProxy()方法。
ProxyFactoryBean特有的其他属性包括:
proxyInterfaces:String接口名称的数组。如果未提供,则使用目标类的 CGLIB 代理(另请参见基于 JDK 和 CGLIB 的代理)。interceptorNames:要应用的Advisor,拦截器或其他建议名称的String数组。Sequences 很重要,先到先得。也就是说,列表中的第一个拦截器是第一个能够拦截调用的拦截器。
名称是当前工厂中的 bean 名称,包括祖先工厂中的 bean 名称。您不能在这里提及 bean 引用,因为这样做会导致ProxyFactoryBean忽略建议的单例设置。
您可以在拦截器名称后加上星号(*)。这样做会导致应用所有顾问 Bean,其名称以要应用星号的部分开头。您可以在使用“全局”顾问中找到使用此功能的示例。
- 单例:无论调用
getObject()方法的频率如何,工厂是否应返回单个对象。几种FactoryBean实现提供了这种方法。默认值为true。如果要使用状态通知(例如,对于状态混合),请使用原型建议以及false的单例值。
6.4.3. 基于 JDK 和 CGLIB 的代理
本部分是有关ProxyFactoryBean如何选择为特定目标对象(将被代理)创建基于 JDK 的代理或基于 CGLIB 的代理的 Authority 性文档。
Note
在 Spring 的 1.2.x 版和 2.0 版之间,ProxyFactoryBean创建基于 JDK 或 CGLIB 的代理的行为发生了变化。 ProxyFactoryBean现在在自动检测接口切面表现出与TransactionProxyFactoryBean类类似的语义。
如果要代理的目标对象的类(以下简称为目标类)没有实现任何接口,则将创建基于 CGLIB 的代理。这是最简单的情况,因为 JDK 代理是基于接口的,并且没有接口意味着甚至无法进行 JDK 代理。您可以插入目标 bean 并通过设置interceptorNames属性来指定拦截器列表。请注意,即使ProxyFactoryBean的proxyTargetClass属性已设置为false,也会创建基于 CGLIB 的代理。 (这样做没有任何意义,最好将其从 Bean 定义中删除,因为它充其量是多余的,并且在最糟的情况下会造成混淆.)
如果目标类实现一个(或多个)接口,则创建的代理类型取决于ProxyFactoryBean的配置。
如果ProxyFactoryBean的proxyTargetClass属性已设置为true,则会创建基于 CGLIB 的代理。这是有道理的,并且符合最小惊讶原则。即使已将ProxyFactoryBean的proxyInterfaces属性设置为一个或多个完全限定的接口名称,但proxyTargetClass属性设置为true的事实也会导致基于 CGLIB 的代理生效。
如果ProxyFactoryBean的proxyInterfaces属性已设置为一个或多个完全限定的接口名称,则将创建基于 JDK 的代理。创建的代理实现proxyInterfaces属性中指定的所有接口。如果目标类恰好实现了比proxyInterfaces属性中指定的接口更多的接口,那就很好了,但是这些附加接口不会由返回的代理实现。
如果尚未设置ProxyFactoryBean的proxyInterfaces属性,但是目标类确实实现了一个(或多个)接口,则ProxyFactoryBean自动检测到目标类实际上确实实现了至少一个接口以及基于 JDK 的代理被构建。实际代理的接口是目标类实现的所有接口。实际上,这与向proxyInterfaces属性提供目标类实现的每个接口的列表相同。但是,它的工作量大大减少,而且不容易出现印刷错误。
6.4.4. 代理接口
考虑一个实际的ProxyFactoryBean的简单示例。此示例涉及:
- 代理的目标 bean。这是示例中的
personTargetbean 定义。 Advisor和Interceptor用于提供建议。- 一个 AOP 代理 bean 定义,用于指定目标对象(
personTargetbean),代理接口以及要应用的建议。
以下清单显示了示例:
1 | <bean id="personTarget" class="com.mycompany.PersonImpl"> <property name="name" value="Tony"/> <property name="age" value="51"/></bean><bean id="myAdvisor" class="com.mycompany.MyAdvisor"> <property name="someProperty" value="Custom string property value"/></bean><bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor"></bean><bean id="person" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="proxyInterfaces" value="com.mycompany.Person"/> <property name="target" ref="personTarget"/> <property name="interceptorNames"> <list> <value>myAdvisor</value> <value>debugInterceptor</value> </list> </property></bean> |
请注意,interceptorNames属性具有String列表,其中包含当前工厂中的拦截器或顾问程序的 Bean 名称。您可以在返回之前,之后使用顾问程序,拦截器并引发建议对象。顾问的 Sequences 很重要。
Note
您可能想知道为什么列表不包含 bean 引用。这样做的原因是,如果ProxyFactoryBean的 singleton 属性设置为false,则它必须能够返回独立的代理实例。如果任何顾问本身就是原型,则需要返回一个独立的实例,因此必须能够从工厂获得原型的实例。保持引用是不够的。
可以使用前面显示的person bean 定义代替Person实现,如下所示:
1 | Person person = (Person) factory.getBean("person"); |
与普通 Java 对象一样,在同一 IoC 上下文中的其他 bean 可以表达对此的强类型依赖性。以下示例显示了如何执行此操作:
1 | <bean id="personUser" class="com.mycompany.PersonUser"> <property name="person"><ref bean="person"/></property></bean> |
在此示例中,PersonUser类公开了Person类型的属性。就其而言,可以透明地使用 AOP 代理代替“真实的”人实现。但是,其类将是动态代理类。可以将其强制转换为Advised接口(稍后讨论)。
您可以使用匿名内部 bean 隐藏目标和代理之间的区别。只有ProxyFactoryBean定义不同。该建议仅出于完整性考虑。以下示例显示如何使用匿名内部 bean:
1 | <bean id="myAdvisor" class="com.mycompany.MyAdvisor"> <property name="someProperty" value="Custom string property value"/></bean><bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor"/><bean id="person" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="proxyInterfaces" value="com.mycompany.Person"/> <!-- Use inner bean, not local reference to target --> <property name="target"> <bean class="com.mycompany.PersonImpl"> <property name="name" value="Tony"/> <property name="age" value="51"/> </bean> </property> <property name="interceptorNames"> <list> <value>myAdvisor</value> <value>debugInterceptor</value> </list> </property></bean> |
使用匿名内部 bean 的优点是只有一个类型为Person的对象。如果我们希望防止应用程序上下文的用户获取对未建议对象的引用,或者需要避免使用 Spring IoC 自动装配的任何歧义,这将非常有用。可以说,还有一个优点是ProxyFactoryBean定义是独立的。但是,有时能够从工厂获得未经建议的目标实际上可能是一个优势(例如,在某些测试方案中)。
6.4.5. 代理类
如果您需要代理一类,而不是一个或多个接口,该怎么办?
想象一下,在我们之前的示例中,没有Person接口。我们需要建议一个名为Person的类,该类未实现任何业务接口。在这种情况下,您可以将 Spring 配置为使用 CGLIB 代理而不是动态代理。为此,请将前面显示的ProxyFactoryBean的proxyTargetClass属性设置为true。尽管最好对接口而不是对类进行编程,但是在处理遗留代码时,建议未实现接口的类的功能可能会很有用。 (通常,Spring 并不是规定性的.虽然可以轻松地应用良好实践,但可以避免强制采用特定方法.)
如果需要,即使您有接口,也可以在任何情况下强制使用 CGLIB。
CGLIB 代理通过在运行时生成目标类的子类来工作。 Spring 配置此生成的子类以将方法调用委托给原始目标。子类用于实现 Decorator 模式,并编织在建议中。
CGLIB 代理通常应对用户透明。但是,有一些问题要考虑:
- 不能建议
Final方法,因为它们不能被覆盖。 - 无需将 CGLIB 添加到您的 Classpath 中。从 Spring 3.2 开始,CGLIB 被重新打包并包含在 spring-core JAR 中。换句话说,基于 CGLIB 的 AOP 就像 JDK 动态代理一样“开箱即用”。
CGLIB 代理和动态代理之间几乎没有性能差异。从 Spring 1.0 开始,动态代理要快一些。但是,将来可能会改变。在这种情况下,性能不应作为决定性的考虑因素。
6.4.6. 使用“全局”顾问
通过在拦截器名称后附加一个星号,所有具有与该星号之前的部分匹配的 Bean 名称的顾问程序都将添加到顾问程序链中。如果您需要添加标准的“全局”顾问程序集,这可能会派上用场。以下示例定义了两个全局顾问程序:
1 | <bean id="proxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="target" ref="service"/> <property name="interceptorNames"> <list> <value>global*</value> </list> </property></bean><bean id="global_debug" class="org.springframework.aop.interceptor.DebugInterceptor"/><bean id="global_performance" class="org.springframework.aop.interceptor.PerformanceMonitorInterceptor"/> |
6.5. 简洁的代理定义
特别是在定义事务代理时,您可能会得到许多类似的代理定义。使用父子 bean 定义和子 bean 定义以及内部 bean 定义可以使代理定义更加简洁明了。
首先,我们为代理创建父模板,bean 定义,如下所示:
1 | <bean id="txProxyTemplate" abstract="true" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"> <property name="transactionManager" ref="transactionManager"/> <property name="transactionAttributes"> <props> <prop key="*">PROPAGATION_REQUIRED</prop> </props> </property></bean> |
它本身从未实例化,因此实际上可能是不完整的。然后,每个需要创建的代理都是一个子 bean 定义,它将代理的目标包装为内部 bean 定义,因为无论如何该目标都不会单独使用。以下示例显示了这样的子 bean:
1 | <bean id="myService" parent="txProxyTemplate"> <property name="target"> <bean class="org.springframework.samples.MyServiceImpl"> </bean> </property></bean> |
您可以从父模板覆盖属性。在以下示例中,我们将覆盖事务传播设置:
1 | <bean id="mySpecialService" parent="txProxyTemplate"> <property name="target"> <bean class="org.springframework.samples.MySpecialServiceImpl"> </bean> </property> <property name="transactionAttributes"> <props> <prop key="get*">PROPAGATION_REQUIRED,readOnly</prop> <prop key="find*">PROPAGATION_REQUIRED,readOnly</prop> <prop key="load*">PROPAGATION_REQUIRED,readOnly</prop> <prop key="store*">PROPAGATION_REQUIRED</prop> </props> </property></bean> |
请注意,在父 bean 的示例中,我们通过将abstract属性设置为true来明确标记父 bean 定义为抽象,如previously所述,因此它实际上可能没有实例化。默认情况下,应用程序上下文(但不是简单的 bean 工厂)会预先实例化所有单例。因此,重要的是(至少对于单例 bean),如果您有一个(父)bean 定义仅打算用作模板,并且此定义指定了一个类,则必须确保将abstract属性设置为true。否则,应用程序上下文实际上会尝试对其进行实例化。
6.6. 使用 ProxyFactory 以编程方式创建 AOP 代理
使用 Spring 以编程方式创建 AOP 代理很容易。这使您可以使用 Spring AOP,而无需依赖 Spring IoC。
由目标对象实现的接口将被自动代理。以下清单显示了使用一个拦截器和一个顾问程序为目标对象创建代理的过程:
1 | ProxyFactory factory = new ProxyFactory(myBusinessInterfaceImpl);factory.addAdvice(myMethodInterceptor);factory.addAdvisor(myAdvisor);MyBusinessInterface tb = (MyBusinessInterface) factory.getProxy(); |
第一步是构造类型为org.springframework.aop.framework.ProxyFactory的对象。您可以使用目标对象创建此对象,如前面的示例中所示,或指定要在备用构造函数中代理的接口。
您可以添加建议(使用拦截器作为一种特殊的建议),建议程序,或同时添加两者,并在ProxyFactory的生命周期内对其进行操作。如果添加IntroductionInterceptionAroundAdvisor,则可以使代理实现其他接口。
ProxyFactory(从AdvisedSupport继承)上还有便捷的方法,可让您添加其他建议类型,例如 before 并引发建议。 AdvisedSupport是ProxyFactory和ProxyFactoryBean的超类。
Tip
在大多数应用程序中,将 AOP 代理创建与 IoC 框架集成在一起是最佳实践。通常,建议您使用 AOP 从 Java 代码外部化配置。
6.7. 操作建议对象
无论创建 AOP 代理,都可以使用org.springframework.aop.framework.Advised界面对其进行操作。任何 AOP 代理都可以强制转换为该接口,无论它实现了哪个其他接口。该界面包括以下方法:
1 | Advisor[] getAdvisors();void addAdvice(Advice advice) throws AopConfigException;void addAdvice(int pos, Advice advice) throws AopConfigException;void addAdvisor(Advisor advisor) throws AopConfigException;void addAdvisor(int pos, Advisor advisor) throws AopConfigException;int indexOf(Advisor advisor);boolean removeAdvisor(Advisor advisor) throws AopConfigException;void removeAdvisor(int index) throws AopConfigException;boolean replaceAdvisor(Advisor a, Advisor b) throws AopConfigException;boolean isFrozen(); |
getAdvisors()方法针对已添加到工厂的每个顾问程序,拦截器或其他建议类型返回Advisor。如果添加了Advisor,则在此索引处返回的顾问程序就是您添加的对象。如果添加了拦截器或其他建议类型,Spring 会将其包装在带有指向始终返回true的切入点的顾问程序中。因此,如果添加了MethodInterceptor,则为此索引返回的顾问程序是DefaultPointcutAdvisor,它返回MethodInterceptor以及与所有类和方法匹配的切入点。
addAdvisor()方法可用于添加任何Advisor。通常,拥有切入点和建议的顾问是通用的DefaultPointcutAdvisor,您可以将其与任何建议或切入点一起使用(但不能用于介绍)。
默认情况下,即使已创建代理,也可以添加或删除顾问程序或拦截器。唯一的限制是不可能添加或删除介绍顾问,因为工厂中的现有代理不会显示界面更改。 (您可以从工厂获取新的代理来避免此问题.)
以下示例显示了将 AOP 代理投射到Advised接口并检查和处理其建议:
1 | Advised advised = (Advised) myObject;Advisor[] advisors = advised.getAdvisors();int oldAdvisorCount = advisors.length;System.out.println(oldAdvisorCount + " advisors");// Add an advice like an interceptor without a pointcut// Will match all proxied methods// Can use for interceptors, before, after returning or throws adviceadvised.addAdvice(new DebugInterceptor());// Add selective advice using a pointcutadvised.addAdvisor(new DefaultPointcutAdvisor(mySpecialPointcut, myAdvice));assertEquals("Added two advisors", oldAdvisorCount + 2, advised.getAdvisors().length); |
Note
尽管无疑存在合法的使用案例,但是否建议(无双关语)修改 Producing 的业务对象的建议值得怀疑。但是,它在开发中(例如在测试中)非常有用。有时我们发现以拦截器或其他建议的形式添加测试代码,并进入我们要测试的方法调用中非常有用。 (例如,建议可以进入为该方法创建的事务内部,也许可以在将事务标记为回滚之前运行 SQL 以检查数据库是否已正确更新.)
根据创建代理的方式,通常可以设置frozen标志。在这种情况下,Advised isFrozen()方法返回true,而通过添加或删除来修改建议的任何尝试都将导致AopConfigException。冻结建议对象状态的功能在某些情况下很有用(例如,防止调用代码删除安全拦截器)。如果已知不需要修改运行时建议,则在 Spring 1.1 中也可以使用它来进行积极的优化。
6.8. 使用“自动代理”功能
到目前为止,我们已经考虑过使用ProxyFactoryBean或类似的工厂 bean 来显式创建 AOP 代理。
Spring 还允许我们使用“自动代理” Bean 定义,该定义可以自动代理选定的 Bean 定义。这是在 Spring 的“ bean 后处理器”基础结构上构建的,该基础结构允许在容器加载时修改任何 bean 定义。
在此模型中,您在 XML bean 定义文件中设置了一些特殊的 bean 定义,以配置自动代理基础结构。这使您可以声明有资格进行自动代理的目标。您无需使用ProxyFactoryBean。
有两种方法可以做到这一点:
- 通过使用在当前上下文中引用特定 bean 的自动代理创建器。
- 自动代理创建的一种特殊情况,值得单独考虑:由源级元数据属性驱动的自动代理创建。
6.8.1. 自动代理 Bean 定义
本节介绍了org.springframework.aop.framework.autoproxy软件包提供的自动代理创建者。
BeanNameAutoProxyCreator
BeanNameAutoProxyCreator类是BeanPostProcessor,它会自动为名称与 Literals 值或通配符匹配的 bean 创建 AOP 代理。以下示例显示了如何创建BeanNameAutoProxyCreator bean:
1 | <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator"> <property name="beanNames" value="jdk*,onlyJdk"/> <property name="interceptorNames"> <list> <value>myInterceptor</value> </list> </property></bean> |
与ProxyFactoryBean一样,有interceptorNames属性而不是拦截器列表,以允许原型顾问程序具有正确的行为。命名为“拦截器”的可以是顾问或任何建议类型。
通常,与自动代理一样,使用BeanNameAutoProxyCreator的要点是将相同的配置一致地应用于多个对象,并且配置量最少。将声明式事务应用于多个对象是一种流行的选择。
名称匹配的 Bean 定义(例如上例中的jdkMyBean和onlyJdk)是带有目标类的普通旧 Bean 定义。 BeanNameAutoProxyCreator自动创建一个 AOP 代理。相同的建议适用于所有匹配的 bean。注意,如果使用了顾问程序(而不是前面示例中的拦截器),则切入点可能会不同地应用于不同的 bean。
DefaultAdvisorAutoProxyCreator
一个更通用,功能更强大的自动代理创建者是DefaultAdvisorAutoProxyCreator。这可以在当前上下文中自动应用合格的顾问程序,而无需在自动代理顾问程序的 Bean 定义中包括特定的 Bean 名称。它具有与BeanNameAutoProxyCreator相同的一致配置和避免重复的优点。
使用此机制涉及:
- 指定
DefaultAdvisorAutoProxyCreatorbean 定义。 - 在相同或相关的上下文中指定任意数量的顾问。请注意,这些必须是顾问,而不是拦截器或其他建议。这是必要的,因为必须有一个评估的切入点,以检查每个建议是否符合候选 bean 定义。
DefaultAdvisorAutoProxyCreator自动评估每个顾问中包含的切入点,以查看它应应用于每个业务对象的建议(如果有)(例如示例中的businessObject1和businessObject2)。
这意味着可以将任意数量的顾问程序自动应用于每个业务对象。如果在任何顾问程序中没有切入点与业务对象中的任何方法匹配,则该对象不会被代理。当为新的业务对象添加 Bean 定义时,如有必要,它们会自动被代理。
通常,自动代理的优点是使调用者或依赖者无法获得不建议的对象。在此ApplicationContext上调用getBean("businessObject1")将返回 AOP 代理,而不是目标业务对象。 (前面显示的“ inner bean”惯用语也提供了这一好处.)
以下示例创建一个DefaultAdvisorAutoProxyCreator bean 和本节中讨论的其他元素:
1 | <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/><bean class="org.springframework.transaction.interceptor.TransactionAttributeSourceAdvisor"> <property name="transactionInterceptor" ref="transactionInterceptor"/></bean><bean id="customAdvisor" class="com.mycompany.MyAdvisor"/><bean id="businessObject1" class="com.mycompany.BusinessObject1"> <!-- Properties omitted --></bean><bean id="businessObject2" class="com.mycompany.BusinessObject2"/> |
如果要将相同的建议一致地应用于许多业务对象,则DefaultAdvisorAutoProxyCreator非常有用。基础结构定义到位后,您可以添加新的业务对象,而无需包括特定的代理配置。您还可以轻松地添加其他切面(例如,跟踪或性能监视切面),而对配置的更改最少。
DefaultAdvisorAutoProxyCreator支持过滤(通过使用命名约定,以便仅评估某些顾问程序,从而允许在同一工厂中使用多个不同配置的 AdvisorAutoProxyCreators)和排序。如果存在问题,顾问可以实现org.springframework.core.Ordered接口以确保正确的排序。前面示例中使用的TransactionAttributeSourceAdvisor具有可配置的 Sequences 值。默认设置为无序。
6.9. 使用 TargetSource 实现
Spring 提供了org.springframework.aop.TargetSource接口中表示的TargetSource的概念。该接口负责返回实现连接点的“目标对象”。每次 AOP 代理处理方法调用时,都会向TargetSource实现请求目标实例。
使用 Spring AOP 的开发人员通常不需要直接使用TargetSource实现,但这提供了支持池化,热插拔和其他复杂目标的强大方法。例如,池TargetSource可以通过使用池来 Management 实例,从而为每次调用返回不同的目标实例。
如果未指定TargetSource,则使用默认实现包装本地对象。每次调用都会返回相同的目标(与您期望的一样)。
本节的其余部分描述了 Spring 随附的标准目标源以及如何使用它们。
Tip
使用自定义目标源时,目标通常需要是原型而不是单例 bean 定义。这样,Spring 可以在需要时创建一个新的目标实例。
6.9.1. 可热交换的目标源
org.springframework.aop.target.HotSwappableTargetSource的存在是为了允许 AOP 代理服务器的目标切换,同时允许呼叫者保留对其的引用。
更改目标源的目标会立即生效。 HotSwappableTargetSource是线程安全的。
您可以使用 HotSwappableTargetSource 上的swap()方法更改目标,如以下示例所示:
1 | HotSwappableTargetSource swapper = (HotSwappableTargetSource) beanFactory.getBean("swapper"); |
以下示例显示了必需的 XML 定义:
1 | <bean id="initialTarget" class="mycompany.OldTarget"/> |
前面的swap()调用更改了可交换 bean 的目标。拥有对该 bean 的引用的 Client 端不知道更改,但立即开始达到新目标。
尽管此示例未添加任何建议(使用TargetSource无需添加建议),但任何TargetSource均可与任意建议结合使用。
6.9.2. 汇集目标源
使用池目标源提供了与 Stateless 会话 EJB 相似的编程模型,在 Stateless 会话 EJB 中,维护了相同实例的池,方法调用将释放池中的对象。
Spring 池和 SLSB 池之间的关键区别在于,Spring 池可以应用于任何 POJO。通常,与 Spring 一样,可以以非侵入性方式应用此服务。
Spring 提供了对 Commons Pool 2.2 的支持,该池提供了相当有效的池实现。您需要在应用程序的 Classpath 上使用commons-pool Jar 才能使用此功能。您还可以将org.springframework.aop.target.AbstractPoolingTargetSource子类化以支持任何其他池化 API。
Note
还支持 Commons Pool 1.5,但从 Spring Framework 4.2 开始不推荐使用。
以下清单显示了一个示例配置:
1 | <bean id="businessObjectTarget" class="com.mycompany.MyBusinessObject" scope="prototype"> ... properties omitted</bean><bean id="poolTargetSource" class="org.springframework.aop.target.CommonsPool2TargetSource"> <property name="targetBeanName" value="businessObjectTarget"/> <property name="maxSize" value="25"/></bean><bean id="businessObject" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="targetSource" ref="poolTargetSource"/> <property name="interceptorNames" value="myInterceptor"/></bean> |
请注意,目标对象(上例中为businessObjectTarget)必须是原型。这使PoolingTargetSource实现可以创建目标的新实例以根据需要扩展池。有关其属性的信息,请参见AbstractPoolingTargetSource 的 javadoc和您希望使用的具体子类。 maxSize是最基本的,并且始终保证存在。
在这种情况下,myInterceptor是需要在同一 IoC 上下文中定义的拦截器的名称。但是,您无需指定拦截器即可使用池。如果只希望池化而没有其他建议,则根本不要设置interceptorNames属性。
您可以将 Spring 配置为能够将任何池化对象投射到org.springframework.aop.target.PoolingConfig接口,该接口通过介绍来公开有关池的配置和当前大小的信息。您需要定义类似于以下内容的顾问程序:
1 | <bean id="poolConfigAdvisor" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean"> <property name="targetObject" ref="poolTargetSource"/> <property name="targetMethod" value="getPoolingConfigMixin"/></bean> |
通过在AbstractPoolingTargetSource类上调用便捷方法(因此使用MethodInvokingFactoryBean)可获得此顾问程序。该顾问的名称(此处为poolConfigAdvisor)必须位于公开池对象的ProxyFactoryBean中的拦截器名称列表中。
演员表的定义如下:
1 | PoolingConfig conf = (PoolingConfig) beanFactory.getBean("businessObject");System.out.println("Max pool size is " + conf.getMaxSize()); |
Note
通常不需要合并 Stateless 服务对象。我们不认为它应该是默认选择,因为大多数 Stateless 对象自然是线程安全的,并且如果缓存了资源,实例池会成问题。
通过使用自动代理,可以简化池。您可以设置任何自动代理创建者使用的TargetSource实现。
6.9.3. 原型目标源
设置“原型”目标源类似于设置池TargetSource。在这种情况下,每次方法调用都会创建目标的新实例。尽管在现代 JVM 中创建新对象的成本并不高,但是连接新对象(满足其 IoC 依赖关系)的成本可能会更高。因此,没有充分的理由就不应使用此方法。
为此,您可以按如下所示修改poolTargetSource定义(为清楚起见,我们也更改了名称):
1 | <bean id="prototypeTargetSource" class="org.springframework.aop.target.PrototypeTargetSource"> <property name="targetBeanName" ref="businessObjectTarget"/></bean> |
唯一的属性是目标 Bean 的名称。 TargetSource实现中使用继承来确保命名的一致性。与池化目标源一样,目标 Bean 必须是原型 Bean 定义。
6.9.4. ThreadLocal 目标源
如果您需要为每个传入请求(每个线程)创建一个对象,则ThreadLocal目标源很有用。 ThreadLocal的概念提供了 JDK 范围的功能,可以透明地将资源与线程一起存储。设置ThreadLocalTargetSource与其他类型的目标源所说明的几乎相同,如以下示例所示:
1 | <bean id="threadlocalTargetSource" class="org.springframework.aop.target.ThreadLocalTargetSource"> <property name="targetBeanName" value="businessObjectTarget"/></bean> |
Note
ThreadLocal实例在多线程和多类加载器环境中使用不正确时会遇到严重问题(可能导致内存泄漏)。您应该始终考虑将 threadlocal 包装在其他一些类中,并且切勿直接使用ThreadLocal本身(包装类中除外)。另外,您应始终记住正确设置和取消设置线程本地资源(在后者中仅涉及对ThreadLocal.set(null)的调用)。在任何情况下都应进行取消设置,因为不取消设置可能会导致出现问题。 Spring 的ThreadLocal支持为您做到了这一点,应该始终考虑使用ThreadLocal实例,而无需其他适当的处理代码。
6.10. 定义新的建议类型
Spring AOP 被设计为可扩展的。尽管目前在内部使用拦截实现策略,但是除了在建议周围,在建议之前,抛出建议和返回建议之后进行拦截之外,还可以支持任意建议类型。
org.springframework.aop.framework.adapter软件包是 SPI 软件包,可在不更改核心框架的情况下添加对新的自定义建议类型的支持。自定义Advice类型的唯一限制是它必须实现org.aopalliance.aop.Advice标记接口。
有关更多信息,请参见org.springframework.aop.framework.adapter javadoc。