20200105 Spring官方文档(Core 5)

5.使用Spring进行面向切面编程

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

Spring的关键组件之一是AOP框架。尽管Spring IoC容器不依赖于AOP(这意味着您不需要使用AOP),但AOP是对Spring IoC的补充,以提供功能强大的中间件解决方案。

具有AspectJ切入点的Spring AOP
Spring通过使用基于模式的方法或@AspectJ批注样式,提供了编写自定义切面的简单而强大的方法 。这两种样式都提供了完全类型化的通知,并使用了AspectJ切入点语言,同时仍使用Spring AOP进行编织。

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

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

5.1。AOP概念

5.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 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注释风格的方法。

5.3。AOP代理

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

Spring AOP也可以使用CGLIB代理。这对于代理类而不是接口是必需的。默认情况下,如果业务对象未实现接口,则使用CGLIB。由于对接口而不是对类进行编程是一种好习惯,因此业务类通常实现一个或多个业务接口。在那些需要建议在接口上未声明的方法或需要将代理对象作为具体类型传递给方法的情况下(在极少数情况下),可以 强制使用CGLIB。

掌握Spring AOP是基于代理的这一事实很重要。

5.4。@AspectJ支持

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

5.4.1。启用@AspectJ支持

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

可以使用XML或Java样式的配置来启用@AspectJ支持。 无论哪种情况,您都需要确保AspectJ的Aspectjweaver.jar库位于应用程序的类路径(版本1.8或更高版本)上。 该库在AspectJ发行版的lib目录中或从Maven Central存储库中可用。

通过Java配置启用@AspectJ支持

要使用Java启用@AspectJ支持@Configuration,请添加@EnableAspectJAutoProxy 注释

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

}

通过XML配置启用@AspectJ支持

要通过基于XML的配置启用@AspectJ支持,请使用aop:aspectj-autoproxy 元素

<aop:aspectj-autoproxy/>

5.4.2。声明一个切面

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

您可以将方面类注册为Spring XML配置中的常规bean,也可以通过类路径扫描自动检测它们-与其他任何Spring管理的bean一样。 但是,请注意,@Aspect注释不足以在类路径中进行自动检测。 为此,您需要添加一个单独的@Component批注(或者,按照Spring的组件扫描程序的规则,有条件的自定义构造型批注)。

在Spring AOP中,方面本身不能成为其他方面的通知目标。@Aspect类上的注释将其标记为一个切面,因此将其从自动代理中排除。

5.4.3。声明切入点

切入点确定了感兴趣的连接点,从而使我们能够控制何时执行通知。 Spring AOP仅支持Spring Bean的方法执行连接点,因此您可以将切入点视为与Spring Bean上的方法执行匹配。 切入点声明由两部分组成:一个包含名称和任何参数的签名,以及一个切入点表达式,该切入点表达式准确确定我们感兴趣的方法执行。在AOP的@AspectJ批注样式中,常规方法定义提供了切入点签名 ,并使用@Pointcut批注指示切入点表达式(用作切入点签名的方法必须具有void返回类型)。

// 该切入点与任何名为transfer的方法的执行相匹配
@Pointcut("execution(* transfer(..))") // the pointcut expression
private void anyOldTransfer() {} // the pointcut signature

形成@Pointcut注释值的切入点表达式是一个常规的AspectJ 5切入点表达式。有关AspectJ的切入点语言的完整讨论,请参见AspectJ编程指南

支持的切入点指示符

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时方法的执行),其中传递的实际参数的运行时类型具有给定类型的注释。
  • @annotation:将匹配限制在连接点的主题(Spring AOP中正在执行的方法)具有给定注释的连接点上。

完整的AspectJ切入点语言支持未在Spring支持额外的切入点指示符:callgetsetpreinitializationstaticinitializationinitializationhandleradviceexecutionwithincodecflowcflowbelowif@this,和@withincode。在Spring AOP解释的切入点表达式中使用这些切入点指示符会导致IllegalArgumentException抛出异常。

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

由于Spring AOP仅将匹配限制为仅方法执行连接点,因此前面对切入点指示符的讨论所给出的定义比在AspectJ编程指南中所能找到的要窄。 此外,AspectJ本身具有基于类型的语义,并且在执行连接点处,此对象和目标都引用同一个对象:执行该方法的对象。 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() {} 

// 如果交易模块中有方法执行,则匹配。
@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>

示例

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))。如果在运行时传递的参数为Serializable,则args版本匹配 ,如果方法签名声明单个Serializable类型的参数,则exection版本匹配。

目标对象带有@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

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

bean(*Service)

写好切入点

现有的指示符自然分为三类之一:同类,作用域和上下文:

  • 同类指示符选择一种特殊的连接点:execution, get, set, callhandler
  • 作用域指示符选择一组感兴趣的连接点(可能是多种):withinwithincode
  • 上下文指示符根据上下文匹配(并可选地绑定):thistarget@annotation

5.4.4。声明通知(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() {
        // ...
    }

}

后置返回通知

后置返回通知,当匹配的方法执行正常返回时运行通知方法。您可以使用@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方法中的参数名称相对应。 当方法执行返回时,返回值将作为相应的参数值传递到通知方法。 返回子句也将匹配限制为仅返回指定类型值的方法执行(在这种情况下为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作异常类型),并将抛出的异常绑定到advice参数。以下示例显示了如何执行此操作:

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属性中使用的名称必须与advice方法中的参数名称相对应。 当通过抛出异常退出方法执行时,该异常将作为相应的参数值传递给通知方法。 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()会使目标方法执行。目标方法也可以以Object []传入。 数组中的值用作方法执行时的参数。

以下示例显示了如何使用环绕通知:

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在环绕通知的正文中可能调用一次,多次调用或根本不调用。所有这些都是合法的。

通知参数

Spring提供了完全类型化的通知,这意味着您可以在通知签名中声明所需的参数(如我们先前在返回和抛出示例中所看到的),而不是一直使用Object []数组。

访问当前 JoinPoint

任何通知方法都可以将org.aspectj.lang.JoinPoint类型的参数声明为它的第一个参数(请注意,环绕通知需要声明ProceedingJoinPoint类型的第一个参数,它是JoinPoint的子类。JoinPoint接口提供了一个 几种有用的方法:

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

将参数传递给通知

要使参数值可用于通知方法,可以使用args的绑定形式。 如果在args表达式中使用参数名称代替类型名称,则在调用通知时会将相应参数的值作为参数值传递。

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

编写此代码的另一种方法是声明一个切入点,当切入点与匹配点匹配时“提供” Account对象值,然后从通知中引用命名切入点。 如下所示:

@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}

@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
    // ...
}

代理对象(this),目标对象(target),和注解(@within@target@annotation,和@args)都可以以类似的方式绑定。接下来的两个示例展示了如何匹配使用@Auditable注解注释的方法的执行 并提取审计代码:

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


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

您可以通过在要拦截方法的参数类型中键入通知参数,将方法类型的拦截限制为某些参数类型:

@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<?>参数并手动检查元素的类型。

确定参数名称

通知调用中的参数绑定依赖于切入点表达式中使用的名称与通知和切入点方法签名中声明的参数名称的匹配。通过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
}

对JoinPoint,ProceedingJoinPoint和JoinPoint.StaticPart类型的第一个参数给予的特殊处理对于不收集任何其他联接点上下文的建议实例特别方便。 在这种情况下,您可以省略argNames属性。 例如,以下建议无需声明argNames属性:

@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
    // ... use jp
}

使用参数进行处理

前面我们提到过,我们将描述如何编写一个在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相同的优先级规则来确定建议执行的顺序。优先级最高的建议首先“向内走”运行(因此,给定两条优先建议,则优先级最高的建议首先运行)。从连接点“向外走”时,优先级最高的建议将最后运行(因此,给定两条后置通知,优先级最高的建议将在后面运行)。

当在不同切面定义的两条通知都需要在同一连接点上运行时,除非另行指定,否则执行顺序是不确定的。您可以通过指定优先级来控制执行顺序。通过在切面类中实现org.springframework.core.Ordered接口或使用Order批注对其进行注释,可以通过常规的Spring方法来完成。给定两个方面,从Ordered.getValue()(或注释值)返回较低值的方面具有较高的优先级。

当在相同切面定义的两个通知都需要在同一连接点上运行时,其顺序是未定义的(因为无法通过反射为javac编译的类检索声明顺序)。考虑将这些通知方法折叠为每个切面类中每个连接点的一个通知方法,或将通知重构为单独的切面类,您可以在切面级别进行排序。

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接口。注意,在前面示例的通知中,服务Bean可以直接用作UsageTracked接口的实现。如果以编程方式访问bean,则应编写以下内容:

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

5.4.6。切面实例化模型

默认情况下,应用程序上下文中每个切面都有一个实例。AspectJ将此称为单例实例化模型。可以使用备用生命周期来定义切面。Spring支持AspectJ的perthispertarget 实例化模型(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”的唯一对象)创建一个方面实例。 切面实例是在服务对象上首次调用方法时创建的。 当服务对象超出范围时,切面将超出范围。 在创建切面实例之前,其中的任何通知都不会执行。 创建切面实例后,在其中声明的通知将在匹配的连接点处执行,但是仅当服务对象是与此切面相关联的对象时才执行。 有关每个子句的更多信息,请参见AspectJ编程指南。

pertarget实例化模型的工作方式与perthis完全相同,但是它在匹配的连接点为每个唯一目标对象创建一个方面实例。

5.4.7。AOP示例

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

}
<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
}
@Around("com.xyz.myapp.SystemArchitecture.businessService() && " +
        "@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
    // ...
}

5.5。基于架构(Schema)的AOP支持

如果您更喜欢基于XML的格式,Spring还提供了对使用新aop名称空间标签定义方面的支持。支持与使用@AspectJ样式时完全相同的切入点表达式和通知类型。

在您的Spring配置中,所有AspectAdvisor元素必须放置在一个<aop:config>元素内(<aop:config>在应用程序上下文配置中可以有多个元素)。一个<aop:config>元素可以包含切点,Advisor和切面元件(注意这些必须按照这个顺序进行声明)。

<aop:config>的配置样式大量使用了Spring的自动代理机制。 如果您已经通过使用BeanNameAutoProxyCreator或类似方法使用显式自动代理,则可能会导致问题(例如,未编织通知)。 推荐的用法模式是仅使用<aop:config>样式或仅使用AutoProxyCreator样式,并且不要混合使用。

5.5.1。声明一个切面

使用模式支持时,方面是在Spring应用程序上下文中定义为Bean的常规Java对象。状态和行为在对象的字段和方法中捕获,切入点和通知信息在XML中捕获。

<aop:config>
    <aop:aspect id="myAspect" ref="aBean">
        ...
    </aop:aspect>
</aop:config>

<bean id="aBean" class="...">
    ...
</bean>

5.5.2。声明切入点

您可以在<aop:config>元素内声明命名的切入点,从而使切入点定义在多个切面和顾问程序之间共享。

<aop:config>

    <aop:pointcut id="businessService"
        expression="execution(* com.xyz.myapp.service.*.*(..))"/>

</aop:config>

组合切入点子表达式时,&&在XML文档中很尴尬,因此您可以分别使用andornot关键字代替&&||!。 例如,上一个切入点可以更好地编写如下:

<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样式所提供的更受限制。

5.5.3。声明通知

基于模式的AOP支持使用与@AspectJ样式相同的五种通知,并且它们具有完全相同的语义。

前置通知

<aop:aspect id="beforeExample" ref="aBean">

    <aop:before
        pointcut-ref="dataAccessOperation"
        method="doAccessCheck"/>

    ...

</aop:aspect>

在这里,dataAccessOperation是在最高(<aop:config>)级别定义的切入点的ID。 要定义内联切入点,请使用以下方法将pointcut-ref属性替换为pointcut属性:

<aop:aspect id="beforeExample" ref="aBean">

    <aop:before
        pointcut="execution(* com.xyz.myapp.dao.*.*(..))"
        method="doAccessCheck"/>

    ...

</aop:aspect>

method属性标识提供通知正文的方法(doAccessCheck)。 必须为包含通知的Aspect元素所引用的bean定义此方法。 在执行数据访问操作(与切入点表达式匹配的方法执行连接点)之前,将调用Aspect Bean上的doAccessCheck方法。

后置通知

<aop:aspect id="afterReturningExample" ref="aBean">

    <aop:after-returning
        pointcut-ref="dataAccessOperation"
        returning="retVal"
        method="doAccessCheck"/>

    ...

</aop:aspect>

使用returning属性指定返回值应传递到的参数的名称

public void doAccessCheck(Object retVal) {...

异常通知

<aop:aspect id="afterThrowingExample" ref="aBean">

    <aop:after-throwing
        pointcut-ref="dataAccessOperation"
        throwing="dataAccessEx"
        method="doRecoveryActions"/>

    ...

</aop:aspect>

使用throwing属性指定异常应传递到的参数的名称

public void doRecoveryActions(DataAccessException dataAccessEx) {...

后置通知

<aop:aspect id="afterFinallyExample" ref="aBean">

    <aop:after
        pointcut-ref="dataAccessOperation"
        method="doReleaseLock"/>

    ...

</aop:aspect>

环绕通知

<aop:aspect id="aroundExample" ref="aBean">

    <aop:around
        pointcut-ref="businessService"
        method="doBasicProfiling"/>

    ...

</aop:aspect>
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
    // start stopwatch
    Object retVal = pjp.proceed();
    // stop stopwatch
    return retVal;
}

通知参数

如果您希望显式指定通知方法的参数名称(不依赖于先前描述的检测策略),则可以通过使用advice元素的arg-names属性来实现,该属性的处理方式与argNames属性相同在通知注释中。

<aop:before
    pointcut="com.xyz.lib.Pointcuts.anyPublicMethod() and @annotation(auditable)"
    method="audit"
    arg-names="auditable"/>

arg-names属性接受以逗号分隔的参数名称列表。

通知顺序

切面之间的优先级是通过将Order批注添加到支持切面的Bean或通过使Bean实现Ordered接口来确定的。

5.5.4。引言(Introductions)

引言(在AspectJ中称为类型间声明)使方面可以声明通知的对象实现给定的接口,并代表那些对象提供该接口的实现。

您可以通过在aop:aspect中使用aop:declare-parents元素进行介绍。 您可以使用aop:declare-parents元素声明匹配类型具有新的父代(因此而得名)。 例如,给定一个名为UsageTracked的接口和该接口名为DefaultUsageTracked的实现,以下方面声明服务接口的所有实现者也都实现UsageTracked接口。 (例如,为了通过JMX公开统计信息。)

<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的类将包含以下方法:

public void recordUsage(UsageTracked usageTracked) {
    usageTracked.incrementUseCount();
}

要实现的接口由implement-interface属性确定。types-matching属性的值是AspectJ类型的模式。任何匹配类型的bean都实现该UsageTracked接口。注意,在前面示例的通知中,服务Bean可以直接用作UsageTracked接口的实现。要以编程方式访问bean,可以编写以下代码:

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

5.5.5。切面实例化模型

模式定义切面唯一受支持的实例化模型是单例模型。在将来的版本中可能会支持其他实例化模型。

5.5.6。顾问(Advisors)

“顾问”的概念来自Spring中定义的AOP支持,并且在AspectJ中没有直接等效的概念。 顾问就像一个独立的小切面,只有一条通知。 通知本身由bean表示,并且必须实现Spring的“通知类型”中描述的通知接口之一。 顾问可以利用AspectJ切入点表达式。

Spring通过<aop:advisor>元素支持顾问程序概念。 您通常会看到它与事务通知结合使用,事务通知在Spring中也有其自己的名称空间支持。 以下示例显示顾问程序:

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

5.6。选择要使用的AOP声明样式

5.6.1。Spring AOP还是Full AspectJ?

使用最简单的方法即可。Spring AOP比使用完整的AspectJ更简单,因为不需要在开发和构建过程中引入AspectJ编译器/编织器。如果您只需要通知在Spring bean上执行操作,则Spring AOP是正确的选择。如果您需要通知不受Spring容器管理的对象(通常是域对象),则需要使用AspectJ。如果您希望通知除简单方法执行之外的连接点(例如,字段get或设置连接点等),则还需要使用AspectJ。

使用AspectJ时,可以选择AspectJ语言语法(也称为“代码样式”)或`@AspectJ``注释样式。 显然,如果您不使用Java 5+,则已经为您做出了选择:使用代码样式。 如果切面在您的设计中起着重要作用,并且您能够使用用于Eclipse的AspectJ开发工具(AJDT)插件,则AspectJ语言语法是首选。 它更干净,更简单,因为该语言是专为编写方面而设计的。 如果您不使用Eclipse或只有少数几个方面在您的应用程序中不起作用,那么您可能要考虑使用@AspectJ样式,在IDE中坚持常规Java编译,并向其中添加方面编织阶段 您的构建脚本。

5.6.2。@AspectJ或Spring AOP的XML?

如果您选择使用Spring AOP,则可以选择@AspectJ或XML样式。

XML样式有两个缺点。首先,它没有完全将要解决的需求的实现封装在一个地方。DRY原则说,系统中的任何知识都应该有一个单一,明确,权威的表示形式。使用XML样式时,关于如何实现需求的知识会在配置文件中的后备bean类的声明和XML中分散。当您使用@AspectJ样式时,此信息将封装在一个模块中:方面。其次,与@AspectJ样式相比,XML样式在表达能力上有更多限制:仅支持“单例”方面实例化模型,并且无法组合以XML声明的命名切入点。例如,使用@AspectJ样式,您可以编写如下内容:

@Pointcut("execution(* get*())")
public void propertyAccess() {}

@Pointcut("execution(org.xyz.Account+ *(..))")
public void operationReturningAnAccount() {}

@Pointcut("propertyAccess() && operationReturningAnAccount()")

XML方法的缺点是您无法通过组合这些定义来定义accountPropertyAccess切入点。

@AspectJ样式支持其他实例化模型和更丰富的切入点组合。它具有将切面保持为模块化单元的优势。它还具有的优点是,Spring AOP和AspectJ都可以理解@AspectJ切面。因此,如果您以后决定需要AspectJ的功能来实现其他要求,则可以轻松地迁移到经典的AspectJ设置。总而言之,Spring团队在自定义切面更喜欢@AspectJ样式,而不是简单地配置企业服务。

5.7。混合切面类型

通过使用自动代理支持,模式定义的<aop:aspect>切面,<aop:advisor>声明的顾问程序,甚至使用相同配置中的其他样式的代理和拦截器,可以完美地混合@AspectJ样式切面。 所有这些都是通过使用相同的基础支持机制实现的,并且可以毫无困难地共存。

5.8。代理机制

Spring AOP使用JDK动态代理或CGLIB创建给定目标对象的代理。JDK动态代理内置在JDK中,而CGLIB是常见的开源类定义库(重新打包进spring-core)。

如果要代理的目标对象实现至少一个接口,则使用JDK动态代理。代理了由目标类型实现的所有接口。如果目标对象未实现任何接口,则将创建CGLIB代理。

如果要强制使用CGLIB代理(例如,代理为目标对象定义的每个方法,而不仅是由其接口实现的方法),都可以这样做。但是,您应该考虑以下问题:

  • 使用CGLIB,不能通知final方法,因为不能在运行时生成的子类中覆盖方法。
  • 从Spring 4.0开始,由于CGLIB代理实例是通过Objenesis创建的,因此不再调用代理对象的构造函数两次。仅当您的JVM不允许绕过构造函数时,您才可以从Spring的AOP支持中看到两次调用和相应的调试日志条目。

要强制使用CGLIB代理,请将<aop:config>元素的proxy-target-class属性值设置为true

<aop:config proxy-target-class="true">
    <!-- other beans defined here... -->
</aop:config>

要在使用@AspectJ自动代理支持时强制CGLIB代理,请将<aop:aspectj-autoproxy>元素的proxy-target-class属性设置为true,如下所示:

<aop:aspectj-autoproxy proxy-target-class="true"/>

多个<aop:config />部分在运行时折叠到一个统一的自动代理创建器中,该创建器将应用任何<aop:config />部分(通常来自不同的XML bean定义文件)指定的最强的代理设置。 这也适用于<tx:annotation-driven /><aop:aspectj-autoproxy />元素。

为了清楚起见,在<tx:annotation-driven /><aop:aspectj-autoproxy /><aop:config />元素上使用proxy-target-class ="true"会强制对所有三个元素使用CGLIB代理 其中。

5.9。以编程方式创建@AspectJ代理

除了使用<aop:config><aop:aspectj-autoproxy>来声明配置中的各个切面外,还可以通过编程方式创建建议目标对象的代理。

您可以使用org.springframework.aop.aspectj.annotation.AspectJProxyFactory类为一个或多个@AspectJ方面建议的目标对象创建代理。

// create a factory that can generate a proxy for the given target object
AspectJProxyFactory factory = new AspectJProxyFactory(targetObject);

// add an aspect, the class must be an @AspectJ aspect
// you can call this as many times as you need with different aspects
factory.addAspect(SecurityManager.class);

// you can also add existing aspect instances, the type of the object supplied must be an @AspectJ aspect
factory.addAspect(usageTracker);

// now get the proxy object...
MyInterfaceType proxy = factory.getProxy();

5.10。在Spring应用程序中使用AspectJ

到目前为止,本章介绍的所有内容都是纯Spring AOP。在本节中,我们将研究如果您的需求超出了Spring AOP所提供的功能,那么如何使用AspectJ编译器或weaver代替Spring AOP或除Spring AOP之外使用。

Spring附带了一个小的AspectJ切面库,该库在您的发行版中可以作为spring-aspects.jar独立使用。 您需要将其添加到类路径中才能使用其中的切面。

posted @ 2020-01-05 11:36  流星<。)#)))≦  阅读(433)  评论(0编辑  收藏  举报