5. 面向切面编程
5.1. 介绍
面向切面编程(AOP)通过提供另一种思考程序结构的方式来补充面向对象编程(OOP)。 OOP中模块化的关键单元是类,而AOP中模块化的单元是切面。 Aspects可以使关注的模块化,例如跨越多种类型和对象的事务管理。 (这种担忧在AOP文献中常常被称为横切关注点。)
AOP框架是Spring的关键组件之一。 虽然Spring IoC容器不依赖于AOP,也就是说,如果您不想使用AOP,那么AOP就是Spring IoC的补充,可以提供非常强大的中间件解决方案。
Spring 2.0+ AOP
Spring 2.0引入了使用基于模式的方法或@AspectJ注释风格来编写自定义方面的更简单和更强大的方法。 这两种风格都提供完全类型的建议和使用AspectJ切入点语言,同时仍然使用Spring AOP进行编织。
本章将讨论基于Spring 2.0+架构和@AspectJ的AOP支持。 下一章将讨论Spring 1.2应用程序中通常公开的较低级别的AOP支持。
AOP被用在Spring框架中
- ...提供声明式企业级服务,特别是作为EJB声明式服务的替代品。 这种服务最重要的是声明式事务管理。
- ...允许用户实现自定义切面,补充使用AOP的OOP。
如果您只对通用声明式服务或其他预先打包的声明式中间件服务(例如池)感兴趣,则不需要直接使用Spring AOP,而可以跳过本章的大部分内容。
5.1.1. AOP 概念
让我们从定义一些中心的AOP概念和术语开始。 这些术语不是特定于Spring的...不幸的是,AOP术语并不是特别直观, 然而,如果Spring使用自己的术语,那将更加令人困惑。
- Aspect:横切多个类的问题的模块化。 事务管理是企业级Java应用程序中横切关注的一个很好的例子。 在Spring AOP中,切面是使用常规类(基于模式的方法)或使用@Aspect注释(@AspectJ风格)注释的常规类来实现的。
- Join point:程序执行期间的一个点,例如执行方法或处理异常。 在Spring AOP中,连接点总是代表一个方法的执行。
- Advice:在特定连接点处采取的行动。 不同类型的建议包括“around”,“before”和“after”的Advice。 (建议类型将在下面讨论。)许多AOP框架(包括Spring)都将建议建模为拦截器,在连接点周围维护一系列拦截器。
Pointcut: 一个匹配连接点的谓词。 建议与切入点表达式相关联,并在切入点匹配的任何连接点(例如,执行具有特定名称的方法)上运行。 与切入点表达式匹配的连接点的概念是AOP的核心,Spring默认使用AspectJ切入点表达式语言。
Introduction:代表类型声明其他方法或字段。 Spring AOP允许您向任何建议的对象引入新的接口(和相应的实现)。 例如,您可以使用简介来使bean实现一个IsModified接口,以简化缓存。 (在AspectJ社区中,介绍被称为“类型间声明”。)
Target object:被一个或多个方面建议的对象。 也被称为建议对象。 由于Spring AOP是使用运行时代理实现的,因此该对象将始终是代理对象。
AOP代理:由AOP框架创建的对象,用于实现方面合约(建议方法执行等)。 在Spring框架中,AOP代理将是JDK动态代理或CGLIB代理。
Weaving:将方面与其他应用程序类型或对象链接以创建建议的对象。 这可以在编译时(例如使用AspectJ编译器),加载时间或运行时完成。 像其他纯Java AOP框架一样,Spring AOP在运行时执行织入。
advice的类型:
Before advice: 在连接点之前执行,但无法阻止执行流程继续到连接点(除非抛出异常)的Advice。
After returning advice: 连接点正常完成后要执行的Advice:例如,如果方法返回而不抛出异常。
After throwing advice: 如果方法通过抛出异常退出,则要执行的Advice。
After (finally) advice: 无论加入点退出的方式(正常或异常退回),要执行的Advice。
Around advice: 围绕连接点(如方法调用)的 Advice。 这是最强有力的 Advice。 周围的Advice可以在方法调用之前和之后执行自定义行为。 它还负责选择是继续加入连接点还是通过返回自己的返回值或引发异常来快速Advice的方法执行。
围绕建议是最普遍的Advice。 由于像AOP这样的Spring AOP提供了一整套的Advice类型,我们建议您使用能够实现所需行为的最不强大的Advice类型。 例如,如果只需要用方法的返回值来更新缓存,则最好是实现一个返回后的通知而不是一个around通知,尽管around通知可以完成同样的事情。 使用最具体的建议类型提供了一个更简单的编程模型,而且错误的可能性更小。 例如,你不需要调用JoinPoint中用于around建议的proceed()方法,因此不能不调用它。
在Spring 2.0中,所有的通知(advice)参数都是静态类型的,所以你可以使用适当类型的通知参数(例如来自方法执行的返回值的类型)而不是对象数组。
连接点的概念,与切入点相匹配,是AOP的关键,区别于仅提供拦截的旧技术。 切入点使建议可以独立于面向对象的层次结构进行设定。 例如,提供声明式事务管理的around通知可以应用于跨多个对象的一组方法(例如服务层中的所有业务操作)。
5.1.2. Spring AOP的功能和目标
Spring AOP是用纯Java实现的。 不需要特殊的编译过程。 Spring AOP不需要控制类加载器层次结构,因此适用于Servlet容器或应用程序服务器。
Spring AOP目前仅支持方法执行连接点(建议Spring bean上的方法的执行)。 虽然可以在不破坏核心Spring AOP API的情况下添加对域拦截的支持,但不会实现域拦截。 如果您需要建议字段访问和更新连接点,请考虑使用诸如AspectJ之类的语言。
Spring AOP的AOP方法与其他大多数AOP框架不同。 目标不是提供最完整的AOP实现(尽管Spring AOP是相当有能力的); 而是提供AOP实现和Spring IoC之间的紧密集成,以帮助解决企业应用程序中的常见问题。
因此,例如,Spring框架的AOP功能通常与Spring IoC容器一起使用。 方面使用正常的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 AOP API的讨论,请参阅以下章节。
Spring框架的核心原则之一是非侵入性; 这是你不应该被迫在你的业务/领域模型中引入框架特定的类和接口的想法。 然而,在一些地方,Spring框架的确给你选择了将Spring框架特定的依赖项引入到你的代码库中:给你这样的选项的理由是因为在某些情况下,阅读或者编码某些特定的 功能就这样。 Spring框架(几乎)总是为您提供选择:您可以自由决定哪个选项最适合您的特定用例或场景。
与本章相关的一个选择是AOP框架(以及AOP风格)的选择。 您可以选择AspectJ和/或Spring AOP,也可以选择@AspectJ注释样式方法或Spring XML配置样式方法。 本章首先选择引入@AspectJ风格的方法不应该被认为是Spring组织支持Spring XML配置风格的@AspectJ注解风格的方法。
请参阅选择使用哪种AOP声明样式来更全面地讨论每种样式的原因和原因。
5.1.3. AOP 代理
Spring AOP默认使用AOP代理的标准JDK动态代理。 这使得任何接口(或一组接口)都可以被代理。
Spring AOP也可以使用CGLIB代理。 这是代理类而不是接口的必要条件。 如果业务对象没有实现接口,则默认使用CGLIB。 编程接口而不是类是个好习惯; 业务类通常会实现一个或多个业务接口。 可以强制使用CGLIB,在那些需要通知未在接口中声明的方法的情况下(希望很少),或者需要将代理对象作为具体类型传递给方法的情况。
掌握Spring AOP是基于代理的事实是很重要的。 请参阅了解AOP代理,以彻底检查此实现细节的实际含义。
5.2. @AspectJ 支持
@AspectJ是指将方面声明为常规Java类的注释类型。 AspectJ项目引入了@AspectJ风格,作为AspectJ 5版本的一部分。 Spring使用AspectJ提供的用于切入点解析和匹配的库来解释与AspectJ 5相同的注释。 AOP运行时仍然是纯粹的Spring AOP,并且不依赖于AspectJ编译器或编织器。
使用AspectJ编译器和编织器可以使用完整的AspectJ语言,在Spring应用程序中使用AspectJ进行了讨论。
5.2.1. 启用 @AspectJ Support
要在Spring配置中使用@AspectJ方面,您需要启用Spring支持,以基于@AspectJ方面配置Spring AOP,并根据这些方面是否建议自动对bean进行自动代理。 通过autoproxying我们的意思是,如果Spring确定一个或多个方面的bean被建议,它会自动生成一个代理来拦截方法调用,并确保在需要时执行通知。
@AspectJ支持可以使用XML或Java风格的配置来启用。 无论哪种情况,您还需要确保AspectJ的aspectjweaver.jar库位于应用程序的类路径(1.6.8或更高版本)上。 这个库可以在AspectJ发行版的'lib'目录下或者通过Maven Central版本库获得。
基于JAVA配置启用 @AspectJ 支持
要使用Java @Configuration启用@AspectJ支持,请添加@EnableAspectJAutoProxy注释:
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
基于XML配置启用 @AspectJ 支持
要使用基于XML的配置启用@AspectJ支持,请使用aop:aspectj-autoproxy元素:
<aop:aspectj-autoproxy/>
这假定您正在使用如基于XML模式的配置中所述的模式支持。 请参阅AOP架构以了解如何在aop名称空间中导入标记。
5.2.2. 声明一个切面
在启用@AspectJ支持的情况下,在您的应用程序上下文中定义的任何具有@AspectJ方面的类(具有@Aspect注释)的bean将被Spring自动检测到,并用于配置Spring AOP。 以下示例显示了一个不太有用的方面所需的最小定义:
应用程序上下文中的常规bean定义,指向具有@Aspect注释的bean类:
<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
<!-- configure properties of aspect here as normal -->
</bean>
和NotVeryUsefulAspect类定义,用org.aspectj.lang.annotation.Aspect注解进行注释;
package org.xyz;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class NotVeryUsefulAspect {
}
切面(用@Aspect注解的类)可能具有与其他类相似的方法和字段。 它们也可能包含切入点,建议和介绍(类型间)声明。
通过组件扫描自动检测方面
您可以在Spring XML配置中将方面类注册为常规bean,或者像通过其他Spring管理的bean一样通过类路径扫描来自动检测它们。 但是,请注意,@Aspect注释对于类路径中的自动检测是不够的:为此,您需要添加一个单独的@Component注释(或者按照Spring组件扫描器的规则,添加一个自定义构造型注释)。
就其他方面(aspect)提供建议?
在Spring AOP中,不可能将方面本身作为其他方面的advice的目标。 类上的@Aspect注解将其标记为一个方面,因此将其从自动代理中排除。
5.2.3. 声明 a pointcut
回想一下,切入点决定了感兴趣的连接点,从而使我们能够控制何时执行建议。 Spring AOP只支持Spring bean的方法执行连接点,所以你可以把一个切入点视为匹配Spring bean上方法的执行。 切入点声明包含两部分:包含名称和任何参数的签名,以及确切地确定我们感兴趣的方法执行的切入点表达式。在AOP的@AspectJ注释样式中,切入点签名由常规方法提供 定义,并且使用@Pointcut注释来指示切入点表达式(用作切入点签名的方法必须具有void返回类型)。
一个例子将有助于区分切入点签名和切入点表达式之间的区别。 以下示例定义了一个名为'anyOldTransfer'的切入点,该切入点将匹配任何名为'transfer'的方法的执行:
@Pointcut("execution(* transfer(..))")// the pointcut expression
private void anyOldTransfer() {}// the pointcut signature
构成@Pointcut注释值的切入点表达式是一个常规的AspectJ 5切入点表达式。 有关AspectJ的切入点语言的完整讨论,请参阅“AspectJ编程指南”(以及用于扩展的AspectJ 5开发人员笔记)或AspectJ的其中一本书,例如Colyer等人的“Eclipse AspectJ”。人。 或Ramnivas Laddad的“AspectJ in Action”。
支持的切入点指示符
Spring AOP支持在切入点表达式中使用以下AspectJ切入点指示符(PCD):
其它 pointcut 类型
完整的AspectJ切入点语言支持Spring中不支持的其他切入点指示符。 这些是:call,get,set,preinitialization,staticinitialization,initialization,handler,adviceexecution,withincode,cflow,cflowbelow,if,@ this和@withincode。 在由Spring AOP解释的切入点表达式中使用这些切入点指示符将导致抛出IllegalArgumentException。
Spring AOP支持的一系列切入点指示符可以在将来的版本中扩展,以支持更多的AspectJ切入点指示符。
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 - 限制匹配连接点的连接点(在AOP中执行的方法)有给定的注释
由于Spring AOP将匹配仅限于方法执行连接点,所以上面的切入点指示符的讨论给出了比在AspectJ编程指南中找到的更窄的定义。 另外,AspectJ本身具有基于类型的语义,并且在执行连接点上,this和target都指向同一个对象 - 执行方法的对象。 Spring AOP是一个基于代理的系统,区分代理对象本身(绑定到此)和代理(绑定到目标)后面的目标对象。
由于Spring的AOP框架的基于代理的性质,目标对象内的调用根本没有被截取。 对于JDK代理,只有代理上的公共接口方法调用才能被拦截。 使用CGLIB,代理上的public和protected方法调用将被拦截,如果需要的话,甚至包package-visible方法。 但是,通过代理的常见交互应始终通过公共签名进行设计。
请注意,切入点定义通常与任何截取的方法相匹配。 如果一个切入点严格意味着仅仅是公开的,即使在通过代理进行潜在的非公共交互的CGLIB代理场景中,也需要相应地定义切入点。
如果拦截需求包含方法调用,甚至包含目标类中的构造函数,请考虑使用Spring驱动的本机AspectJ编织,而不是Spring的基于代理的AOP框架。 这构成了不同特征的AOP使用方式,所以在做出决定之前一定要先熟悉编织。
Spring AOP还支持额外的PCD命名bean。 这个PCD允许你限制连接点到一个特定的Spring bean的匹配,或者一组命名的Spring bean(当使用通配符时)。 bean PCD有以下形式:
bean(idOrNameOfBean)
idOrNameOfBean标记可以是任何Spring bean的名称:提供了使用*字符的有限通配符支持,所以如果你为Spring bean建立了一些命名约定,你可以很容易地写一个bean PCD表达式来挑选它们。 与其他切入点指示符的情况一样,bean PCD可以 &&'ed, ||'ed, 和 ! (否定)。
请注意,bean PCD仅在Spring AOP中受支持,而不是在AspectJ本地编织中支持。 这是AspectJ定义的标准PCD的Spring特定扩展,因此不适用于@Aspect模型中声明的方面。
bean PCD在实例级别运行(基于Spring bean名称概念构建),而不是仅在类型级别(这是基于编织的AOP所限制的)。 基于实例的切入点指示符是Spring基于代理的AOP框架的一个特殊功能,它与Spring bean工厂紧密集成,通过名称识别特定的bean是很自然和直接的。
结合切入点表达式
切入点表达式可以使用'&&','||' 和“!”。 也可以通过名称来引用切入点表达式。 以下示例显示三个切入点表达式:anyPublicOperation(如果方法执行连接点表示任何公共方法的执行,则匹配); inTrading(与交易模块中的方法执行相匹配)和tradingOperation(如果方法执行代表交易模块中的任何公共方法,则匹配)。
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}
@Pointcut("within(com.xyz.someapp.trading..*)")
private void inTrading() {}
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}
如上所示,使用较小的命名组件构建更复杂的切入点表达式是一种最佳做法。 当按名称引用切入点时,将应用普通的Java可见性规则(您可以看到相同类型的私有切入点,层次结构中受保护的切入点,任何位置的公共切入点等)。 可见性不影响切入点匹配。
共享通用的切入点定义
在使用企业级应用程序时,您经常要从几个方面参考应用程序的模块和特定的一组操作。 我们建议定义一个“SystemArchitecture”方面来捕获常见的切入点表达式。 典型的这个方面看起来如下:
package com.xyz.someapp;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class SystemArchitecture {
/**
* A join point is in the web layer if the method is defined
* in a type in the com.xyz.someapp.web package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.web..*)")
public void inWebLayer() {}
/**
* A join point is in the service layer if the method is defined
* in a type in the com.xyz.someapp.service package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.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.someapp.dao package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.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.someapp.abc.service and com.xyz.someapp.def.service) then
* the pointcut expression "execution(* com.xyz.someapp..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.someapp..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.someapp.dao.*.*(..))")
public void dataAccessOperation() {}
}
在这方面定义的切入点可以在任何需要切入点表达式的地方引用。 例如,要使服务层事务化,您可以编写:
<aop:config>
<aop:advisor
pointcut="com.xyz.someapp.SystemArchitecture.businessService()"
advice-ref="tx-advice"/>
</aop:config>
<tx:advice id="tx-advice">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
在Schema-based的AOP支持中讨论<aop:config>和<aop:advisor>元素。 事务管理中讨论了事务元素。.
例子
Spring AOP用户可能最常使用执行切入点指示符。 执行表达式的格式是:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)
除了返回类型模式(上面代码段中的ret-type-pattern),名称模式和参数模式以外的所有部分都是可选的。返回类型模式决定了该方法的返回类型必须是什么,才能使连接点匹配。大多数情况下,您将使用*作为返回类型,它匹配任何返回类型。只有当方法返回给定类型时,完全限定类型名称才会匹配。名称模式匹配方法名称。您可以使用*通配符作为名称模式的全部或部分。如果指定一个声明类型模式,则包含一个尾随。将其加入名称模式组件。参数模式稍微复杂一点:()匹配不带参数的方法,而(..)匹配任意数量的参数(零个或多个)。模式(*)匹配任何类型的一个参数的方法,(*,String)匹配一个方法采取两个参数,第一个可以是任何类型,第二个必须是一个字符串。有关更多信息,请参阅“AspectJ编程指南”的“语言语义”部分。
常见切入点表达式的一些例子如下所示。
- 执行任何公共方法:
execution(public * *(..))
- 以“set”开头的任何方法的执行:
execution(* set*(..))
- 由AccountService接口定义的任何方法的执行:
execution(* com.xyz.service.AccountService.*(..))
- 执行服务包中定义的任何方法:
execution(* com.xyz.service.*.*(..))
- 执行服务包或子包中定义的任何方法:
execution(* com.xyz.service..*.*(..))
服务包中的任何连接点(只在Spring AOP中执行的方法):
within(com.xyz.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”更常用于绑定形式: - 请参阅以下关于如何在通知主体中使方法参数可用的建议。
请注意,此示例中给出的切入点与执行不同(* *(java.io.Serializable)):如果在运行时传递的参数是Serializable,则args版本会匹配,如果方法签名声明单个参数 类型可序列化。
- 目标对象具有@Transactional注解的任何连接点(只在Spring AOP中执行的方法):
@target(org.springframework.transaction.annotation.Transactional)
'@target'也可以以绑定形式使用: - 关于如何使通知体中的注释对象可用,请参阅以下部分。
- 任何连接点(仅在Spring AOP中执行的方法),其中目标对象的声明类型具有@Transactional注释:
@within(org.springframework.transaction.annotation.Transactional)
'@within'也可用于绑定形式: - 关于如何使通知体中的注释对象可用,请参阅以下部分。
- 任何连接点(只在Spring AOP中执行的方法)执行方法有一个@Transactional注解的地方:
@annotation(org.springframework.transaction.annotation.Transactional)
'@annotation'也可以以绑定形式使用: - 关于如何使通知体中的注释对象可用,请参阅以下部分。
- 任何连接点(只在Spring AOP中执行的方法)接受一个参数,并且传递的参数的运行时类型具有@Classified注解:
@args(com.xyz.security.Classified)
'@args'也可以以绑定形式使用: - 关于如何使通知体中的注释对象可用,请参阅以下部分。
- 在名为tradeService的Spring bean上的任何连接点(只在Spring AOP中执行的方法):
bean(tradeService)
- Spring Bean上的任何连接点(只在Spring AOP中执行的方法),其名称与通配符表达式匹配* Service:
bean(*Service)
写好切入点
在编译期间,AspectJ会处理切入点以试图优化匹配性能。 检查代码并确定每个连接点是否匹配(静态或动态)给定的切入点是一个代价高昂的过程。 (动态匹配意味着无法从静态分析完全确定匹配,并且将在代码中放置测试以确定代码运行时是否存在实际匹配)。 在第一次遇到切入点声明时,AspectJ会将其重写为匹配过程的最佳形式。 这是什么意思? 基本上,切入点被重写为DNF(析取范式),并且切入点的组件被排序,以便首先检查那些评估更便宜的组件。 这意味着您不必担心理解各种切入点指示符的性能,并且可能会在切入点声明中以任何顺序提供它们。
然而,AspectJ只能使用它所说的内容,为了获得最佳的匹配性能,您应该考虑他们正在努力实现的目标,并尽可能地缩小匹配的搜索空间。 现有的指定者自然地分为三类:kindded,scoping和context:
- kinded 指示符是那些选择特定类型的连接点。 例如:execution, get, set, call, handler
- scoping指示符是指选择一组感兴趣的连接点(可能是多种类型)的指定者。 例如:within, withincode
- 上下文指示符是基于上下文匹配(并且可选地绑定)的那些指示符。 例如:this,target,@annotation
一个写得好的切入点应至少包括前两种类型(kinded和scoping),如果希望基于连接点上下文进行匹配,则可以包含上下文标识符,或者将该上下文绑定以用于建议。 只提供一个指定的指示符或仅指定一个上下文指示符将会起作用,但是会由于所有额外的处理和分析而影响编织性能(使用的时间和内存)。 范围标识符的匹配速度非常快,而且它们的使用方式意味着AspectJ可以很快地解除不应该进一步处理的连接点组 - 这就是为什么一个好的切入点应该总是包含一个
5.2.4. 声明 advice
建议与切入点表达式相关联,并在切入点匹配的方法执行之前,之后或周围运行。 切入点表达式可以是对命名切入点的简单引用,也可以是就地声明的切入点表达式。
Before advice
前置通知是在一个切面中使用@Before 注解
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
}
如果使用就地切入点表达式,我们可以将上面的例子重写为:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("execution(* com.xyz.myapp.dao.*.*(..))")
public void doAccessCheck() {
// ...
}
}
After returning advice
After returning advice当匹配的方法执行正常返回时运行。 它使用@AfterReturning注释声明:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
}
注意:当然也可以有多个建议声明和其他成员,都在同一个方面。 我们只是在这些例子中展示一个建议声明,以关注当时正在讨论的问题。
有时您需要在advice主体中访问返回的实际值。 你可以使用@AfterReturning的形式来绑定这个返回值:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
returning="retVal")
public void doAccessCheck(Object retVal) {
// ...
}
}
返回属性中使用的名称必须对应于通知方法中参数的名称。 当方法执行返回时,返回值将作为相应的参数值传递给通知方法。 返回子句还将匹配限制为仅返回指定类型的值的方法执行(在这种情况下,将与任何返回值相匹配的对象)。
请注意,使用after-returning的Advice时,不可能返回完全不同的参考。
After throwing advice
抛出通知后,通过抛出异常退出匹配的方法执行。 它使用@AfterThrowing注释来声明
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doRecoveryActions() {
// ...
}
}
通常,只有在抛出给定类型的异常时,才需要运行建议才能运行,而且还经常需要访问建议主体中抛出的异常。 使用throwing属性来限制匹配(如果需要,则使用Throwable作为异常类型,否则)将抛出的异常绑定到通知参数。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
throwing="ex")
public void doRecoveryActions(DataAccessException ex) {
// ...
}
}
throwing属性中使用的名称必须与通知方法中参数的名称相对应。 当一个方法执行通过抛出一个异常退出时,异常将作为相应的参数值传递给通知方法。 抛出子句也将匹配限制在只抛出指定类型的异常的方法执行(在这种情况下为DataAccessException)。
After (finally) advice
(After (finally) advice ,匹配的方法执行退出后运行。 它使用@After注释声明。 建议必须准备好处理正常和异常返回条件。 它通常用于释放资源等
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;
@Aspect
public class AfterFinallyExample {
@After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doReleaseLock() {
// ...
}
}
Around advice
最后的通知是围绕通知。 围绕通知运行匹配的方法执行。 它有机会在方法执行之前和之后执行工作,并确定方法实际上何时,如何甚至是如何执行。 如果您需要以线程安全的方式(例如启动和停止计时器)在方法执行之前和之后共享状态,则通常会使用“围绕”通知。 总是使用符合您要求的最不强大的通知形式(即在通知之前不要使用通知)。
环绕通知是使用@Around注释来声明的。 通知方法的第一个参数必须是ProceedingJoinPoint类型。 在通知的主体中,在ProceedingJoinPoint上调用proceed()会导致底层方法执行。 proceed方法也可以被称为传递Object [] - 数组中的值将在继续时用作方法执行的参数。
使用Object []调用时的继续行为与AspectJ编译器编译的around处理的行为稍有不同。对于使用传统AspectJ语言编写的环绕通知,传递给进行处理的参数数量必须与传递给around通知的参数数量(而不是基础连接点采用的参数数量)相匹配,并将传递的值传递给给定的参数位置取代了该值绑定到的实体的连接点的原始值(如果现在没有意义的话,请不要担心)。 Spring采用的方法更简单,与其基于代理的只执行语义更匹配。如果您正在编译针对Spring编写的@AspectJ方面,并使用AspectJ编译器和编织器进行参数处理,则只需要了解这种差异。有一种方法可以编写与Spring AOP和AspectJ 100%兼容的方面,这在下面的通知参数部分讨论。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
@Aspect
public class AroundExample {
@Around("com.xyz.myapp.SystemArchitecture.businessService()")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
}
around通知返回的值将是方法调用者看到的返回值。 例如,一个简单的缓存方面可以从一个缓存中返回一个值,如果它没有,则调用proceed()。 请注意,可能会一次,多次或根本没有在周围建议的内容中引用,所有这些都是非常合法的。
Advice parameters
Spring提供了完全类型化的通知 - 意味着你在建议签名中声明了你需要的参数(就像我们上面看到的返回和抛出的例子),而不是一直使用Object []数组。 我们将看到如何使建议机构可以立即提供参数和其他上下文值。 首先让我们来看看如何编写通用的通知,可以找出通知目前通知的方法。
Access to the current JoinPoint
任何通知方法都可以声明org.aspectj.lang.JoinPoint类型的参数作为第一个参数(请注意,需要around通知来声明ProceedingJoinPoint类型的第一个参数,它是JoinPoint的子类,JoinPoint接口提供了一个 getArgs()(返回方法参数),getThis()(返回代理对象),getTarget()(返回目标对象),getSignature()(返回正在建议的方法的描述 )和toString()(打印一个有用的描述方法建议)。请咨询javadocs的全部细节。
Passing parameters to advice
我们已经看到如何绑定返回的值或异常值(在返回之后和抛出建议之后使用)。 为了使参数值可用于通知主体,可以使用args的绑定形式。 如果在args表达式中使用参数名称代替类型名称,则在调用通知时,相应参数的值将作为参数值传递。 一个例子应该更清楚。 假设你想建议执行以第一个参数为Account对象的dao操作,并且你需要访问通知体中的账户。 你可以写下面的内容:
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
// ...
}
切入点表达式的args(account,..)部分有两个目的:首先,它将匹配限制为只有那些方法至少需要一个参数的方法执行,并且传递给该参数的参数是一个Account实例; 其次,它通过帐户参数使实际的帐户对象可用于建议。
另一种编写这种方式的方法是声明一个切入点,当它匹配一个连接点时“提供”Account对象的值,然后从通知中引用指定的切入点。 这看起来如下所示:
@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}
@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
// ...
}
有兴趣的读者再次参考AspectJ编程指南了解更多细节。
代理对象(this),目标对象(target)和注释(@within,@target,@annotation,@args)都可以以类似的方式绑定。 以下示例显示如何匹配使用@Auditable批注注释的方法的执行,并提取审计代码。
首先定义@Auditable注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
AuditCode value();
}
然后是匹配@Auditable方法的执行的Advice:
@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
AuditCode code = auditable.value();
// ...
}
Advice parameters and generics
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
}
为了使这个工作,我们将不得不检查集合中的每一个元素,这是不合理的,因为我们也不能决定如何处理一般的空值。 为了实现类似于此的操作,您必须将参数键入Collection <?>并手动检查元素的类型。
Determining argument names
通知调用中的参数绑定依赖于在切入点表达式中使用的匹配名称(通知和切入点)方法签名中的声明参数名称。 参数名称不能通过Java反射来使用,所以Spring AOP使用以下策略来确定参数名称:
- 如果用户明确指定了参数名称,则使用指定的参数名称:通知和切入点注释都具有可选的“argNames”属性,可用于指定注释方法的参数名称 - 这些参数 名称在运行时可用。 例如:
@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && 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.lib.Pointcuts.anyPublicMethod() && 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.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
// ... use jp
}
- 使用'argNames'属性有点笨拙,所以如果没有指定'argNames'属性,那么Spring AOP将查看类的调试信息,并尝试从局部变量表中确定参数名称。 只要这些类已经用调试信息编译(至少是'-g:vars'),这个信息就会出现。 使用这个标志进行编译的结果是:(1)你的代码会稍微容易理解(反向工程),(2)类文件的大小会稍微大一点(通常是无关紧要的),(3) 未使用的本地变量将不会被编译器应用。 换句话说,用这个标志建立起来就不会遇到困难。
如果AspectJ编译器(ajc)编译了@AspectJ方面,即使没有调试信息,也不需要添加argNames属性,因为编译器会保留所需的信息。
- 如果代码已经被编译而没有必要的调试信息,那么Spring AOP会尝试推断绑定变量与参数的配对(例如,如果在切入点表达式中只绑定了一个变量,并且通知方法只有一个参数, 配对是显而易见的!)。 如果给定可用的信息,变量的绑定是不明确的,那么就会抛出一个AmbiguousBindingException异常。
- 如果上述所有的策略都失败了,那么IllegalArgumentException将被抛出。
Proceeding with arguments
我们之前说过,我们将描述如何使用Spring AOP和AspectJ一致的参数来编写一个继续调用。 解决方案只是确保建议签名按顺序绑定每个方法参数。 例如:
@Around("execution(List<Account> find*(..)) && " +
"com.xyz.myapp.SystemArchitecture.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相同的优先规则来确定建议执行的顺序。 最高优先级的通知首先在“在途中”运行(因此先给出两条优先级最高的优先级)。 “从出发点出发”,优先级最高的通知最后运行(因此给定两条通知后,优先级最高的通知将运行第二条)。
当在不同方面定义的两条建议都需要在同一个连接点上运行时,除非你指定了否则执行的顺序是未定义的。 您可以通过指定优先级来控制执行顺序。 这是以正常的Spring方式完成的,方法是在方面类中实现org.springframework.core.Ordered接口或使用Order注解对其进行注释。 给定两个方面,从Ordered.getValue()(或注释值)返回较低值的方面具有较高的优先级。
当在同一方面定义的两条建议都需要在同一个连接点上运行时,排序是未定义的(因为没有办法通过反射为javac编译的类检索声明顺序)。 考虑将这些通知方法分解成每个方面类中每个连接点的一个通知方法,或者将通知重构成单独的方面类 - 可以在方面级别进行排序。
5.2.5. Introductions
Introductions(在AspectJ中称为类型间声明)使得一个方面能够声明被通知的对象实现给定的接口,并且代表这些对象提供该接口的实现。
使用@DeclareParents注释进行介绍。 这个注解用来声明匹配的类型有一个新的父亲(因此名字)。 例如,给定一个接口UsageTracked和一个接口DefaultUsageTracked的实现,下面的方面声明所有服务接口的实现者也实现了UsageTracked接口。 (例如,为了通过JMX公开统计信息)
@Aspect
public class UsageTracking {
@DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
public static UsageTracked mixin;
@Before("com.xyz.myapp.SystemArchitecture.businessService() && this(usageTracked)")
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
}
要实现的接口由注释字段的类型决定。 @DeclareParents注解的value属性是一个AspectJ类型模式: - 匹配类型的任何bean将实现UsageTracked接口。 请注意,在上述示例的before建议中,服务bean可以直接用作UsageTracked接口的实现。 如果以编程方式访问一个bean,你可以编写下面的代码:
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
5.2.6. 方面实例化模型
(这是一个高级话题,所以如果你刚开始使用AOP,你可以安全地跳过它,直到后来。)
默认情况下,应用程序上下文中将存在每个方面的单个实例。 AspectJ把这称为单例实例化模型。 可以使用不同的生命周期定义方面:Spring支持AspectJ的perthis和pertarget实例化模型(percflow,percflowbelow和pertypewithin目前不支持)。
通过在@Aspect注释中指定perthis子句来声明“perthis”方面。 我们来看一个例子,然后我们将解释它是如何工作的。
@Aspect("perthis(com.xyz.myapp.SystemArchitecture.businessService())")
public class MyAspect {
private int someState;
@Before(com.xyz.myapp.SystemArchitecture.businessService())
public void recordServiceUsage() {
// ...
}
}
'perthis'子句的作用是,将为执行业务服务的每个唯一服务对象(在与切入点表达式匹配的连接点处绑定到“this”的每个唯一对象)创建一个方面实例。 方面实例是在服务对象上首次调用方法时创建的。 当服务对象超出范围时,方面超出范围。 在创建aspect实例之前,其中的任何通知都不会执行。 一旦创建了aspect实例,其中声明的通知将在匹配的连接点上执行,但只有在服务对象是与此方面相关联的那个时才会执行。 有关每个子句的更多信息,请参阅AspectJ编程指南。
“pertarget”实例化模型的工作方式与perthis完全相同,但为匹配的连接点处的每个唯一目标对象创建一个方面实例。
5.2.7. Example
现在你已经看到所有的组成部分是如何工作的,让我们把它们放在一起做一些有用的事情!
业务服务的执行有时会由于并发问题而失败(例如失败者死锁)。 如果手术重新进行,下一次很有可能成功。 对于适合在这种情况下重试的业务服务(不需要返回给用户进行冲突解决的幂等操作),我们希望透明地重试操作以避免客户端看到PessimisticLockingFailureException。 这是明确切断服务层中多个服务的要求,因此非常适合通过某个方面实现。
因为我们想重试操作,所以我们需要使用周围的建议,以便我们可以多次调用。 以下是基本方面实现的外观:
@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.myapp.SystemArchitecture.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.myapp.service.impl.ConcurrentOperationExecutor">
<property name="maxRetries" value="3"/>
<property name="order" value="100"/>
</bean>
为了改进方面,使其只重试幂等运算,我们可以定义一个幂等注释:
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
// marker annotation
}
并使用注解来注释服务操作的实现。 只改变aspect等幂操作只涉及改进切入点表达式,以便只有@Idempotent操作匹配
@Around("com.xyz.myapp.SystemArchitecture.businessService() && " +
"@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
...
}