5.4。@Aspectj的支持 【spring 核心技术 翻译】

5.4。@Aspectj的支持

@AspectJ引用了一种将切面声明为带有注释的常规Java类的样式。@AspectJ样式是由AspectJ项目作为AspectJ 5发行版的一部分引入的。Spring使用AspectJ提供的用于切入点解析和匹配的库来解释与AspectJ 5相同的注释。然而,AOP运行时仍然是纯粹的Spring AOP,并且不依赖于AspectJ编译器或weaver。使用AspectJ编译器和weaver可以使用完整的AspectJ语言,在与Spring应用程序一起使用AspectJ中对此进行了讨论。

5.4.1 启用@Aspectj的支持

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

可以通过XML或java风格的配置启用@AspectJ支持。无论哪种情况,都需要确保AspectJ的aspectjwever .jar库位于应用程序的类路径中(版本1.8或更高)。这个库可以在AspectJ发行版的lib目录中或从Maven中央存储库中获得。

使用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.4.2。声明一个切面

启用了@AspectJ支持后,在应用程序上下文中定义的带有@AspectJ切面(具有@Aspect注释)类的任何bean都将被Spring自动检测并用于配置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上方法的执行。切入点声明有两个部分:由名称和任何参数组成的签名,以及确切确定我们感兴趣的方法执行的切入点表达式。在@AspectJ注释风格的AOP中,切入点签名由常规方法定义提供,切入点表达式通过使用@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):

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

完整的AspectJ切入点语言支持Spring不支持的其他切入点指示器:调用、获取、设置、预初始化、静态初始化、初始化、处理程序、adviceexecution、withincode、cflow、cflowbelow、if、@this和@withincode。在由Spring AOP解释的切入点表达式中使用这些切入点指示器会导致抛出一个IllegalArgumentException。
Spring AOP支持的切入点指示器集可能在未来的版本中得到扩展,以支持更多的AspectJ切入点指示器。

因为Spring AOP将匹配限制为只匹配方法执行连接点,所以前面对切入点指示器的讨论给出了比AspectJ编程指南中更窄的定义。此外,AspectJ本身具有基于类型的语义,并且在一个执行连接点上,this和target都引用同一个对象:执行方法的对象。Spring AOP是一个基于代理的系统,区分了代理对象本身(绑定到此)和代理背后的目标对象(绑定到target)。

由于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工厂的紧密集成的一种特殊功能,在Spring bean工厂中,通过名称来识别特定的bean是自然而直接的。

结合切入点表达式

可以通过使用&&、||和!组合切入点表达式。您还可以通过名称引用切入点表达式。下面的例子显示了三个切入点表达式:

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

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

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

如果方法执行连接点表示任何公共方法的执行,则anyPublicOperation匹配。
如果一个方法执行在trading模块中,则匹配inTrading。
如果一个方法执行表示trading模块中的任何公共方法,则tradingOperation匹配。

从较小的已命名组件构建更复杂的切入点表达式是一种最佳实践,如前面所示。当通过名称引用切入点时,应用普通的Java可见性规则(您可以看到相同类型的私有切入点、层次结构中的受保护切入点、任何地方的公共切入点,等等)。可见性不影响切入点匹配。

共享公共切入点定义

在使用企业应用程序时,开发人员通常希望从几个切面引用应用程序的模块和特定的操作集。我们建议定义一个“系统体系结构”切面,它可以捕获为此目的的公共切入点表达式。这样的切面通常类似于下面的例子:

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.)
     * 假定你的 spring service beans 命名有始终如一的风格
     */
    @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(modifiers-pattern?ret-type-pattern declaring-type-pattern ?name-pattern(param-pattern)throws-pattern ?)

除了返回类型模式(前面代码片段中的ret-type-模式)、名称模式和参数模式之外,所有部分都是可选的。返回类型模式确定要匹配连接点,方法的返回类型必须是什么。是最常用的返回类型模式。它匹配任何返回类型。只有当方法返回给定类型时,全限定类型名称才匹配。名称模式与方法名称匹配。您可以使用通配符作为名称模式的全部或部分。如果指定了声明类型模式,则应包括尾部。将其连接到名称模式组件。parameters模式稍微复杂一些:()匹配不接受参数的方法,而(…)匹配任意数量(零或更多)的参数。模式匹配接受任意类型的一个参数的方法。匹配一个有两个参数的方法。第一个可以是任何类型,而第二个必须是字符串。有关更多信息,请参阅AspectJ编程指南的语言语义部分。
下面的例子展示了一些常见的切入点表达式:
任何公共方法的执行:

execution(public * *(..))
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)
    。。。

编写好的切入点

在编译期间,AspectJ处理切入点以优化匹配性能。检查代码并确定每个连接点是否(静态或动态)匹配给定的切入点是一个代价高昂的过程。(动态匹配意味着不能从静态分析完全确定匹配,并且在代码中放置一个测试来确定在代码运行时是否存在实际匹配)。在第一次遇到切入点声明时,AspectJ将其重写为用于匹配过程的最佳形式。这是什么意思?基本上,用DNF(析取范式)重写切入点,对切入点的组件进行排序,以便首先检查那些计算成本较低的组件。这意味着您不必担心理解各种切入点指示器的性能,并且可以在切入点声明中以任何顺序提供它们。
但是,AspectJ只能使用它被告知的内容。为了优化匹配性能,您应该考虑它们试图实现什么,并在定义中尽可能地缩小匹配的搜索空间。现有的指示器自然分为三组:kinded, scoping, and context:
Kinded指示器选择特定类型的连接点:执行、获取、设置、调用和处理程序。
作用域指示器选择一组感兴趣的连接点(可能是多种类型的):在within和withincode
上下文指示符根据上下文匹配(和可选绑定):this、target和@annotation

一个编写良好的切入点应该至少包括前两种类型(kinded和scoping)。您可以包含上下文指示符来基于连接点上下文进行匹配,或者绑定上下文以便在通知中使用。仅提供kinded指示符或仅提供上下文指示符可以工作,但由于额外的处理和分析,可能会影响编织性能(时间和内存使用)。范围指示符匹配起来非常快,使用它们意味着AspectJ可以很快地删除不应该进一步处理的连接点组。一个好的切入点应该尽可能包含一个切入点。

5.4.4 声明通知

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

前置通知

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

}

(译注:@Aspect 不能让类成为spring组件,需要增加 @Component, 多个@Before 怎么确定顺序?)

返回后置通知

在返回通知后,当匹配的方法执行正常返回时运行。你可以使用@Afterreturn注释来声明它:

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

}

可以有多个通知声明(以及其他成员),都在同一个切面内。在这些示例中,我们只展示了一个通知声明,以集中说明每个通知的效果。
有时候,您需要在通知正文中访问返回的实际值。您可以使用@Afterreturn的形式绑定返回值来获得访问,如下面的示例所示:

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

}

返回属性中使用的名称必须与通知方法中的参数名称对应。当一个方法执行返回时,返回值作为相应的参数值传递给通知方法。return子句还将匹配仅限于那些返回指定类型值的方法执行(在本例中是Object,它匹配任何返回值)。请注意,它是不可能返回一个完全不同的参考时,使用后返回建议。

异常后置通知

在抛出通知后,当匹配的方法执行通过抛出异常退出时运行。你可以使用@ afterthrow注解来声明它,如下面的例子所示:

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

}

通常,您希望仅在抛出给定类型的异常时才运行通知,而且您还经常需要访问通知主体中抛出的异常。可以使用抛出属性来限制匹配(如果需要,可以使用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) {
        // ...
    }

}

抛出属性中使用的名称必须与通知方法中参数的名称相对应。当方法执行通过抛出异常而退出时,异常将作为相应的参数值传递给通知方法。抛出子句还限制只匹配抛出指定类型异常(本例中为DataAccessException)的方法执行。

finally后置通知

当匹配的方法执行退出时,通知运行。它是使用@After注释声明的。After advice必须准备好处理正常和异常返回条件。它通常用于释放资源和类似的目的。下面的例子展示了如何使用after finally advice:

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通知。总是使用最弱的形式的建议来满足你的要求(也就是说,不要使用around advice,如果before advice可以做的话)。
Around通知是使用@Around注释声明的。通知方法的第一个参数的类型必须是ProceedingJoinPoint。在通知的主体中,对ProceedingJoinPoint调用proceed()将导致底层方法的执行。proceed方法还可以传入一个对象[]。数组中的值用作方法执行时的参数。

使用对象[]调用proceed时的行为与AspectJ编译器编译的proceed for around通知的行为略有不同。建议使用传统的AspectJ语言编写,左右进行传递的参数的数量必须匹配的参数传递到周围的建议(不是参数由底层连接点的数量),并继续在一个给定的参数传递的价值立场取代原来的价值实体价值的连接点是绑定到(不要担心如果现在没有意义)。Spring采用的方法更简单,更符合其基于代理的、仅执行的语义。如果您编译了为Spring编写的@AspectJ切面,并使用AspectJ编译器和weaver的proceed with参数,那么您只需要知道这种区别。有一种方法可以编写这种在Spring AOP和AspectJ之间100%兼容的切面,这将在下面的通知参数部分中讨论。
下面的例子展示了如何使用around advice:

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()。注意,在around通知的主体中,proceed可能被调用一次、多次,或者根本不被调用。所有这些都是合法的。

通知参数

Spring提供了完全类型的通知,这意味着您可以在通知签名中声明所需的参数(正如我们在前面的返回和抛出示例中看到的那样),而不是一直使用Object[]数组。在本节的后面,我们将看到如何使参数和其他上下文值对advice主体可用。首先,我们看一下如何编写通用的建议,以找出该通知目前正在通知的方法。

访问当前连接点

任何通知方法都可以声明一个org.aspectj.lang类型的参数作为它的第一个参数。JoinPoint(注意around通知需要声明一个类型为ProceedingJoinPoint的第一个参数,它是JoinPoint的一个子类。JoinPoint接口提供了许多有用的方法:
getArgs(): Returns the method arguments.
getThis(): Returns the proxy object.
getTarget(): Returns the target object.
getSignature(): Returns a description of the method that is being advised.
toString(): Prints a useful description of the method being advised.

向通知传递参数

我们已经了解了如何绑定返回值或异常值(在返回和抛出通知之后使用)。要使通知主体可用参数值,可以使用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注释的方法的执行,并提取审计代码:

两个示例中的第一个展示了@Auditable注释的定义:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
    AuditCode 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
}

要做到这一点,我们必须检查集合的每个元素,这是不合理的,因为我们也无法决定如何处理空值。要实现类似的功能,必须将参数键入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
}

如果第一个参数是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
}

对连接点的第一个参数、过程连接点和连接点的特殊处理。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方法只接受一个参数,那么这种配对是显而易见的)。如果给定可用信息,变量的绑定是不明确的,则抛出一个含糊不清的bindingexception。
如果上述所有策略都失败,则抛出一个IllegalArgumentException。

Proceeding with Arguments

我们前面提到过,我们将描述如何使用在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以正常的Spring方式完成的。切面类中的有序接口,或者使用@Order注释对其进行注释。对于两个切面,从order . getvalue()(或注释值)返回较低值的切面具有较高的优先级。

在Spring Framework 5.2.7中,在同一个@Aspect类中定义的需要在同一个连接点运行的通知方法的优先级是基于它们的通知类型,从最高到最低的优先级顺序分配的:@Around, @Before, @After, @ afterreturn, @AfterThrowing。但是请注意,由于Spring的AspectJAfterAdvice中的实现风格,在同一个切面中的任何@Afterreturn或@Afterthrow通知方法之后都会有效地调用@After advice方法。
当两块相同类型的建议(例如,两个@After建议方法)定义在相同的@Aspect类都需要运行在同一连接点,定是未定义的(因为没有办法获取源代码javac-compiled类的声明顺序通过反射)。考虑将此类通知方法分解为每个@Aspect类中的每个连接点的一个通知方法,或者将通知片段重构为单独的@Aspect类,您可以通过Ordered或@Order在切面级别对其进行排序。

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");

5.4.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’)创建一个切面实例。切面实例是在服务对象上第一次调用方法时创建的。当服务对象超出范围时,切面就超出了范围。在创建切面实例之前,其中的通知都不会执行。一旦创建了切面实例,在其中声明的通知就会在匹配的连接点上执行,但仅当服务对象与此切面相关联时才执行。有关per子句的更多信息,请参阅AspectJ编程指南。
pertarget实例化模型的工作方式与perthis完全相同,但是它在匹配的连接点上为每个唯一的目标对象创建一个切面实例。

5.4.7。AOP的例子

现在您已经了解了所有组成部分是如何工作的,我们可以将它们组合在一起做一些有用的事情。
业务服务的执行有时会由于并发问题(例如,死锁失败)而失败。如果重试该操作,很可能在下一次尝试时成功。对于适合在这种情况下重试的业务服务(幂等操作不需要返回用户以解决冲突),我们希望透明地重试操作,以避免客户机看到一个悲观lockingfailureexception异常。这是一个明显跨越服务层中的多个服务的需求,因此非常适合通过切面实现。
因为我们希望重试该操作,所以需要使用around通知,以便多次调用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;
    }

}

请注意,切面实现了有序接口,以便我们可以将切面的优先级设置为高于事务通知的优先级(每次重试时都需要一个新的事务)。maxRetries和order属性都由Spring配置。主要操作发生在围绕通知的doConcurrentOperation中。请注意,目前我们将重试逻辑应用于每个businessService()。我们尝试继续下去,如果因为一个悲观的lockingfailureexception异常失败了,我们会再次尝试,除非我们已经耗尽了所有的重试尝试。

对应的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 {
    // ...
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值