Spring AOP

Posted jhhu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring AOP相关的知识,希望对你有一定的参考价值。

使用Spring进行面向切面编程(AOP)

 

6.1. 简介

 

面向切面编程(AOP)通过提供另外一种思考程序结构的途经来弥补面向对象编程(OOP)的不足。在OOP中模块化的关键单元是类(classes),而在AOP中模块化的单元则是切面。切面能对关注点进行模块化,例如横切多个类型和对象的事务管理。(在AOP术语中通常称作横切(crosscutting)关注点。)

AOP框架是Spring的一个重要组成部分。但是Spring IoC容器并不依赖于AOP,这意味着你有权利选择是否使用AOP,AOP做为Spring IoC容器的一个补充,使它成为一个强大的中间件解决方案。

AOP在Spring Framework中的作用

  • 提供声明式企业服务,特别是为了替代EJB声明式服务。最重要的服务是声明性事务管理

  • 允许用户实现自定义切面,用AOP来完善OOP的使用。

如果你只打算使用通用的声明式服务或者封装好的声明式中间件服务,例如缓冲池(pooling),那么你不必与Spring AOP直接打交道,并且本章的大部分内容可以跳过了。

6.1.1. AOP概念

 

首先让我们从一些重要的AOP概念和术语开始。这些术语不是Spring特有的。不过AOP术语并不是特别的直观,如果Spring使用自己的术语,将会变得更加令人困惑。

  • 切面(Aspect):一个关注点的模块化,这个关注点可能会横切多个对象。事务管理是J2EE应用中一个关于横切关注点的很好的例子。在Spring AOP中,切面可以使用基于模式)或者基于@Aspect注解的方式来实现。

  • 连接点(Joinpoint):在程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候。在Spring AOP中,一个连接点总是表示一个方法的执行。

  • 通知(Advice):在切面的某个特定的连接点上执行的动作。其中包括了“around”、“before”和“after”等不同类型的通知(通知的类型将在后面部分进行讨论)。许多AOP框架(包括Spring)都是以拦截器做通知模型,并维护一个以连接点为中心的拦截器链。

  • 切入点(Pointcut):匹配连接点的断言。通知和一个切入点表达式关联,并在满足这个切入点的连接点上运行(例如,当执行某个特定名称的方法时)。切入点表达式如何和连接点匹配是AOP的核心:Spring缺省使用AspectJ切入点语法。

  • 引入(Introduction):用来给一个类型声明额外的方法或属性(也被称为连接类型声明(inter-type declaration))。Spring允许引入新的接口(以及一个对应的实现)到任何被代理的对象。例如,你可以使用引入来使一个bean实现IsModified接口,以便简化缓存机制。

  • 目标对象(Target Object): 被一个或者多个切面所通知的对象。也被称做被通知(advised)对象。 既然Spring AOP是通过运行时代理实现的,这个对象永远是一个被代理(proxied)对象。

  • AOP代理(AOP Proxy):AOP框架创建的对象,用来实现切面契约(例如通知方法执行等等)。在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。

  • 织入(Weaving):把切面连接到其它的应用程序类型或者对象上,并创建一个被通知的对象。这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。

通知类型:

  • 前置通知(Before advice):在某连接点之前执行的通知,但这个通知不能阻止连接点之前的执行流程(除非它抛出一个异常)。

  • 后置通知(After returning advice):在某连接点正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回。

  • 异常通知(After throwing advice):在方法抛出异常退出时执行的通知。

  • 最终通知(After (finally) advice):当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。

  • 环绕通知(Around Advice):包围一个连接点的通知,如方法调用。这是最强大的一种通知类型。环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它自己的返回值或抛出异常来结束执行。

环绕通知是最常用的通知类型。和AspectJ一样,Spring提供所有类型的通知,我们推荐你使用尽可能简单的通知类型来实现需要的功能。例如,如果你只是需要一个方法的返回值来更新缓存,最好使用后置通知而不是环绕通知,尽管环绕通知也能完成同样的事情。用最合适的通知类型可以使得编程模型变得简单,并且能够避免很多潜在的错误。比如,你不需要在JoinPoint上调用用于环绕通知的proceed()方法,就不会有调用的问题。

在Spring 2.0中,所有的通知参数都是静态类型,因此你可以使用合适的类型(例如一个方法执行后的返回值类型)作为通知的参数而不是使用Object数组。

通过切入点匹配连接点的概念是AOP的关键,这使得AOP不同于其它仅仅提供拦截功能的旧技术。 切入点使得通知可以独立对应到面向对象的层次结构中。例如,一个提供声明式事务管理 的环绕通知可以被应用到一组横跨多个对象的方法上(例如服务层的所有业务操作)。

6.1.2. Spring AOP的功能和目标

 

Spring AOP使用纯Java实现。它不需要专门的编译过程。Spring AOP不需要控制类装载器层次,因此它适用于J2EE web容器或应用服务器。

Spring目前仅支持使用方法调用作为连接点(join point)(在Spring bean上通知方法的执行)。虽然可以在不影响到Spring AOP核心API的情况下加入对成员变量拦截器支持,但Spring并没有实现成员变量拦截器。如果你需要把对成员变量的访问和更新也作为通知的连接点,可以考虑其它的语言,如AspectJ。

Spring实现AOP的方法跟其他的框架不同。Spring并不是要提供最完整的AOP实现(尽管Spring AOP有这个能力),相反的,它其实侧重于提供一种AOP实现和Spring IoC容器之间的整合,用于帮助解决在企业级开发中的常见问题。

因此,Spring的AOP功能通常都和Spring IoC容器一起使用。切面使用普通的bean定义语法来配置(尽管Spring提供了强大的"自动代理(autoproxying)"功能):与其他AOP实现相比这是一个显著的区别。有些事使用Spring AOP是无法轻松或者高效完成的,比如说通知一个细粒度的对象(例如典型的域对象):这种时候,使用AspectJ是最好的选择。不过经验告诉我们,对于大多数在J2EE应用中适合用AOP来解决的问题,Spring AOP都提供了一个非常好的解决方案。

Spring AOP从来没有打算通过提供一种全面的AOP解决方案来与AspectJ竞争。我们相信无论是基于代理(proxy-based)的框架如Spring AOP或者是成熟的框架如AspectJ都是很有价值的,他们之间应该是互补而不是竞争的关系。Spring 2.0可以无缝的整合Spring AOP,IoC和AspectJ,使得所有的AOP应用完全融入基于Spring的应用体系。这样的集成不会影响Spring AOP API或者AOP Alliance API;Spring AOP保持了向下兼容性。下一章会详细讨论Spring AOP的API。

[Note] Note

Spring Framework一个重要的原则就是无侵入性(non-invasiveness); 这个思想指你不应当被迫引入框架特定的类和接口到你的业务/领域模型中。然而,Spring Framework在某些地方给你一个是否引入Spring框架特定依赖到你的代码的选项: 给你这个选项的理由是因为在特定的场景中它可能仅仅是容易阅读或用这种方法编写特定的功能块。Spring Framework(几乎)一直会为你提供这种选择:从而使你能做出一个明智的决定,使它最适应你的特定用例或场景。

你可以选择AspectJ或者Spring AOP,以及选择是使用@AspectJ注解风格还是Spring XML配置风格。事实上本章选择先介绍@AspectJ风格的方法不应当被看作是这样一个暗示:Spring小组喜欢@AspectJ注解风格更胜于Spring XML配置。

Section 6.4, “AOP声明风格的选择”一章有对使用各个风格理由的一个更全面的讨论。

6.1.3. AOP代理

 

Spring缺省使用J2SE 动态代理(dynamic proxies)来作为AOP的代理。 这样任何接口(或者接口集)都可以被代理。

Spring也可以使用CGLIB代理. 对于需要代理类而不是代理接口的时候CGLIB代理是很有必要的。如果一个业务对象并没有实现一个接口,默认就会使用CGLIB。作为面向接口编程的最佳实践,业务对象通常都会实现一个或多个接口。但也有可能会强制使用CGLIB,在这种情况(希望不常有)下,你可能需要通知一个没有在接口中声明的方法,或者需要传入一个代理对象给方法作为具体类型

为了明白Spring AOP是基于代理(proxy-based)的事实,请参阅Section 6.6.1, “理解AOP代理”

6.2. @AspectJ支持

 

@AspectJ使用了Java 5的注解,可以将切面声明为普通的Java类。@AspectJ样式在AspectJ 5发布的AspectJ project部分中被引入。Spring 2.0使用了和AspectJ 5一样的注解,并使用AspectJ来做切入点解析和匹配。但是,AOP在运行时仍旧是纯的Spring AOP,并不依赖于AspectJ的编译器或者织入器(weaver)。

使用AspectJ的编译器或者织入器的话就可以使用完整的AspectJ语言,我们将在Section 6.8, “在Spring应用中使用AspectJ”中讨论这个问题。

6.2.1. 启用@AspectJ支持

 

为了在Spring配置中使用@AspectJ切面,你首先必须启用Spring对@AspectJ切面配置的支持,并确保自动代理(autoproxying)的bean是否能被这些切面通知。自动代理是指Spring会判断一个bean是否使用了一个或多个切面通知,并据此自动生成相应的代理以拦截其方法调用,并且确保通知在需要时执行。

通过在你的Spring的配置中引入下列元素来启用Spring对@AspectJ的支持:

<aop:aspectj-autoproxy/>

我们假定你正在使用Appendix A, XML Schema-based configuration所描述的schema支持。关于如何在aop的命名空间中引入这些标签,请参见Section A.2.7, “The aop schema”

如果你正在使用DTD,你仍然可以通过在你的application context中添加如下定义来启用@AspectJ支持:

<bean class="org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator" />

你需要在你的应用程序的classpath中引入两个AspectJ库:aspectjweaver.jaraspectjrt.jar。这些库可以在AspectJ的安装包(1.5.1或者之后的版本)的\'lib\'目录里找到,或者也可以在Spring-with-dependencies发布包的\'lib/aspectj\'目录下找到。

6.2.2. 声明一个切面

 

启用@AspectJ支持后,在application context中定义的任意带有一个@Aspect切面(拥有@Aspect注解)的bean都将被Spring自动识别并用于配置Spring AOP。以下例子展示了为完成一个不是非常有用的切面所需要的最小定义:

application context中一个常见的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注解的类)和其他类一样有方法和字段定义。他们也可能包括切入点,通知和引入(inter-type)声明。

[Note] 通知切面

在Spring AOP中,拥有切面的类本身可能是其它切面中通知的目标。一个类上面的@Aspect注解标识它为一个切面,并且从自动代理中排除它。

6.2.3. 声明一个切入点(pointcut)

 

在前面我们提到,切入点决定了连接点关注的内容,使得我们可以控制通知什么时候执行。Spring AOP只支持Spring bean的方法执行连接点。所以你可以把切入点看做是Spring bean上方法执行的匹配。一个切入点声明有两个部分:一个包含名字和任意参数的签名,还有一个切入点表达式,该表达式决定了我们关注那个方法的执行。在@AspectJ注解风格的AOP中,一个切入点签名通过一个普通的方法定义来提供,并且切入点表达式使用@Pointcut注解来表示(作为切入点签名的方法必须返回void 类型)。

用一个例子能帮我们清楚的区分切入点签名和切入点表达式之间的差别,下面的例子定义了一个切入点\'anyOldTransfer\',这个切入点将匹配任何名为 "transfer" 的方法的执行:

@Pointcut("execution(* transfer(..))")// the pointcut expression
private void anyOldTransfer() {}// the pointcut signature

切入点表达式,也就是组成@Pointcut注解的值,是正规的AspectJ 5切入点表达式。如果你想要更多了解AspectJ的切入点语言,请参见AspectJ编程指南(如果要了解基于Java 5的扩展请参阅AspectJ 5 开发手册)或者其他人写的关于AspectJ的书,例如Colyer et. al.著的“Eclipse AspectJ”或者Ramnivas Laddad著的“AspectJ in Action”。

6.2.3.1. 切入点指示符(PCD)的支持

 

Spring AOP支持在切入点表达式中使用如下的AspectJ切入点指示符:

  • execution - 匹配方法执行的连接点,这是你将会用到的Spring的最主要的切入点指示符。

  • within - 限定匹配特定类型的连接点(在使用Spring AOP的时候,在匹配的类型中定义的方法的执行)。

  • this - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中bean reference(Spring AOP 代理)是指定类型的实例。

  • target - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中目标对象(被代理的应用对象)是指定类型的实例。

  • args - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中参数是指定类型的实例。

  • @target - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中正执行对象的类持有指定类型的注解。

  • @args - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中实际传入参数的运行时类型持有指定类型的注解。

  • @within - 限定匹配特定的连接点,其中连接点所在类型已指定注解(在使用Spring AOP的时候,所执行的方法所在类型已指定注解)。

  • @annotation - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中连接点的主题持有指定的注解。

另外,Spring AOP还提供了一个名为\'bean\'的PCD。这个PCD允许你限定匹配连接点到一个特定名称的Spring bean,或者到一个特定名称Spring bean的集合(当使用通配符时)。\'bean\' PCD具有下列的格式:

bean(idOrNameOfBean)

\'idOrNameOfBean\'标记可以是任何Spring bean的名字:限定通配符使用\'*\'来提供,如果你为Spring bean制定一些命名约定,你可以非常容易地编写一个\'bean\' PCD表达式将它们选出来。和其它连接点指示符一样,\'bean\' PCD也支持&&, ||和 !逻辑操作符。

[Note] Note

请注意\'bean\' PCD仅仅 被Spring AOP支持而不是AspectJ. 这是Spring对AspectJ中定义的标准PCD的一个特定扩展。

\'bean\' PCD不仅仅可以在类型级别(被限制在基于织入AOP上)上操作而还可以在实例级别(基于Spring bean的概念)上操作。

因为Spring AOP限制了连接点必须是方法执行级别的,上文pointcut指示符中的讨论也给出了一个定义,这个定义和AspectJ的编程指南中的定义相比显得更加狭窄。除此之外,AspectJ它本身有基于类型的语义,在执行的连接点\'this\'和\'target\'都是指同一个对象,也就是执行方法的对象。Spring AOP是一个基于代理的系统,并且严格区分代理对象本身(对应于\'this\')和背后的目标对象(对应于\'target\')

6.2.3.2. 组合切入点表达式

 

切入点表达式可以使用\'&\', \'||\' 和 \'!\'来组合。还可以通过名字来指向切入点表达式。以下的例子展示了三种切入点表达式: anyPublicOperation(在一个方法执行连接点代表了任意public方法的执行时匹配);inTrading(在一个代表了在交易模块中的任意的方法执行时匹配)和 tradingOperation(在一个代表了在交易模块中的任意的公共方法执行时匹配)。

@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}

@Pointcut("within(com.xyz.someapp.trading..*")
private void inTrading() {}

@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}
        

如上所示,用更少的命名组件来构建更加复杂的切入点表达式是一种最佳实践。当用名字来指定切入点时使用的是常见的Java成员可视性访问规则。(比如说,你可以在同一类型中访问私有的切入点,在继承关系中访问受保护的切入点,可以在任意地方访问公共切入点)。成员可视性访问规则不影响到切入点的匹配

6.2.3.3. 共享通用切入点定义

 

当开发企业级应用的时候,你通常会想要从几个切面来引用模块化的应用和特定操作的集合。我们推荐定义一个“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.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() {}

}

示例中的切入点定义了一个你可以在任何需要切入点表达式的地方可引用的切面。比如,为了使service层事务化,你可以写成:

<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>

我们将在Section 6.3, “基于Schema的AOP支持”中讨论 <aop:config><aop:advisor>标签。在Chapter 9, 事务管理中讨论事务标签。

6.2.3.4. 示例

 

Spring AOP 用户可能会经常使用 execution切入点指示符。执行表达式的格式如下:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)
          throws-pattern?)

除了返回类型模式(上面代码片断中的ret-type-pattern),名字模式和参数模式以外, 所有的部分都是可选的。返回类型模式决定了方法的返回类型必须依次匹配一个连接点。 你会使用的最频繁的返回类型模式是*,它代表了匹配任意的返回类型。 一个全限定的类型名将只会匹配返回给定类型的方法。名字模式匹配的是方法名。 你可以使用*通配符作为所有或者部分命名模式。 参数模式稍微有点复杂:()匹配了一个不接受任何参数的方法, 而(..)匹配了一个接受任意数量参数的方法(零或者更多)。 模式(*)匹配了一个接受一个任何类型的参数的方法。 模式(*,String)匹配了一个接受两个参数的方法,第一个可以是任意类型, 第二个则必须是String类型。更多的信息请参阅AspectJ编程指南中 语言语义的部分。

下面给出一些通用切入点表达式的例子。

  • 任意公共方法的执行:

    execution(public * *(..))
  • 任何一个名字以“set”开始的方法的执行:

    execution(* set*(..))
  • AccountService接口定义的任意方法的执行:

    execution(* com.xyz.service.AccountService.*(..))
  • 在service包中定义的任意方法的执行:

    execution(* com.xyz.service.*.*(..))
  • 在service包或其子包中定义的任意方法的执行:

    execution(* com.xyz.service..*.*(..))
  • 在service包中的任意连接点(在Spring AOP中只是方法执行):

    within(com.xyz.service.*)
  • 在service包或其子包中的任意连接点(在Spring AOP中只是方法执行):

    within(com.xyz.service..*)
  • 实现了AccountService接口的代理对象的任意连接点 (在Spring AOP中只是方法执行):

    this(com.xyz.service.AccountService)

    \'this\'在绑定表单中更加常用:- 请参见后面的通知一节中了解如何使得代理对象在通知体内可用。

  • 实现AccountService接口的目标对象的任意连接点 (在Spring AOP中只是方法执行):

    target(com.xyz.service.AccountService)

    \'target\'在绑定表单中更加常用:- 请参见后面的通知一节中了解如何使得目标对象在通知体内可用。

  • 任何一个只接受一个参数,并且运行时所传入的参数是Serializable 接口的连接点(在Spring AOP中只是方法执行)

    args(java.io.Serializable)
    \'args\'在绑定表单中更加常用:- 请参见后面的通知一节中了解如何使得方法参数在通知体内可用。

    请注意在例子中给出的切入点不同于 execution(* *(java.io.Serializable)): args版本只有在动态运行时候传入参数是Serializable时才匹配,而execution版本在方法签名中声明只有一个 Serializable类型的参数时候匹配。

  • 目标对象中有一个 @Transactional 注解的任意连接点 (在Spring AOP中只是方法执行)

    @target(org.springframework.transaction.annotation.Transactional)

    \'@target\'在绑定表单中更加常用:- 请参见后面的通知一节中了解如何使得注解对象在通知体内可用。

  • 任何一个目标对象声明的类型有一个 @Transactional 注解的连接点 (在Spring AOP中只是方法执行):

    @within(org.springframework.transaction.annotation.Transactional)

    \'@within\'在绑定表单中更加常用:- 请参见后面的通知一节中了解如何使得注解对象在通知体内可用。

  • 任何一个执行的方法有一个 @Transactional 注解的连接点 (在Spring AOP中只是方法执行)

    @annotation(org.springframework.transaction.annotation.Transactional)

    \'@annotation\'在绑定表单中更加常用:- 请参见后面的通知一节中了解如何使得注解对象在通知体内可用。

  • 任何一个只接受一个参数,并且运行时所传入的参数类型具有@Classified 注解的连接点(在Spring AOP中只是方法执行)

    @args(com.xyz.security.Classified)

    \'@args\'在绑定表单中更加常用:- 请参见后面的通知一节中了解如何使得注解对象在通知体内可用。

  • 任何一个在名为\'tradeService\'的Spring bean之上的连接点 (在Spring AOP中只是方法执行):

    bean(tradeService)
  • 任何一个在名字匹配通配符表达式\'*Service\'的Spring bean之上的连接点 (在Spring AOP中只是方法执行):

    bean(*Service)

6.2.4. 声明通知

 

通知是跟一个切入点表达式关联起来的,并且在切入点匹配的方法执行之前或者之后或者前后运行。 切入点表达式可能是指向已命名的切入点的简单引用或者是一个已经声明过的切入点表达式。

6.2.4.1. 前置通知

 

一个切面里使用 @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() {
    // ...
  }

}

如果使用一个in-place 的切入点表达式,我们可以把上面的例子换个写法:

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() {
    // ...
  }

}

6.2.4.2. 后置通知(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() {
    // ...
  }

}
说明:你可以在相同的切面里定义多个通知,或者其他成员。 我们只是在展示如何定义一个简单的通知。这些例子主要的侧重点是正在讨论的问题。

有时候你需要在通知体内得到返回的值。你可以使用@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) {
    // ...
  }
  
}

在 returning属性中使用的名字必须对应于通知方法内的一个参数名。 当一个方法执行返回后,返回值作为相应的参数值传入通知方法。 一个returning子句也限制了只能匹配到返回指定类型值的方法。 (在本例子中,返回值是Object类,也就是说返回任意类型都会匹配)

请注意当使用后置通知时允许返回一个完全不同的引用。

6.2.4.3. 异常通知(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属性中使用的名字必须与通知方法内的一个参数对应。 当一个方法因抛出一个异常而中止后,这个异常将会作为那个对应的参数送至通知方法。 throwing 子句也限制了只能匹配到抛出指定异常类型的方法 (上面的示例为DataAccessException)。

6.2.4.4. 最终通知(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() {
    // ...
  }

}

6.2.4.5. 环绕通知

 

最后一种通知是环绕通知。环绕通知在一个方法执行之前和之后执行。它使得通知有机会 在一个方法执行之前和执行之后运行。而且它可以决定这个方法在什么时候执行,如何执行,甚至是否执行。 环绕通知经常在某线程安全的环境下,你需要在一个方法执行之前和之后共享某种状态的时候使用。 请尽量使用最简单的满足你需求的通知。(比如如果简单的前置通知也可以适用的情况下不要使用环绕通知)。

环绕通知使用@Around注解来声明。通知的第一个参数必须是 ProceedingJoinPoint类型。在通知体内,调用 ProceedingJoinPointproceed()方法会导致 后台的连接点方法执行。proceed 方法也可能会被调用并且传入一个 Object[]对象-该数组中的值将被作为方法执行时的参数。

当传入一个Object[]对象的时候,处理的方法与通过AspectJ编译器处理环绕通知略有不同。 对于使用传统AspectJ语言写的环绕通知来说,传入参数的数量必须和传递给环绕通知的参数数量匹配 (不是后台的连接点接受的参数数量),并且特定顺序的传入参数代替了将要绑定给连接点的原始值 (如果你看不懂不用担心)。Spring采用的方法更加简单并且能更好匹配它基于代理(proxy-based)的执行语法, 如果你使用AspectJ的编译器和编织器来编译为Spring而写的@AspectJ切面和处理参数,你只需要知道这一区别即可。 有一种方法可以让你写出100%兼容Spring AOP和AspectJ的表达式,我们将会在后续的通知参数的章节中讨论它。
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;
  }

}

方法的调用者得到的返回值就是环绕通知返回的值。 例如:一个简单的缓存切面,如果缓存中有值,就返回该值,否则调用proceed()方法。 请注意proceed可能在通知体内部被调用一次,许多次,或者根本不被调用,所有这些都是合法的。

6.2.4.6. 通知参数(Advice parameters)

 

Spring 2.0 提供了完整的通知类型 - 这意味着你可以在通知签名中声明所需的参数, (就像我们在前面看到的后置和异常通知一样)而不总是使用Object[]。 我们将会看到如何使得参数和其他上下文值对通知体可用。 首先让我们看以下如何编写普通的通知以找出正在被通知的方法。

6.2.4.6.1. 访问当前的连接点
 

任何通知方法可以将第一个参数定义为org.aspectj.lang.JoinPoint类型 (环绕通知需要定义第一个参数为ProceedingJoinPoint类型, 它是 JoinPoint 的一个子类)。JoinPoint 接口提供了一系列有用的方法,比如 getArgs()(返回方法参数)、 getThis()(返回代理对象)、getTarget()(返回目标)、getSignature()(返回正在被通知的方法相关信息)和 toString() (打印出正在被通知的方法的有用信息)。详细的内容请参考JavaDoc。

6.2.4.6.2. 传递参数给通知
 

我们已经看到了如何绑定返回值或者异常(使用后置通知和异常通知)。为了可以在通知体内访问参数, 你可以使用args来绑定。如果在一个args表达式中应该使用类型名字的地方 使用一个参数名字,那么当通知执行的时候对应的参数值将会被传递进来。用一个例子应该会使它变得清晰。 假使你想要通知以一个Account对象作为第一个参数的DAO操作的执行, 你想要在通知体内也能访问account对象,可以编写如下的代码:

@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() &&" + 
        "args(account,..)")
public void validateAccount(Account account) {
  // ...
}

切入点表达式的 args(account,..) 部分有两个目的:首先它保证了 只会匹配那些接受至少一个参数的方法的执行,而且传入的参数必须是Account类型的实例, 其次它使得在通知体内可以通过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注解来匹配方法执行,并提取Audit代码。

首先是@Auditable注解的定义:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
	AuditCode value();
}

然后是匹配@Auditable方法执行的通知:

@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && " + 
        "@annotation(auditable)")
public void audit(Auditable auditable) {
  AuditCode code = auditable.value();
  // ...
}
6.2.4.6.3. 确定参数名
 

绑定在通知上的参数依赖切入点表达式的匹配名,并借此在(通知和切入点)的方法签名中声明参数名。 参数名无法 通过Java反射来获取,所以Spring AOP使用如下的策略来确定参数名字:

  1. 如果参数名字已经被用户明确指定,则使用指定的参数名: 通知和切入点注解有一个额外的"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
    }
  2. 使用\'argNames\'属性有一点笨拙,所以如果\'argNames\' 属性没有被指定,Spring AOP将查看类的debug信息并尝试从本地的变量表确定参数名。只要类编译时有debug信息, (最少要有\'-g:vars\')这个信息将会出现。打开这个标志编译的结果是: (1)你的代码稍微容易理解(反向工程), (2)class文件的大小稍微有些大(通常不重要), (3)你的编译器将不会应用优化去移除未使用的本地变量。换句话说,打开这个标志创建时你应当不会遇到困难。

    如果一个@AspectJ切面已经被AspectJ编译器(ajc)编译过,即使没有debug信息, 也不需要添加argNames参数,因为编译器会保留必需的信息。
  3. 如果不加上必要的debug信息来编译的话,Spring AOP将会尝试推断绑定变量到参数的配对。 (例如,要是只有一个变量被绑定到切入点表达式,通知方法只接受一个参数, 配对是显而易见的)。 如果变量的绑定不明确,将会抛出一个AmbiguousBindingException异常。

  4. 如果以上所有策略都失败了,将会抛出一个IllegalArgumentException异常。

6.2.4.6.4. 处理参数
 

我们之前提过我们将会讨论如何编写一个带参数的的proceed()调用, 使得在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});
}        

大多数情况下你都会这样绑定(就像上面的例子那样)。

6.2.4.7. 通知顺序

 

如果有多个通知想要在同一连接点运行会发生什么?Spring AOP遵循跟AspectJ一样的优先规则来确定通知执行的顺序。 在“进入”连接点的情况下,最高优先级的通知会先执行(所以给定的两个前置通知中,优先级高的那个会先执行)。 在“退出”连接点的情况下,最高优先级的通知会最后执行。(所以给定的两个后置通知中, 优先级高的那个会第二个执行)。

当定义在不同的切面里的两个通知都需要在一个相同的连接点中运行, 那么除非你指定,否则执行的顺序是未知的。你可以通过指定优先级来控制执行顺序。 在标准的Spring方法中可以在切面类中实现org.springframework.core.Ordered 接口或者用Order注解做到这一点。在两个切面中, Ordered.getValue()方法返回值(或者注解值)较低的那个有更高的优先级。

当定义在相同的切面里的两个通知都需要在一个相同的连接点中运行, 执行的顺序是未知的(因为这里没有方法通过反射javac编译的类来获取声明顺序)。 考虑在每个切面类中按连接点压缩这些通知方法到一个通知方法,或者重构通知的片段到各自的切面类中 - 它能在切面级别进行排序。

6.2.5. 引入(Introduction)

 

引入(在AspectJ中被称为inter-type声明)使得一个切面可以定义被通知对象实现给定的接口, 并且可以为那些对象提供具体的实现。

使用@DeclareParents注解来定义引入。这个注解用来定义匹配的类型 拥有一个新的父类(所以有了这个名字)。比如,给定一个接口UsageTracked, 和接口的具体实现DefaultUsageTracked类, 接下来的切面声明了所有的service接口的实现都实现了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接口。请注意,在上面的前置通知的例子中,service beans 可以直接用作UsageTracked接口的实现。如果需要编程式的来访问一个bean, 你可以这样写:

UsageTracked usageTracked = (UsageTracked) context.getBean("myService");

6.2.6. 切面实例化模型

 
(这是一个高级主题,所以如果你刚开始学习AOP你可以跳过它到后面的章节)

默认情况下,在application context中每一个切面都会有一个实例。AspectJ把这个叫做单例化模型。 也可以用其他的生命周期来定义切面:Spring支持AspectJ的 perthis 和pertarget实例化模型(现在还不支持percflow、percflowbelow 和pertypewithin)。

一个"perthis" 切面通过在@Aspect注解中指定perthis 子句来声明。让我们先来看一个例子,然后解释它是如何运作的:

@Aspect("perthis(com.xyz.myapp.SystemArchitecture.businessService())")
public class MyAspect {

  private int someState;
	
  @Before(com.xyz.myapp.SystemArchitecture.businessService())
  public void recordServiceUsage() {
    // ...
  }
  	
}

这个\'perthis\'子句的效果是每个独立的service对象执行一个业务时都会 创建一个切面实例(切入点表达式所匹配的连接点上的每一个独立的对象都会绑定到\'this\'上)。 在service对象上第一次调用方法的时候,切面实例将被创建。切面在service对象失效的同时失效。 在切面实例被创建前,所有的通知都不会被执行,一旦切面对象创建完成, 定义的通知将会在匹配的连接点上执行,但是只有当service对象是和切面关联的才可以。 请参阅 AspectJ 编程指南了解更多关于per-clauses的信息。

\'pertarget\'实例模型的跟“perthis”完全一样,只不过是为每个匹配于连接点 的独立目标对象创建一个切面实例。

6.2.7. 例子

 

现在你已经看到了每个独立的部分是如何运作的了,是时候把他们放到一起做一些有用的事情了!

因为并发的问题,有时候业务服务(business services)可能会失败(例如,死锁失败)。如果重新尝试一下, 很有可能就会成功。对于业务服务来说,重试几次是很正常的(Idempotent操作不需要用户参与,否则会得出矛盾的结论) 我们可能需要透明的重试操作以避免客户看到一个PessimisticLockingFailureException异常。 很明显,在一个横切多层的情况下,这是非常有必要的,因此通过切面来实现是很理想的。

因为我们想要重试操作,我们会需要使用到环绕通知,这样我们就可以多次调用proceed()方法。 下面是简单的切面实现:

@Aspect
public class ConcurrentOperationExecutor implements Ordered {
   
   private static final int DEFAULT_MAX_RETRIES = 2;

   private int maxRetries = DEFAULT_MAX_RETRIES;
   private int order = 1;

   public void setMaxRetries(int maxRetries) {
      this.maxRetries = maxRetries;
   }
   
   public int getOrder() {
      return this.order;
   }
   
   public void setOrder(int order) {
      this.order = order;
   }
   
   @Around("com.xyz.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以上是关于Spring AOP的主要内容,如果未能解决你的问题,请参考以下文章

JAVA之AOP

Spring AOP

Spring源码高级笔记之——Spring AOP应用

2018.12.24 Spring中的aop演示

Spring框架 AOP

Spring的AOP面向切面编程