Spring 5 中文解析核心篇-IoC容器之AOP编程(上)

面向切面的编程(AOP)经过提供另外一种思考程序结构的方式来补充面向对像的编程(OOP)。OOP中模块化的关键单元是<u>类</u>,而在AOP中模块化是<u>切面</u>。切面使关注点(例如事务管理)的模块化能够跨越多种类型和对象。(这种关注在AOP文献中一般被称为“跨领域”关注。)html

Spring的关键组件之一是AOP框架。虽然Spring IoC容器不依赖于AOP(这意味着若是你不想使用AOP,就不须要使用AOP),但AOP对Spring IoC进行了补充,提供了一个很是强大的中间件解决方案。java

具备AspectJ切入点的Spring AOPgit

Spring提供了使用基于schema的方法或@AspectJ注解样式来编写自定义切面的简单而强大的方法。这两种样式都提供了彻底类型化的建议,并使用了AspectJ切入点语言,同时仍然使用Spring AOP进行编织。web

本章讨论基于schema和基于@AspectJ的AOP支持。下一章将讨论较低级别的AOP支持。spring

AOP在Spring框架中用于:express

  • 提供声明式企业服务。此类服务中最重要的是声明式事务管理
  • 让用户实现自定义切面,并用AOP补充其对OOP的使用。

若是你只对通用声明性服务或其余预包装的声明性中间件服务(例如池)感兴趣,则无需直接使用Spring AOP,而且能够跳过本章的大部份内容。编程

5.1 AOP概念

让咱们首先定义一些主要的AOP概念和术语。这些术语不是特定于Spring的。不幸的是,AOP术语并非特别直观。可是,若是使用Spring本身的术语,将会更加使人困惑。api

  • 切面:涉及多个类别的关注点的模块化。事务管理是企业Java应用程序中横切关注的一个很好的例子。在Spring AOP中,切面是经过使用常规类(基于schema的方法)或使用@Aspect注解(@AspectJ样式)注释的常规类来实现的。
  • 链接点:程序执行过程当中的一点,例如方法的执行或异常的处理。在Spring AOP中,链接点始终表明方法的执行。
  • 通知:切面在特定的链接点处采起的操做。不一样类型的通知包括:“around”,“before”和“after”通知。(通知类型将在后面讨论。)包括Spring在内的许多AOP框架都将通知建模为拦截器,并在链接点周围维护一系列拦截器。
  • 切入点:表示匹配链接点。通知与切入点表达式关联,并在与该切入点匹配的任何链接点处运行(例如,执行具备特定名称的方法)。切入点表达式匹配的链接点的概念是AOP的核心,默认状况下,Spring使用AspectJ切入点表达语言。
  • 引入:在类型上声明其余方法或字段。Spring AOP容许你向任何通知对象引入新的接口(和相应的实现)。例如,你可使用引入使Bean实现IsModified接口,以简化缓存。(引入在AspectJ社区中称为类型间声明。)
  • 目标对象:一个或多个切面通知的对象。也称为“通知对象”。因为Spring AOP是使用运行时代理实现的,所以该对象始终是代理对象。
  • AOP代理:由AOP框架建立的对象,用于实现切面约定(通知方法执行等)。在Spring Framework中,AOP代理是JDK动态代理或CGLIB代理。
  • 编织:将切面与其余应用程序类型或对象连接以建立通知的对象。这能够在编译时(例如,使用AspectJ编译器),加载时或在运行时完成。像其余纯Java AOP框架同样,Spring AOP在运行时执行编织。

Spring AOP包括如下类型的通知:数组

  • 前置通知:在链接点以前运行但没法阻止执行流前进到链接点的通知(除非它引起异常)。
  • 后置通知:链接点正常完成后要运行的通知(例如,若是某个方法返回而没有引起异常)。
  • 后置异常通知:若是方法因抛出异常而退出,将执行的通知。
  • 最终通知:不管链接点退出的方式如何(正常或异常返回),都将执行通知。
  • 环绕通知:围绕链接点的通知,例如方法调用。这是最强大的通知。环绕通知能够在方法调用以前和以后执行自定义行为。它还负责选择是继续到链接点,仍是经过返回本身的返回值或抛出异常来简化通知的方法执行。

环绕通知是最通用的通知。因为Spring AOP与AspectJ同样,提供了各类通知类型,所以咱们建议你使用功能最弱的建议类型,以实现所需的行为。例如,若是你只须要使用方法的返回值更新缓存,则最好使用后置通知而不是环绕通知,尽管环绕通知能够完成相同的事情。使用最具体的通知类型可提供更简单的编程模型,并减小出错的可能性。例如,你不须要在用于环绕通知的JoinPoint上调用proceed()方法,所以,你不会失败。缓存

全部通知参数都是静态类型的,所以你可使用适当类型(例如,从方法执行返回的值的类型)而不是对象数组的 通知参数。

切入点匹配的链接点的概念是AOP的关键,它与仅提供拦截功能的旧技术不一样。切入点使通知的目标独立于面向对象的层次结构。例如,你能够将提供声明性事务管理的环绕通知应用于跨越多个对象(例在服务层中的全部业务操做)的一组方法。

5.2 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 Framework的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 Framework确实为你提供了将特定于Spring Framework的依赖项引入代码库的选项。提供此类选项的理由是,在某些状况下,以这种方式阅读或编码某些特定功能可能会变得更加容易。可是,Spring框架(几乎)老是为你提供选择:你能够自由地就哪一个选项最适合你的特定用例或场景作出明智的决定。

与本章相关的一种选择是选择哪一种AOP框架(以及哪一种AOP样式)。你能够选择AspectJ和或Spring AOP。你也能够选择@AspectJ注解样式方法或Spring XML配置样式方法。本章选择首先介绍@AspectJ风格的方法,这不能代表Spring比Spring XML配置风格更喜欢@AspectJ注释风格的方法(备注:使用AspectJ编写例子不能说明Spring更喜欢AspectJ注解编程)。

有关每种样式的“前因后果”的更完整讨论,请参见选择要使用的AOP声明样式

5.3 AOP代理

Spring AOP默认将标准JDK动态代理用于AOP代理。这使得能够代理任何接口(或一组接口)。

Spring AOP也可使用CGLIB代理。这对于代理类而不是接口是必需的。默认状况下,若是业务对象未实现接口,则使用CGLIB。因为对接口而不是对类进行编程是一种好习惯,所以业务类一般实现一个或多个业务接口。在某些状况下(可能极少发生),你须要通知在接口上未声明的方法,或须要将代理对象做为具体类型传递给方法,则能够强制使用CGLIB

掌握Spring AOP是基于代理的这一事实很重要。请参阅了解AOP代理以全面了解此实现细节的实际含义。

5.4 @AspectJ支持

@AspectJ是一种将切面声明为带有注解的常规Java类的样式。@AspectJ样式是AspectJ项目在AspectJ 5版本中引入的。Spring使用AspectJ提供的用于切入点解析和匹配的库来解释与AspectJ 5相同的注解。可是,AOP运行时仍然是纯Spring AOP,而且不依赖于AspectJ编译器或编织器。

使用AspectJ编译器和编织器可使用完整的AspectJ语言,有关在Spring Applications中使用AspectJ进行了讨论。

5.4.1 激活@AspectJ支持

要在Spring配置中使用@AspectJ切面,你须要启用Spring支持以基于@AspectJ切面配置Spring AOP,并根据这些切面是否通知对Bean进行自动代理。经过自动代理,咱们的意思是,若是Spring肯定一个或多个切面通知一个bean,它会自动为该bean生成一个代理来拦截方法调用并确保按需执行通知。

可使用XML或Java样式的配置来启用@AspectJ支持。不管哪一种状况,你都须要确保AspectJAspectjweaver.jar库位于应用程序的类路径(版本1.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 Schema的配置中所述的架构支持。有关如何在aop名称空间中导入标签的信息,请参见AOP schema

5.4.2 声明一个切面

启用@AspectJ支持后,Spring会自动检测在应用程序上下文中使用@AspectJ切面(有@Aspect注解)的类定义的任何bean,并用于配置Spring AOP。接下来的两个示例显示了一个不太有用的切面所需的最小定义。

两个示例中的第一个示例显示了应用程序上下文中的常规bean定义,该定义指向具备@Aspect注解的bean类:

<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
    <!-- configure properties of the aspect here -->
</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的组件扫描程序的规则,有条件的自定义构造型注解)。

向其余切面提供通知

在Spring AOP中,切面自己不能成为其余切面的通知目标。类上的@Aspect注解将其标记为一个切面,所以将其从自动代理中排除。

5.4.3 声明切入点

切入点肯定了感兴趣的链接点,从而使咱们可以控制什么时候执行通知。Spring AOP仅支持Spring Bean的方法执行链接点,所以你能够将切入点视为与Spring Bean上的方法执行匹配。切入点声明由两部分组成:一个包含名称和任何参数的签名,以及一个切入点表达式,该表达式精确肯定咱们感兴趣的方法执行。在AOP的@AspectJ注解样式中,常规方法定义提供了切入点签名,而且使用@Pointcut注解指示了切入点表达式(用做切入点签名的方法必须具备void返回类型)。一个示例可能有助于使切入点签名和切入点表达式之间的区别变得清晰。下面的示例定义一个名为anyOldTransfer的切入点,该切入点与任何名为transfer方法的执行相匹配:

@Pointcut("execution(* transfer(..))") // 切入点表达式
private void anyOldTransfer() {} // 切入点方法签名

造成@Pointcut注解的值的切入点表达式是一个常规的AspectJ 5切入点表达式。有关AspectJ的切入点语言的完整讨论,请参见AspectJ编程指南(以及扩展,包括AspectJ 5开发人员手册)或有关AspectJ的书籍之一(如《Eclipse AspectJ》或《 AspectJ in Action》 )。

支持的切入点指示符

Spring AOP支持如下在切入点表达式中使用的AspectJ<u>切入点指示符(PCD)</u>:

  • execution: 用于匹配方法执行的链接点。这是使用Spring AOP时要使用的主要切入点指示符。
  • within: 限制对某些类型内的链接点的匹配(使用Spring AOP时在匹配类型内声明的方法的执行)。
  • this:限制匹配到链接点(使用Spring AOP时方法的执行)的匹配,其中bean引用(Spring AOP代理)是给定类型的实例。
  • target: 限制匹配到链接点(使用Spring AOP时方法的执行)的匹配,其中目标对象(代理的应用程序对象)是给定类型的实例。
  • args: 限制匹配到链接点(使用Spring AOP时方法的执行)的匹配,其中参数是给定类型的实例。
  • @target: 限制匹配到链接点(使用Spring AOP时方法的执行)的匹配,其中执行对象的类具备给定类型的注释。
  • @args:限制匹配的链接点(使用Spring AOP时方法的执行),其中传递的实际参数的运行时类型具备给定类型的注解。
  • @within:限制匹配到具备给定注解的类型中的链接点(使用Spring AOP时,使用给定注解在类型中声明的方法的执行)。
  • @annotation: 将匹配点限制在链接点的主题(Spring AOP中正在执行的方法)具备给定注解的链接点。

其余切入点

完整的AspectJ切入点语言支持Spring不支持的其余切入点指示符:call, get, set, preinitialization,staticinitialization, initialization, handler, adviceexecution, withincode, cflow, cflowbelow, if, @this@withincode(备注:意思是Spring不支持这些指示符)。在Spring AOP解释的切入点表达式中使用这些切入点指示符会致使抛出IllegalArgumentException

Spring AOP支持的切入点指示符集合可能会在未来的版本中扩展,以支持更多的 AspectJ切入点指示符。

因为Spring AOP仅将匹配限制为仅方法执行链接点,所以前面对切入点指示符的讨论所给出的定义比在AspectJ编程指南中所能找到的要窄。此外,AspectJ自己具备基于类型的语义,而且在执行链接点处,thistarget都引用同一个对象:执行该方法的对象。Spring AOP是基于代理的系统,可区分代理对象自己(绑定到此对象)和代理背后的目标对象(绑定到目标)。

因为Spring的AOP框架基于代理的性质,所以根据定义,不会拦截目标对象内的调用。对于JDK代理,只能拦截代理上的公共接口方法调用。使用CGLIB,将拦截代理上的公共方法和受保护的方法调用(必要时甚至包可见的方法)。可是,一般应经过公共签名设计经过代理进行的常见交互。

请注意,切入点定义一般与任何拦截方法匹配。若是严格地将切入点设置为仅公开使用,即便在CGLIB代理方案中经过代理可能存在非公开交互,也须要相应地进行定义。

若是你的拦截须要在目标类中包括方法调用甚至构造函数,请考虑使用Spring驱动的本地AspectJ编织,而不是Spring的基于代理的AOP框架。这构成了具备不一样特征的AOP使用模式,所以在作出决定以前必定要熟悉编织。

Spring AOP还支持其余名为bean的PCD。使用PCD,能够将链接点的匹配限制为特定的命名Spring Bean或一组命名Spring Bean(使用通配符时)。Bean PCD具备如下形式:

bean(idOrNameOfBean)

idOrNameOfBean标记能够是任何Spring bean的名称。提供了使用*字符的有限通配符支持,所以,若是为Spring bean创建了一些命名约定,则能够编写bean PCD表达式来选择它们。与其余切入点指示符同样,bean PCD能够与&&(和)、|| (或)、和!(否认)运算符一块儿使用。

Bean PCD仅在Spring AOP中受支持,而在本地AspectJ编织中不受支持。它是AspectJ定义的标准PCD的特定于Spring的扩展,所以不适用于@Aspect模型中声明的切面。

Bean PCD在实例级别(基于Spring bean名称概念构建)上运行,而不是仅在类型级别(基于编织的AOP受其限制)上运行。基于实例的切入点指示符是Spring基于代理的AOP框架的特殊功能,而且与Spring bean工厂紧密集成,所以能够天然而直接地经过名称识别特定bean。

组合切入点表达式

你可使用&&||!组合切入点表达式。你还能够按名称引用切入点表达式。如下示例显示了三个切入点表达式:

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

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

@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {} //3
  1. 若是方法执行链接点表示任何公共方法的执行,则anyPublicOperation匹配。
  2. 若是交易模块中有方法执行,则inTrading匹配。
  3. 若是方法执行表明交易模块中的任何公共方法,则tradingOperation匹配。

最佳实践是从较小的命名组件中构建更复杂的切入点表达式,如先前所示。按名称引用切入点时,将应用常规的Java可见性规则(你能够看到相同类型的private切入点,层次结构中protected的切入点,任何位置的public切入点,等等)。可见性不影响切入点匹配。

共享通用切入点定义

在企业级应用中,开发人员一般但愿从多个方面引用应用程序的模块和特定的操做集。咱们建议为此定义一个 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的AOP支持中讨论了<aop:config><aop:advisor>元素。事务管理中讨论了事务元素。

实例

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

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

除了返回类型模式(前面的代码片断中的ret-type-pattern),名称模式(name-pattern)和参数模式(param-pattern)之外的全部部分都是可选的。返回类型模式肯定要匹配链接点、方法的返回类型必须是什么。最经常使用做返回类型模式。它匹配任何返回类型。仅当方法返回给定类型时,标准类型名称才匹配。名称模式与方法名称匹配。你能够将通配*符用做名称模式的所有或一部分。若是你指定了声明类型模式,请在其后加上.将其加入名称模式组件。参数模式稍微复杂一些:()匹配不带参数的方法,而(..)匹配任意数量(零个或多个)的参数。(*)模式与采用任何类型的一个参数的方法匹配。(*,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))不一样。若是在运行时传递的参数为Serializable,则args版本匹配;若是方法签名声明一个类型为Serializable的参数,则执行版本匹配。

  • 目标对象具备@Transactional注解的任何链接点(仅在Spring AOP中方法执行):

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

你也能够在绑定形式中使用@target。有关如何使注解对象在建议正文中可用的信息,请参见“声明通知”部分。

  • 目标对象的声明类型具备@Transactional注解的任何链接点(仅在Spring AOP中方法执行):

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

你也能够在绑定形式中使用@within。有关如何使注解对象在通知正文中可用的信息,请参见“声明通知”部分。

  • 任何执行方法带有@Transactional注解的链接点(仅在Spring AOP中是方法执行):

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

你也能够在绑定形式中使用@annotation。有关如何使注解对象在通知正文中可用的信息,请参见“声明通知”部分。

  • 任何采用单个参数的链接点(仅在Spring AOP中是方法执行),而且传递的参数的运行时类型具备@Classified注解:

    @args(com.xyz.security.Classified)

你也能够在绑定形式中使用@args。请参阅“声明通知”部分,如何使通知对象中的注解对象可用。

  • 名为tradeService的Spring bean上的任何链接点(仅在Spring AOP中执行方法):

    bean(tradeService)

  • Spring Bean上具备与通配符表达式* Service匹配的名称的任何链接点(仅在Spring AOP中才执行方法):

    bean(*Service)

写一个好的链接点

在编译期间,AspectJ处理切入点以优化匹配性能。检查代码并肯定每一个链接点是否(静态或动态)匹配给定的切入点是一个耗时的过程。(动态匹配意味着没法从静态分析中彻底肯定匹配,而且在代码中进行了测试以肯定在运行代码时是否存在实际匹配)。首次遇到切入点声明时,AspectJ将其重写为匹配过程的最佳形式。这是什么意思?基本上,切入点以DNF(析取范式)重写,而且对切入点的组件进行排序,以便首先检查那些较便宜(消耗最小)的组件。这意味着你没必要担忧理解各类切入点指示符的性能,而且能够在切入点声明中以任何顺序提供它们。

可是,AspectJ只能使用所告诉的内容。为了得到最佳的匹配性能,你应该考虑他们试图达到的目标,并在定义中尽量缩小匹配的搜索空间。现有的指示符天然分为三类之一:同类、做用域和上下文:

  • Kinded指示器选择特定类型的链接点:executiongetsetcallhandler

  • Scoping指示器选择一组感兴趣的链接点(多是多种类型的):withinwithincode

  • Contextual指示符根据上下文匹配(和可选绑定):thistarget@annotation

编写正确的切入点至少应包括前两种类型(KindedScoping)。你能够包括上下文指示符以根据链接点上下文进行匹配,也能够绑定该上下文以在通知中使用。仅提供Kinded的标识符或仅提供Contextual的标识符是可行的,可是因为额外的处理和分析,可能会影响编织性能(使用的时间和内存)。Scoping指定符的匹配很是快,使用它们意味着AspectJ能够很是迅速地消除不该进一步处理的链接点组。一个好的切入点应尽量包括一个切入点。

参考代码:com.liyong.ioccontainer.starter.AopIocContiner

5.4.4 声明通知

通知与切入点表达式关联,而且在切入点匹配的方法执行以前、以后或周围运行。切入点表达式能够是对命名切入点的简单引用,也能够是在适当位置声明的切入点表达式。

前置通知

你可使用@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() {
        // ...
    }

}

返回通知

在当匹配方法正常的执行返回时,返回通知运行。你可使用@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) {
        // ...
    }

}

返回属性中使用的名称必须与advice方法中的参数名称相对应。当方法执行返回时,返回值将做为相应的参数值传递到通知方法。returning也将匹配限制为仅返回指定类型值的方法执行(在这种状况下为Object,它匹配任何返回值)。

请注意,当使用返回后通知时,不可能返回彻底不一样的引用。

异常后置通知

在抛异常通知后,当匹配的方法执行经过抛出异常退出时运行。你可使用@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)的方法执行。

最终通知

当匹配的方法执行退出时,通知(最终)运行。经过使用@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注解来声明环绕通知。通知方法的第一个参数必须是ProceedingJoinPoint类型。在通知的正文中,在ProceedingJoinPoint上调用proceed()会使底层(真正的执行方法)方法执行。proceed方法也能够传入Object []。数组中的值用做方法执行时的参数。

当用Object []进行调用时,proceed的行为与AspectJ编译器所编译的around 通知的proceed为略有不一样。对于使用传统AspectJ语言编写的环绕通知,传递给proceed的参数数量必须与传递给环绕通知的参数数量(而不是基础链接点采用的参数数量)相匹配,而且传递给给定的参数位置会取代该值绑定到的实体的链接点处的原始值(不要担忧,若是这如今没有意义)。Spring采起的方法更简单,而且更适合其基于代理的,仅执行的语义。若是你编译为Spring编写的@AspectJ切面,并在AspectJ编译器和weaver中使用参数进行处理,则只须要意识到这种区别。有一种方法能够在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;
    }

}

环绕通知返回的值是该方法的调用者看到的返回值。例如,若是一个简单的缓存切面有一个值,则它能够从缓存中返回一个值,若是没有,则调用proceed()。请注意,在环绕通知的正文中,proceed可能被调用一次,屡次或彻底不被调用。全部这些都是合法的。

参考代码:com.liyong.ioccontainer.starter.AopIocContiner

通知参数

Spring提供了彻底类型化的通知,这意味着你能够在通知签名中声明所需的参数(如咱们先前在返回和抛出示例中所看到的),而不是一直使用Object []数组。咱们将在本节的后面部分介绍如何使参数和其余上下文值可用于通知主体。首先,咱们看一下如何编写通用通知,以了解该通知当前通知的方法。

获取当前JoinPoint

任何通知方法均可以将org.aspectj.lang.JoinPoint类型的参数声明为其第一个参数。请注意,环绕通知声明ProceedingJoinPoint类型为第一个参数,该参数是JoinPoint的子类。JoinPoint接口提供了许多有用的方法:

  • getArgs(): 返回方法参数。
  • getThis(): 返回代理对象。
  • getTarget(): 返回目标对象。
  • getSignature(): 返回通知使用的方法的描述。
  • toString(): 打印有关全部通知方法的有用描述。

有关更多详细信息,请参见javadoc

传递参数给通知

咱们已经看到了如何绑定返回的值或异常值(在返回以后和引起通知以后)。要使参数值可用于通知正文,可使用args的绑定形式。若是在args表达式中使用参数名称代替类型名称,则在调用通知时会将相应参数的值做为参数值传递。一个例子应该使这一点更清楚。假设你要通知以Account对象做为第一个参数的DAO操做的执行,而且你须要在通知正文中访问该账户。你能够编写如下内容:

@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注解的方法的执行并提取审计代码:

这两个示例中的第一个显示了@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();
    // ...
}

通知参数和泛型

Spring AOP能够处理类声明和方法参数中使用的泛型。假设你具备以下通用类型:

public interface Sample<T> {
    void sampleGenericMethod(T param);
    void sampleGenericCollectionMethod(Collection<T> param);
}

你能够经过在要拦截方法的参数类型中键入advice参数,将方法类型的拦截限制为某些参数类型:

@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
    // Advice implementation
}

这种方法不适用于泛型集合。所以,你不能按如下方式定义切入点:

@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
public void beforeSampleMethod(Collection<MyType> param) {
    // Advice implementation
}

为了使这项工做有效,咱们将不得不检查集合的每一个元素,这是不合理的,由于咱们也没法决定一般如何处理null。要实现相似的目的,你必须将参数键入Collection <?>并手动检查元素的类型。

肯定参数名称

通知调用中的参数绑定依赖于切入点表达式中使用的名称与通知和切入点方法签名中声明的参数名称的匹配。

经过Java反射没法得到参数名称,所以Spring AOP使用如下策略来肯定参数名称:

  • 若是用户已明确指定参数名称,则使用指定的参数名称。通知和切入点注解均具备可选的argNames属性,你可使用该属性来指定带注解的方法的参数名称。这些参数名称在运行时可用。如下示例显示如何使用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
}

若是第一个参数是JoinPointProceedingJoinPointJoinPoint.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
}

JoinPointProceedingJoinPointJoinPoint.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将尝试推断绑定变量与参数的配对(例如,若是切入点表达式中仅绑定了一个变量,而且advice方法仅接受一个参数,则配对很明显)。若是在给定可用信息的状况下变量的绑定不明确,则抛出AmbiguousBindingException

  • 若是以上全部策略均失败,则抛出IllegalArgumentException

proceed参数

前面咱们提到过,咱们将描述如何编写一个在Spring AOP和AspectJ中始终有效的参数的proceed调用。解决方案是确保通知签名按顺序绑定每一个方法参数。如下示例显示了如何执行此操做:

@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});
}

在许多状况下,不管如何都要进行此绑定(如上例所示)。

通知顺序

当多条通知都但愿在同一链接点上运行时会发生什么? Spring AOP遵循与AspectJ相同的优先级规则来肯定通知执行的顺序。优先级最高的通知在进入时首先运行(所以,给定两个before通知,优先级最高的通知首先运行)。在从链接点出来的过程当中,优先级最高的通知最后运行(所以,给定两个after通知,优先级最高的通知将排在第二)。

在不一样切面定义的两个通知都须要在同一个链接点上运行时,除非另行指定,不然执行顺序是未定义的。你能够经过指定优先级来控制执行顺序。经过在切面类中实现org.springframework.core.Ordered接口或使用Order注解对其进行注解,能够经过常规的Spring方法来完成。给定两个切面,从Ordered.getValue()(或注解值)返回较低值的切面具备较高的优先级

当在同一个切面中定义的两个通知都须要在同一个链接点上运行时,顺序是未定义的(由于没法经过java编译类的反射检索声明顺序)。考虑将此类通知方法分解为每一个切面类中的每一个链接点的一个通知方法,或者将通知片断重构为能够在切面级别排序的单独切面类。

5.4.5 引入

引入(在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");

参考代码:com.liyong.ioccontainer.starter.AopDeclareParentsIocContiner

5.4.6 切面实例化模型

这是一个高级主题。若是你刚开始使用AOP,则能够放心地跳过它,直到之后。

默认状况下,应用程序上下文中每一个切面都有一个实例。 AspectJ将此称为单例实例化模型。可使用bean生命周期来定义切面。Spring支持AspectJ的perthispertarget实例化模型(当前不支持percflowpercflowbelowpertypewithin)。

你能够经过在@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”的惟一对象)建立一个切面实例。切面实例是在服务对象上首次调用方法时建立的。当服务对象超出范围时,切面将超出范围。在建立切面实例以前,其中的任何通知都不会执行。一旦建立了切面实例,在其中声明的通知就会在匹配的链接点上执行,但仅当服务对象与此切面相关联时才执行。有关每一个子句的更多信息,请参见AspectJ编程指南。

pertarget实例化模型的工做方式与perthis彻底相同,可是它在匹配的链接点为每一个惟一目标对象建立一个切面实例。

5.4.7 AOP例子

如今你已经了解了全部组成部分是如何工做的,咱们能够将它们组合在一块儿作一些有用的事情。

有时因为并发问题(例如,死锁失败),业务服务的执行可能会失败。若是重试该操做,则极可能在下一次尝试中成功。对于适合在这种状况下重试的业务服务(不须要为解决冲突而须要返回给用户的幂等操做),咱们但愿透明地重试该操做,以免客户端看到PessimisticLockingFailureException。这是一个明显跨越服务层中的多个服务的需求,所以很是适合经过切面实现。

由于咱们想重试该操做,因此咱们须要使用环绕通知,以即可以屡次调用proceed。如下清单显示了基本切面的实现:

@Aspect
public class ConcurrentOperationExecutor implements Ordered {

    private static final int DEFAULT_MAX_RETRIES = 2;

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

    public void setMaxRetries(int maxRetries) {
        this.maxRetries = maxRetries;
    }

    public int getOrder() {
        return this.order;
    }

    public void setOrder(int order) {
        this.order = order;
    }

    @Around("com.xyz.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接口,以便咱们能够将切面的优先级设置为高于事务通知的优先级(每次重试时都须要一个新的事务)。maxRetriesorder属性均由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
}

而后,咱们可使用注解来注释服务操做的实现。切面更改成仅重试幂等操做涉及更改切入点表达式,以便仅@Idempotent操做匹配,以下所示:

@Around("com.xyz.myapp.SystemArchitecture.businessService() && " +
        "@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
    // ...
}

做者

我的从事金融行业,就任过易极付、思建科技、某网约车平台等重庆一流技术团队,目前就任于某银行负责统一支付系统建设。自身对金融行业有强烈的爱好。同时也实践大数据、数据存储、自动化集成和部署、分布式微服务、响应式编程、人工智能等领域。同时也热衷于技术分享创立公众号和博客站点对知识体系进行分享。关注公众号:青年IT男 获取最新技术文章推送!

博客地址: http://youngitman.tech

CSDN: https://blog.csdn.net/liyong1028826685

微信公众号:

技术交流群:

相关文章
相关标签/搜索