20220507 Core - 5. Aspect Oriented Programming with Spring

前言

文档地址

面向切面编程 (AOP) 提供另一种思考程序结构来补充面向对象编程 (OOP) 。OOP 中模块化的关键单位是类,而 AOP 中模块化的单位是切面。切面能够实现跨越多种类型和对象的关注点(例如事务管理)的模块化。(在 AOP 文献中这种关注点通常被称为 “横切” (crosscutting)关注点)

Spring 的关键组件之一就是 AOP 框架。虽然 Spring IoC 容器不依赖于 AOP(意味着如果您不想使用 AOP,则不需要使用 AOP),但 AOP 补充了 Spring IoC 以提供一个非常强大的中间件解决方案。

Spring 通过使用 基于 schema 的方法@AspectJ 注解模式 提供了编写自定义切面的方法 。这两种风格都提供了完全类型化的通知和 AspectJ 切点语言的使用,同时仍然使用 Spring AOP 进行编织。

本章讨论基于 schema 和 @AspectJ 的 AOP 支持。较低级别的 AOP 支持将在 Spring AOP APIs 讨论。

AOP 在 Spring 框架中用于:

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

AOP 概念

让我们从定义一些核心 AOP 概念和术语开始。这些术语不是特定于 Spring 的。不幸的是,AOP 术语并不是特别直观。但是,如果 Spring 使用自己的术语会更加混乱。

  • 切面(Aspect):跨多个类的关注点的模块化。事务管理是企业 Java 应用程序中横切关注点的一个很好的例子。在 Spring AOP 中,切面是通过使用常规类(基于 schema 的方法)或使用@Aspect 注解的常规类 (@AspectJ 风格)实现的。
  • 连接点(Join point):程序执行过程中的一个点,例如方法的执行或异常的处理。在 Spring AOP 中,一个连接点总是代表一次方法的执行。
  • 通知(或增强)(Advice):切面在特定连接点采取的行动。不同类型的通知包括 “around” 、“before” 和 “after” 建议。许多 AOP 框架,包括 Spring,将通知建模为拦截器,并在连接点周围维护一个拦截器链。
  • 切点(Pointcut):匹配连接点的判断。通知与切点表达式相关联,并在与切点匹配的任何连接点上运行(例如,执行具有特定名称的方法)。由切点表达式匹配的连接点的概念是 AOP 的核心,Spring 默认使用 AspectJ 切点表达式语言。
  • 引入(Introduction):代表类型声明额外的方法或字段。Spring AOP 允许您向任何被通知的对象引入新的接口(和相应的实现)。例如,您可以使用引入使 bean 实现 IsModified 接口,以简化缓存。
  • 目标对象(Target object):被一个或多个切面通知的对象,也称为“增强对象”。由于 Spring AOP 是使用运行时代理实现的,所以这个对象始终是一个被代理的对象。
  • AOP 代理(AOP proxy):由 AOP 框架创建的对象,用于实现切面契约(执行通知方法等)。在 Spring 中,AOP 代理是 JDK 动态代理或 CGLIB 代理。
  • 编织(Weaving):将切面与其他应用程序类型或对象联系起来以创建通知对象。这可以在编译时(例如,使用 AspectJ 编译器)、加载时或运行时完成。Spring AOP 与其他纯 Java AOP 框架一样,在运行时执行编织。

Spring AOP 包括以下类型的通知:

  • 前置通知(Before advice):在连接点之前运行的通知,但不能阻止执行流继续到连接点(除非它抛出异常)。
  • 后置返回通知(After returning advice):在连接点正常完成后运行的通知(例如,如果方法返回而没有抛出异常)。
  • 异常通知(After throwing advice):如果方法通过抛出异常退出,则运行此通知。
  • 后置通知(After (finally) advice):不管连接点退出的方式(正常或异常返回)都将运行的通知。
  • 环绕通知(Around advice):环绕连接点(例如方法调用)的通知。这是最有力的通知。环绕通知可以在方法调用前后执行自定义行为。它还负责选择是继续连接点还是通过返回自己的返回值或抛出异常来缩短通知的方法执行。

推荐使用最具体的通知而不是全部使用环绕通知。

所有通知参数都是静态类型的,因此您可以使用适当类型的通知参数(例如,方法执行返回值的类型)而不是 Object 数组。

切点匹配的连接点概念是 AOP 的关键,它区别于仅提供拦截的旧技术。切点使通知成为独立于面向对象层次结构的目标。例如,您可以将提供声明式事务管理的环绕通知应用于一组跨越多个对象的方法(例如服务层中的所有业务操作)。

Spring AOP 的能力和目标

Spring AOP 是用纯 Java 实现的。不需要特殊的编译过程。Spring AOP 不需要控制类加载器层次结构,因此适用于 servlet 容器或应用程序服务器。

Spring AOP 当前仅支持方法执行连接点(建议在 Spring bean 上执行方法)。没有实现字段拦截,尽管可以在不破坏核心 Spring AOP API 的情况下添加对字段拦截的支持。如果您需要增强字段访问和更新连接点,请考虑使用 AspectJ 之类的语言。

Spring AOP 的 AOP 方法与大多数其他 AOP 框架不同。目的不是提供最完整的 AOP 实现。相反,目的是提供 AOP 实现和 Spring IoC 之间的紧密集成,以帮助解决企业应用程序中的常见问题。

因此,Spring 的 AOP 功能通常与 Spring IoC 容器结合使用。切面是使用普通 bean 定义语法配置的(尽管这允许强大的“自动代理”功能)。这是与其他 AOP 实现的关键区别。无法使用 Spring AOP 轻松或高效地完成某些工作,例如增强非常细粒度的对象(通常是域对象)。在这种情况下,AspectJ 是最佳选择。但是,我们的经验是 Spring AOP 为适用于 AOP 的企业 Java 应用程序中的大多数问题提供了出色的解决方案。

Spring AOP 从不努力与 AspectJ 竞争以提供全面的 AOP 解决方案。我们相信,基于代理的框架(如 Spring AOP)和成熟的框架(如 AspectJ)都很有价值,而且它们是互补的,而不是相互竞争的。Spring 将 Spring AOP 和 IoC 与 AspectJ 无缝集成,以便在基于 Spring 的应用程序中启用 AOP 的所有用途。此集成不会影响 Spring AOP API 或 AOP Alliance API 。Spring AOP 保持向后兼容。

Spring 框架的核心原则之一是非侵入性原则。这就是您不应该被迫将特定于框架的类和接口引入您的业务或领域模型。但是,在某些地方,Spring Framework 确实为您提供了将特定于 Spring Framework 的依赖项引入代码库的选项。为您提供此类选项的理由是,在某些情况下,以这种方式阅读或编写某些特定功能可能会更容易。但是,Spring 框架(几乎)总是为您提供选择:您可以自由地做出明智的决定,选择哪个选项最适合您的特定用例或场景。

AOP 代理

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

Spring AOP 也可以使用 CGLIB 代理,这对于代理类而不是接口来说是必要的。默认情况下,如果业务对象未实现接口,则使用 CGLIB 。由于基于接口编程而不是基于类是一种很好的做法,因此业务类通常实现一个或多个业务接口。在需要增强未在接口上声明的方法或需要将代理对象作为具体类型传递给方法的情况下,可以 强制使用 CGLIB

@AspectJ 支持

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

使用 AspectJ 编译器和编织器可以使用完整的 AspectJ 语言,并在 将 AspectJ 与 Spring 应用程序一起使用中进行 了讨论。

启用 @AspectJ 支持

要在 Spring 配置中使用 @AspectJ 切面,您需要启用 Spring 支持以基于 @AspectJ 切面配置 Spring AOP ,并根据这些切面是否增强自动代理 bean。通过自动代理,如果 Spring 确定一个 bean 被一个或多个切面通知,它会自动为该 bean 生成一个代理来拦截方法调用并确保根据需要运行通知。

可以使用 XML 或 Java 的配置启用 @AspectJ 支持。在任何一种情况下,您还需要确保 AspectJ 的 aspectjweaver.jar 库位于您的应用程序的类路径中(版本 1.8 或更高)。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
使用 Java 配置启用 @AspectJ 支持

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

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

}
使用 XML 配置启用 @AspectJ 支持
<aop:aspectj-autoproxy />

声明切面

启用 @AspectJ 支持后,Spring 会自动检测到应用上下文中定义的任何 bean,其类是 @AspectJ 切面(具有 @Aspect 注解)并用于配置 Spring AOP

<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
    <!-- configure properties of the aspect here -->
</bean>
package org.xyz;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class NotVeryUsefulAspect {

}

切面(用 @Aspect 注解的类)可以有方法和字段,与任何其他类相同。它们还可以包含切点、通知和引入(类型间)声明。

@Aspect 注解不足以在类路径中进行自动检测,需要添加 @Component 注解

在 Spring AOP 中,切面本身不能成为其他切面增强的目标。@Aspect 类上的注解将其标记为切面,因此将其排除在自动代理之外。

声明切点

切点确定感兴趣的连接点,从而使我们能够控制通知何时运行。Spring AOP 仅支持 bean 的方法执行连接点,因此您可以将切点视为匹配 bean 上方法的执行。一个切点声明有两个部分:一个包含名称和任何参数的签名以及一个切点表达式,它确定我们对哪些方法执行感兴趣。

在 AOP 的 @AspectJ 注解样式中,切点签名由常规方法定义提供,切点表达式通过 @Pointcut 注解表示(作为切点签名的方法必须是 void 返回类型)。

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

构成 @Pointcut 注解值的切点表达式是一个正则 AspectJ 切点表达式。有关 AspectJ 切点语言的完整讨论,请参阅 AspectJ 编程指南

支持的切点指示符

Spring AOP 支持切点表达式使用以下 AspectJ 切点指示符 (PCD,Pointcut Designators) :

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

其他切点类型:完整的 AspectJ 切点语言支持 Spring 中不支持的其他切点指示符:callgetsetpreinitializationstaticinitializationinitializationhandleradviceexecutionwithincodecflowcflowbelowif@this@withincode 。在 Spring AOP 解释的切点表达式中使用这些切点指示符会导致抛出异常 IllegalArgumentException 。Spring AOP 支持的切点指示符集可能会在未来版本中扩展以支持更多的 AspectJ 切点指示符。

AspectJ 本身具有基于类型的语义,在执行的连接点,无论是 thistarget 指的是相同的对象:对象执行方法。Spring AOP 是一个基于代理的系统,它区分代理对象本身(绑定到 this )和代理背后的目标对象(绑定到 target )。

由于 Spring 的 AOP 框架基于代理的特性,根据定义,目标对象内的调用不会被拦截。对于 JDK 代理,只能拦截代理上的公共接口方法调用。使用 CGLIB,可以拦截代理上的公共和受保护方法调用(如果需要,甚至可以拦截包可见的方法)。但是,通过代理进行的常见交互应始终通过公共签名进行设计。

注意,切点定义通常与任何拦截的方法匹配。如果切点严格来说是仅公开的,即使是在 CGLIB 代理场景中,通过代理进行潜在的非公开交互,也需要相应地定义它。

如果您的拦截需要包括目标类中的方法调用甚至构造函数,请考虑使用 Spring 驱动的 原生 AspectJ 织入 ,而不是 Spring 的基于代理的 AOP 框架。这就构成了不同特点的不同 AOP 使用模式,因此在做出决定之前,一定要让自己熟悉编织。

Spring AOP 还支持一个名为 bean 的 PCD ,允许您将连接点的匹配限制为特定命名的 Spring bean 或一组命名的 Spring bean(使用通配符)。原生 AspectJ 不支持 bean PCD

bean(idOrNameOfBean)

idOrNameOfBean 可以是任何 Spring bean 的名字。提供了使用 * 字符的有限通配符支持,因此,如果您为 Spring bean 建立一些命名约定,您可以编写一个 bean 表达式来选择它们。与其他切点指示符的情况一样,bean PCD 也可以与 && (and) 、|| (or) 和 ! (negation) 运算符一起使用。

组合切点表达式

可以使用 &&||! 组合切点表达式。

还可以按名称引用切点表达式。

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

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

@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {} 
  • anyPublicOperation :匹配任何公共方法
  • inTrading :匹配 trading 模块中方法
  • tradingOperation :匹配 trading 模块中的任何公共方法

最佳实践是从较小的命名组件开始构建更复杂的切点表达式。当按名称引用切点时,适用正常的 Java 可见性规则(可以看到相同类型的私有切点、层次结构中的受保护切点、任何地方的公共切点,等等)。可见性不影响切点匹配。

共享公共切点定义

在使用企业应用程序时,开发人员通常希望从多个切面参考应用程序的模块和特定的操作集。为此,我们建议定义一个捕获常见切点表达式的切面 CommonPointcuts

package com.xyz.myapp;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class CommonPointcuts {

    /**
     * A join point is in the web layer if the method is defined
     * in a type in the com.xyz.myapp.web package or any sub-package
     * under that.
     */
    @Pointcut("within(com.xyz.myapp.web..*)")
    public void inWebLayer() {}

    /**
     * A join point is in the service layer if the method is defined
     * in a type in the com.xyz.myapp.service package or any sub-package
     * under that.
     */
    @Pointcut("within(com.xyz.myapp.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.myapp.dao package or any sub-package
     * under that.
     */
    @Pointcut("within(com.xyz.myapp.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.myapp.abc.service and com.xyz.myapp.def.service) then
     * the pointcut expression "execution(* com.xyz.myapp..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.myapp..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.myapp.dao.*.*(..))")
    public void dataAccessOperation() {}

}

您可以在任何需要切点表达式的地方引用此类方面中定义的切点。例如,要使服务层具有事务性,您可以编写以下内容:

<aop:config>
    <aop:advisor
        pointcut="com.xyz.myapp.CommonPointcuts.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 )、名称模式( name-pattern )和参数模式( param-pattern )之外的所有部分都是可选的。

  • * 最常用作返回类型模式。它匹配任何返回类型
  • 可以将 * 通配符用作名称模式的全部或部分。如果您指定声明类型模式,请包含一个尾随 . 以将其连接到名称模式组件。
  • 参数模式稍微复杂一点:
    • () 匹配一个不带参数的方法
    • (..) 匹配任意数量(零个或多个)的参数
    • (*) 匹配一个接受任何类型参数的方法
    • (*,String) 匹配带有两个参数的方法。第一个可以是任何类型,而第二个必须是String

一些常见的切点表达式:

描述 表达式
任何公共方法 execution(public * *(..))
任何名称以 set 开头的方法 execution(* set*(..))
AccountService 接口定义的任何方法 execution(* com.xyz.service.AccountService.*(..))
service 包中定义的任何方法 execution(* com.xyz.service.*.*(..))
service 包或其子包中定义的任何方法 execution(* com.xyz.service..*.*(..))
service 包内的任何连接点 within(com.xyz.service.*)
service 包或其子包中的任何连接点 within(com.xyz.service..*)
实现 AccountService 接口的代理的任何连接点 this(com.xyz.service.AccountService)
实现 AccountService 接口的目标对象的任何连接点 target(com.xyz.service.AccountService)
带有单个参数的任何连接点,并且在运行时传递的参数是 Serializable args(java.io.Serializable)

注意,本例中给出的切点不同于 execution(* *(java.io.Serializable)) ,如果在运行时传递的参数为 Serializable,则 args 版本匹配 ,如果方法签名声明类型为 Serializable 的单个参数,则 execution 版本匹配
具有 @Transactional 注解的目标对象的任何连接点 @target(org.springframework.transaction.annotation.Transactional)
目标对象的声明类型具有 @Transactional 注解的任何连接点 @within(org.springframework.transaction.annotation.Transactional)
任何连接点里 @Transactional 注解的方法 @annotation(org.springframework.transaction.annotation.Transactional)
任何带有单个参数的连接点,并且传递的参数的运行时类型具有 @Classified 注解 @args(com.xyz.security.Classified)
名为 tradeService 的 Spring bean 上的任何连接点 bean(tradeService)
名称与通配符表达式 *Service 匹配的 Spring bean 上的任何连接点 bean(*Service)
编写良好的切点

为了获得最佳匹配性能,您应该考虑切点试图实现的目标,并在定义中尽可能缩小匹配的搜索空间。现有的指示符分为三类:kindedscopingcontextual

  • kinded 指示符选择特定类型的连接点: executiongetsetcallhandler
  • scoping 指示符选择一组感兴趣的连接点(可能有多种):withinwithincode
  • contextual 指示符基于上下文匹配(和可选的绑定): thistarget@annotation

一个良好的切点应该至少包括前两种类型,scoping 指示符的匹配速度非常快,应该尽量使用。一个好的切入点应该尽可能只匹配一个目标。

声明通知

通知与切点表达式相关联,并在与切点匹配的方法执行之前、之后或环绕运行。切点表达式可以是对指定切点的简单引用,也可以是直接声明的切点表达式。

前置通知
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

    // 引用已定义的切点方法
    @Before("com.xyz.myapp.CommonPointcuts.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() {
        // ...
    }
}
后置返回通知
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

    @AfterReturning("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }
}

带返回值:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

    @AfterReturning(
        pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
        returning="retVal")
    public void doAccessCheck(Object retVal) {
        // ...
    }
}

返回属性中使用的名称必须对应于通知方法中参数的名称。returning 还将匹配返回值的类型

异常通知
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

    @AfterThrowing("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doRecoveryActions() {
        // ...
    }
}

抛出指定异常类型才执行通知:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

    @AfterThrowing(
        pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
        throwing="ex")
    public void doRecoveryActions(DataAccessException ex) {
        // ...
    }
}

throwing 属性使用的名称必须与通知方法中的参数名称对应。

@AfterThrowing 通知方法只应该从连接点(用户声明的目标方法)本身接收异常,而不是从伴随的 @After / @AfterReturning 方法接收异常 。

后置通知

后置通知必须准备好处理正常和异常返回条件。它通常用于释放资源和类似目的。

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;

@Aspect
public class AfterFinallyExample {

    @After("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doReleaseLock() {
        // ...
    }
}

注意,AspectJ 中的 @After 通知定义为 “after finally advice” ,类似于 try-catch 语句中的 finally 块。它将对从连接点(用户声明的目标方法)返回的任何结果、正常返回或异常进行调用,而 @AfterReturning 只适用于成功的正常返回。

环绕通知

环绕通知“环绕”匹配方法的执行。它有机会在方法运行之前和之后进行工作,并确定方法何时、如何、甚至是否真正开始运行。

如果您需要以 线程安全 的方式(例如,启动和停止计时器)在方法执行前后共享状态,则通常使用环绕通知。

始终使用满足您要求的最不强大的通知形式(即,如果 before 通知可以,请不要使用 around 通知)。

环绕通知是通过使用 @Around 注解来声明的。建议方法的第一个参数的类型必须为 ProceedingJoinPoint 。在通知的主体中,对 ProceedingJoinPoint 调用 proceed() 会导致匹配方法运行 。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.CommonPointcuts.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.CommonPointcuts.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
    // ...
}

args(account,..) 切点表达式有两个目的。首先,它限制了匹配,只有那些方法执行所述方法采用至少一个参数,并传递给该参数是的一个 Account 实例。其次,通过 account 参数使实际 Account 对象可用于通知。

另一种写法是声明一个切点,当它匹配一个连接点时提供 Account 对象值,然后从通知中引用命名的切点。

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

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

代理对象( this ),目标对象( target ),和注解( @within@target@annotation@args )都可以以类似的方式结合。

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

为了实现这一点,我们必须检查集合的每个元素,这是不合理的,因为我们也无法决定如何处理 null 值。要实现与此类似的功能,您必须出入 Collection<?> 参数并手动检查元素的类型。

确定参数名称

通知调用中的参数绑定依赖于切点表达式中使用的名称与通知和切点方法签名中声明的参数名称的匹配。参数名称不能通过 Java 反射获得,因此 Spring AOP 使用以下策略来确定参数名称:

  • 如果用户已明确指定参数名称,则使用指定的参数名称。通知和切点注解都有一个可选的 argNames 属性,您可以使用它来指定被注解方法的参数名称。这些参数名称在运行时可用。

    @Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
            argNames="bean,auditable")
    public void audit(Object bean, Auditable auditable) {
        AuditCode code = auditable.value();
        // ... use code and bean
    }
    

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

    @Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
    public void audit(JoinPoint jp) {
        // ... use jp
    }
    
  • 如果没有指定 argNames 属性,Spring AOP 会查看类的调试信息,并尝试从局部变量表中确定参数名。至少 -g:vars

  • 如果代码是在没有必要的调试信息的情况下编译的,Spring AOP 会尝试推断绑定变量与参数之间的配对关系(例如,如果切点表达式中只绑定了一个变量,而通知方法只采用一个参数,那么配对就是显而易见的)。如果根据可用的信息,变量的绑定是不明确的,则会抛出异常 AmbiguousBindingException

  • 如果上面所有策略都失败,就会抛出 IllegalArgumentException

带参数的继续处理( Proceed )

如何使用参数编写 proceed 调用,解决方案是确保通知签名按顺序绑定每个方法参数

@Around("execution(List<Account> find*(..)) && " +
        "com.xyz.myapp.CommonPointcuts.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 相同的优先级规则来确定通知执行的顺序。on the way in 时,最高优先级的通知首先运行(因此,给定两个前置通知,具有最高优先级的通知最先运行)。on the way out ,最高优先级通知最后运行(因此,给定两个后置通知,具有最高优先级的将最后运行)。

当在不同切面定义的两个通知都需要在同一个连接点运行时,除非您另外指定,否则执行顺序是未定义的。您可以通过指定优先级来控制执行顺序。这是通过在切面类中实现 org.springframework.core.Ordered 接口或使用 @Order 注解对其进行注解。给定两个切面,从 Ordered.getOrder() (或注解值)返回 较低值的切面具有更高的优先级

特定切面的每个不同通知类型在概念上都应该直接应用于连接点。因此,@AfterThrowing 通知方法不应该从 @After / @AfterReturning 方法接收异常。

在 Spring 5.2.7 中,在同一个 @Aspect 类中定义的、需要在同一个连接点上运行的通知方法,根据它们的通知类型按以下顺序分配优先级,从高到低:@Around , @Before , @After , @AfterReturning , @AfterThrowing

注意,在同一切面中的 @AfterReturning@AfterThrowing 通知方法之后,将调用 @After 通知方法。

当在同一 @Aspect 类中定义的两个相同类型的通知(例如,两个 @After 通知方法)都需要在同一个连接点上运行时,顺序是未定义的(因为没有办法通过 javac 编译类的反射来检索源代码声明顺序)。考虑在每个 @Aspect 类的每个连接点上将这些通知方法合并为一个通知方法,或者将通知片段重构为单独的 @Aspect 类,您可以通过 Ordered@Order 在切面级别上排序

引入( Introductions )

引入(在 AspectJ 中称为类型间声明)使切面能够声明被通知的对象实现给定的接口,并代表这些对象提供该接口的实现。

可以使用 @DeclareParents 注解进行引入。此注解用于声明匹配类型具有新的父类型(因此得名)。例如,给定一个名为 UsageTracked 的接口和名为 DefaultUsageTracked 的接口的实现 ,以下切面声明服务接口的所有实现者也实现了 UsageTracked 接口:

@Aspect
public class UsageTracking {

    @DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
    public static UsageTracked mixin;

    @Before("com.xyz.myapp.CommonPointcuts.businessService() && this(usageTracked)")
    public void recordUsage(UsageTracked usageTracked) {
        usageTracked.incrementUseCount();
    }

}

要实现的接口由注解字段的类型决定。@DeclareParents 注解的 value 属性是一个 AspectJ 类型的模式。任何匹配类型的 bean 都实现了 UsageTracked 接口。注意,在示例的前置通知中,服务 bean 可以直接用作 UsageTracked 接口的实现。如果以编程方式访问 bean,您将编写以下内容:

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

切面实例化模型

默认情况下,应用上下文中的每个切面都有一个实例。AspectJ 将此称为单例实例化模型。可以定义具有替代生命周期的切面。Spring 支持 AspectJ perthispertarget 实例化模型;目前不支持 percflowpercflowbelowpertypewithin

可以通过在 @Aspect 注解中指定 perthis 子句来声明 perthis 切面。考虑以下示例:

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

    private int someState;

    @Before("com.xyz.myapp.CommonPointcuts.businessService()")
    public void recordServiceUsage() {
        // ...
    }
}

在前面的示例中,perthis 子句的效果是为每个执行业务服务的唯一 service 对象(每个唯一对象绑定到切点表达式匹配的连接点 this )创建一个切面实例。切面实例是第一次在 service 对象上调用方法时创建的。当 service 对象超出作用域时,切面超出作用域。在创建切面实例之前,其中的任何通知都不会运行。一旦创建了切面实例,其中声明的通知就会在匹配的连接点运行,但前提是 service 对象是与切面相关联的对象。

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

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

为了优化切面以使其仅重试幂等操作,可以定义以下 Idempotent 注解:

@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    // marker annotation
}

然后我们可以注解服务操作。将切面更改为仅重试幂等操作细化改进切点表达式,仅 @Idempotent 操作匹配,如下所示:

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

基于模式(schema)的 AOP 支持

如果您更喜欢基于 XML 的格式,Spring 支持使用 aop 命名空间标签定义切面。支持与使用 @AspectJ 风格时完全相同的切点表达式和通知类型

在 Spring 配置中,所有切面(aspect )和顾问(advisor)标签都必须放置在 <aop:config> 标签中(可以在一个应用上下文配置中有多个 <aop:config> 标签)。 <aop:config> 标签可以包含切点(pointcut)、顾问(advisor)和切面(aspect )标签(注意,必须按该顺序声明这些元素)。

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

声明切面

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

使用 <aop:aspect> 标签声明切面

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

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

声明切点

切点定义可以在多个切面和顾问之间共享

<aop:config>

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

</aop:config>

切点表达式本身使用与 @AspectJ 支持中 描述的相同的 AspectJ 切点表达式语言。如果您使用基于模式的声明样式,则可以在切点表达式中引用以类型 (@Aspects) 定义的命名切点。假设您有一个 CommonPointcuts 切面,如共享通用切点定义中所述,定义上述切点的另一种方法如下:

<aop:config>

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

</aop:config>

在切面内声明切点与声明顶级切点非常相似

<aop:config>

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

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

        ...
    </aop:aspect>

</aop:config>

@AspectJ 切面非常相似,使用基于模式的定义样式声明的切点可以收集连接点上下文。例如,以下切点收集 this 对象作为连接点上下文并将其传递给通知:

<aop:config>

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

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

        <aop:before pointcut-ref="businessService" method="monitor"/>

        ...
    </aop:aspect>

</aop:config>

必须通过包含匹配名称的参数来声明通知以接收收集的连接点上下文,如下所示:

public void monitor(Object service) {
    // ...
}

当需要连接子表达式,&amp;&amp; 是 XML 文档中的尴尬,这样你就可以使用 andor 以及 not 关键字代替 &&||! 。例如,前面的切点可以更好地写成如下:

<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 样式提供的支持更有限。

声明通知

前置通知
<aop:aspect id="beforeExample" ref="aBean">

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

    ...

</aop:aspect>

这里,dataAccessOperation 是在顶部 <aop:config> 级别定义的切点的 id。为了定义内联切点,用 pointcut 属性替换 pointcut-ref 属性:

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

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

    ...
</aop:aspect>
后置返回通知

存在方法返回值

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

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

    ...
</aop:aspect>
public void doAccessCheck(Object retVal) {...
异常通知
<aop:aspect id="afterThrowingExample" ref="aBean">

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

    ...
</aop:aspect>
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;
}
通知参数

基于模式的声明风格以与 @AspectJ 支持 相同的方式支持完全类型化的通知——通过名称匹配切点参数与通知方法参数。如果您希望为通知方法显式指定参数名称(不依赖于前面描述的检测策略),您可以使用通知元素的 arg-names 属性来实现,该属性的处理方式与通知注解中的 argNames 属性相同

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

示例:与许多强类型参数结合使用的环绕通知:

package x.y.service;

public interface PersonService {

    Person getPerson(String personName, int age);
}

public class DefaultPersonService implements PersonService {

    public Person getPerson(String name, int age) {
        return new Person(name, age);
    }
}
package x.y;

import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;

public class SimpleProfiler {

    public Object profile(ProceedingJoinPoint call, String name, int age) throws Throwable {
        StopWatch clock = new StopWatch("Profiling for '" + name + "' and '" + age + "'");
        try {
            clock.start(call.toShortString());
            return call.proceed();
        } finally {
            clock.stop();
            System.out.println(clock.prettyPrint());
        }
    }
}
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- this is the object that will be proxied by Spring's AOP infrastructure -->
    <bean id="personService" class="x.y.service.DefaultPersonService"/>

    <!-- this is the actual advice itself -->
    <bean id="profiler" class="x.y.SimpleProfiler"/>

    <aop:config>
        <aop:aspect ref="profiler">

            <aop:pointcut id="theExecutionOfSomePersonServiceMethod"
                expression="execution(* x.y.service.PersonService.getPerson(String,int))
                and args(name, age)"/>

            <aop:around pointcut-ref="theExecutionOfSomePersonServiceMethod"
                method="profile"/>

        </aop:aspect>
    </aop:config>

</beans>
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import x.y.service.PersonService;

public final class Boot {

    public static void main(final String[] args) throws Exception {
        BeanFactory ctx = new ClassPathXmlApplicationContext("x/y/plain.xml");
        PersonService person = (PersonService) ctx.getBean("personService");
        person.getPerson("Pengo", 12);
    }
}

输出结果:

StopWatch 'Profiling for 'Pengo' and '12'': running time (millis) = 0
-----------------------------------------
ms     %     Task name
-----------------------------------------
00000  ?  execution(getFoo)
通知顺序

切面之间的优先级是通过 <aop:aspect> 标签中的 order 属性确定的,或者通过向支持切面的 bean 添加 @Order 注解,或者通过让 bean 实现 Ordered 接口来确定的。

与定义在同一个 @Aspect 类中的通知方法的优先级规则不通,当定义在同一个 <aop:aspect> 标签中的两个通知都需要在同一个连接点运行时,优先级由通知标签在 <aop:aspect> 元素中声明的顺序决定,从高到低的优先级。这里是因为通过反射拿到类中所有方法时,不能保证顺序,而在读取 XML 时,可以确定顺序。

例如,给定一个 around 通知和在同一 <aop:aspect> 标签中定义的 before 通知应用于同一连接点,为了确保 around 通知具有比 before 通知更高的优先级,<aop:around> 标签必须在 <aop:before> 标签之前声明。

作为一般经验法则,如果您发现在同一个 <aop:aspect> 标签中定义了多个通知并应用于同一个连接点,请考虑将这些通知方法折叠为每个 <aop:aspect> 标签中每个连接点的一个通知方法,或者重构这些通知分成单独的 <aop:aspect> 标签,您可以在切面级别为这些通知排序。

引入

<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.CommonPointcuts.businessService()
            and this(usageTracked)"
            method="recordUsage"/>

</aop:aspect>
public void recordUsage(UsageTracked usageTracked) {
    usageTracked.incrementUseCount();
}

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

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

切面实例化模型

模式定义切面唯一支持的实例化模型是单例模型。

未来版本可能支持其他实例化模型。

顾问(Advisor)

“顾问”的概念来自 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>

除了前面示例中使用的 pointcut-ref 属性外,您还可以使用 pointcut 属性来定义内联切点表达式。

要定义顾问的优先级以便通知可以参与排序,请使用 order 属性来定义顾问的 Ordered

AOP 模式示例

@AspectJ 示例等效

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

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

    <aop:aspect id="concurrentOperationRetry" ref="concurrentOperationExecutor">

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

        <aop:around
            pointcut-ref="idempotentOperation"
            method="doConcurrentOperation"/>

    </aop:aspect>

</aop:config>

<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
}
<aop:pointcut id="idempotentOperation"
        expression="execution(* com.xyz.myapp.service.*.*(..)) and
        @annotation(com.xyz.myapp.service.Idempotent)"/>

选择要使用的 AOP 声明样式

一旦您确定一个切面是实现给定需求的最佳方法,您如何在使用 Spring AOP 或 AspectJ 以及在 Aspect 语言(代码)样式、@AspectJ 注解样式或 Spring XML 样式之间做出决定?这些决策受到许多因素的影响,包括应用程序需求、开发工具和团队对 AOP 的熟悉程度

Spring AOP 还是 Full AspectJ?

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

Spring AOP 配置的 @AspectJ 或 XML?

使用 XML 样式,可以集中管理切面配置。

XML 样式有两个缺点。首先,它没有在一个地方完全封装它所解决的需求的实现。DRY 原则(Don't Repeat Yourself)认为,系统内的任何知识都应该有一个单一的、明确的、权威的表示。使用 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()")
public void accountPropertyAccess() {}
<aop:pointcut id="propertyAccess"
        expression="execution(* get*())"/>

<aop:pointcut id="operationReturningAnAccount"
        expression="execution(org.xyz.Account+ *(..))"/>

XML 方法的缺点是不能通过组合这些定义来定义 accountPropertyAccess 切点。

@AspectJ 风格支持额外的实例化模型和更丰富的切点组合。它具有将切面保持为模块化单元的优点。它还具有以下优点:Spring AOP 和 AspectJ 都可以理解 @AspectJ 切面。因此,如果您稍后决定需要 AspectJ 的功能来实现其他要求,您可以轻松迁移到经典的 AspectJ 设置。

总的来说,除了企业服务的简单配置之外,Spring 团队更喜欢 @AspectJ 风格的自定义切面。

混合切面类型

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

代理机制

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 代理。

理解 AOP 代理

Spring AOP 是基于代理的。

首先考虑您有一个普通的、未代理的、直接的对象引用的场景,如以下代码片段所示:

public class SimplePojo implements Pojo {

    public void foo() {
        // this next method invocation is a direct call on the 'this' reference
        this.bar();
    }

    public void bar() {
        // some logic...
    }
}

如果您在对象引用上调用方法,则会直接在该对象引用上调用该方法,如下图和清单所示:

aop 代理普通 pojo 调用

public class Main {

    public static void main(String[] args) {
        Pojo pojo = new SimplePojo();
        // this is a direct method call on the 'pojo' reference
        pojo.foo();
    }
}

当客户端代码具有的引用是代理时,情况略有变化。考虑以下图表和代码片段:

aop 代理调用

public class Main {

    public static void main(String[] args) {
        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.addInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());

        Pojo pojo = (Pojo) factory.getProxy();
        // this is a method call on the proxy!
        pojo.foo();
    }
}

这里要理解的关键是 Main 类的 main(..) 方法内的客户端代码具有对代理的引用。这意味着对该对象引用的方法调用是对代理的调用。因此,代理可以委托与该特定方法调用相关的所有拦截器(通知)。但是,一旦调用最终到达目标对象(在本例中为 SimplePojo 引用),它可能对自身进行的任何方法调用(例如 this.bar()this.foo() )都将针对 this 引用而不是代理调用。这具有重要意义。

这意味着自调用不会导致与方法调用相关的通知有机会运行。

最好的方法是重构你的代码,这样自调用就不会发生。这确实需要您做一些工作,但这是最好的、侵入性最小的方法。

不推荐的方法是将类中的逻辑完全绑定到 Spring AOP,如以下示例所示:

public class SimplePojo implements Pojo {

    public void foo() {
        // this works, but... gah!
        ((Pojo) AopContext.currentProxy()).bar();
    }

    public void bar() {
        // some logic...
    }
}

这将您的代码完全耦合到 Spring AOP,并且它使类感知到它正在 AOP 上下文中使用的事实,这与 AOP 背道而驰。要想这样使用,创建代理时还需要一些额外的配置,如以下示例所示:

public class Main {

    public static void main(String[] args) {
        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.addInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());
        factory.setExposeProxy(true);

        Pojo pojo = (Pojo) factory.getProxy();
        // this is a method call on the proxy!
        pojo.foo();
    }
}

最后,必须指出的是,AspectJ 没有这种自调用问题,因为它不是基于代理的 AOP 框架

编程方式创建 @AspectJ 代理

可以使用 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();

在 Spring 应用中使用 AspectJ

Spring 附带了一个小的 AspectJ 切面库,spring-aspects.jar ,您需要将其添加到类路径中才能使用其中的切面

使用 AspectJ 在 Spring 中依赖注入域对象

spring-aspects.jar 包含一个注解驱动的切面,它利用这个能力来允许任何对象的依赖注入。该支持用于在任何容器的控制之外创建的对象。域对象通常属于这一类,因为它们通常是通过 new 操作符或 ORM 工具作为数据库查询的结果以编程方式创建的 。

@Configurable 注解标记一个类为通过 Spring 驱动的配置。在最简单的情况下,可以纯粹用作标记注解

package com.xyz.myapp.domain;

import org.springframework.beans.factory.annotation.Configurable;

@Configurable
public class Account {
    // ...
}

当以这种方式用作标记接口时,Spring 通过使用与完全限定类型名称 ( com.xyz.myapp.domain.Account ) 同名的 bean 定义(通常为原型作用域)来配置带注解类型(在本例中为 Account )的新实例。由于 bean 的默认名称是其类型的完全限定名称,因此声明原型定义的一种简便方法是省略 id 属性,如以下示例所示:

<bean class="com.xyz.myapp.domain.Account" scope="prototype">
    <property name="fundsTransferService" ref="fundsTransferService"/>
</bean>

如果要显式指定要使用的原型 bean 定义的名称,可以直接在注解中执行此操作,如以下示例所示:

package com.xyz.myapp.domain;

import org.springframework.beans.factory.annotation.Configurable;

@Configurable("account")
public class Account {
    // ...
}

Spring 现在查找名为 account 的 bean 定义,并将其用作配置新 Account 实例的定义。

还可以使用自动装配来完全避免指定专用 bean 定义。要让 Spring 应用自动装配,使用 @Configurable 注解的 autowire 属性。您可以分别按类型 @Configurable(autowire=Autowire.BY_TYPE) 或名称指定 @Configurable(autowire=Autowire.BY_NAME) 用于自动装配。作为替代方案,最好通过 @Autowired@Inject 在字段或方法级别为您的 @Configurable bean 指定显式的、注解驱动的依赖注入

最后,您可以使用 dependencyCheck 属性(例如, @Configurable(autowire=Autowire.BY_NAME,dependencyCheck=true) )为 新创建和配置的对象中的对象引用启用 Spring 依赖项检查。如果此属性设置为 true ,Spring 会在配置后验证所有属性(除了基础类型或集合)都已设置。

请注意,单独使用注解没有任何作用。 spring-aspects.jar 里的 AnnotationBeanConfigurerAspect 作用于注解。本质上,切面是指,“从一个用 @Configurable 注解的类型的新对象的初始化返回后,根据注解的属性使用 Spring 配置新创建的对象”。在这种情况下,“初始化”是指新实例化的对象(例如,使用 new 运算符实例化的对象)以及正在进行反序列化的 Serializable 对象。

在大多数情况下,“从新对象的初始化返回后” 的确切语义是好的。在这种情况下,“初始化之后” 意味着在对象被构造之后注入依赖项。这意味着依赖项不可用于类的构造函数体。如果您希望在构造函数运行之前注入依赖项,从而可以在构造函数体中使用,则需要在 @Configurable 声明中定义它 ,如下所示:

@Configurable(preConstruction = true)

为此,必须使用 AspectJ 编织器编织带注解的类型。您可以使用构建时 Ant 或 Maven 任务来执行此操作(例如,参见 AspectJ 开发环境指南)或加载时编织(参见 Spring Framework 中的 AspectJ 加载时编织)。需要由 Spring (以获取对用于配置新对象的 bean 工厂的引用)配置 AnnotationBeanConfigurerAspect 。如果使用基于 Java 的配置,则可以添加 @EnableSpringConfigured 到任何 @Configuration 类,如下所示:

@Configuration
@EnableSpringConfigured
public class AppConfig {
}

等价的基于 XML 的配置:

<context:spring-configured/>

在配置切面之前创建的 @Configurable 对象实例会导致向调试日志发出一条消息,并且不会发生对象的配置。例如 Spring 配置中的 bean 在被 Spring 初始化时创建域对象。在这种情况下,您可以使用 bean 属性 depends-on 手动指定 bean 依赖于配置切面。以下示例显示了如何使用 depends-on 属性:

<bean id="myService"
        class="com.xzy.myapp.service.MyService"
        depends-on="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect">

    <!-- ... -->

</bean>

不要通过 bean 配置切面激活 @Configurable 处理,除非您真的打算在运行时依赖其语义。特别是,请确保不要在容器中注册为常规 bean 上使用 @Configurable 。这样做会导致双重初始化,一次通过容器,一次通过切面。

单元测试 @Configurable 对象

@Configurable 支持的目标之一是启用域对象的独立单元测试。如果 @Configurable 类型没有被 AspectJ 编织,则注解在单元测试期间没有任何影响。您可以在被测对象中设置模拟或存根属性引用并照常进行。如果 @Configurable 类型被 AspectJ 编织,你仍然可以像往常一样在容器外进行单元测试,但是每次你构造一个 @Configurable 对象时你都会看到一条警告消息,表明它没有被 Spring 配置。

使用多个应用上下文

AnnotationBeanConfigurerAspect 是用于实现 @Configurable 支持的 AspectJ 单例切面。单例切面的作用域与 static 成员的作用域相同:每个类加载器都有一个切面实例来定义类型。这意味着,如果您在同一个类加载器层次结构中定义多个应用上下文,您需要考虑在何处定义 @EnableSpringConfigured bean 以及在类路径上放置 spring-aspects.jar

考虑一个典型的 Spring Web 应用程序配置,它具有一个共享的父应用上下文,该上下文定义了公共业务服务、支持这些服务所需的一切,以及每个 servlet 的一个子应用上下文(其中包含特定于该 servlet 的定义)。所有这些上下文共存于同一个类加载器层次结构中,因此它们只能持有对其中一个 AnnotationBeanConfigurerAspect 的引用。在这种情况下,我们建议在共享(父)应用上下文中定义 @EnableSpringConfigured bean 。这定义了您可能想要注入到域对象中的服务。结果是您无法通过使用 @Configurable 机制配置具有在子(特定于 servlet )上下文中定义的 bean 的引用的域对象。

在同一个容器中部署多个 Web 应用程序时,确保每个 Web 应用使用自己的类加载器(例如,通过放置 spring-aspects.jar WEB-INF/lib 目录下)加载 spring-aspects.jar 里的类。如果仅添加 spring-aspects.jar 到容器范围的类路径(因此由共享的父类加载器加载),则所有 Web 应用程序共享相同的切面实例。

AspectJ 的其他 Spring 切面

除了 @Configurable 切面之外,spring-aspects.jar 还包含一个 AspectJ 切面,您可以使用它来驱动 Spring 对使用 @Transactional 注解的类型和方法的事务管理。这主要适用于想要在 Spring 容器之外使用 Spring Framework 的事务支持的用户。

解释 @Transactional 注解的切面是 AnnotationTransactionAspect 。当您使用此切面时,您必须注解实现类(或该类中的方法),而不是该类实现的接口。AspectJ 遵循 Java 的规则,即接口上的注解不能被继承。

对于想要使用 Spring 配置以及事务管理支持但不想使用注解的 AspectJ 程序员,spring-aspects.jar 包含可以扩展以提供您自己的切点定义的 abstract 方面。有关更多信息,请参阅 AbstractBeanConfigurerAspectAbstractTransactionAspect 切面。作为示例,以下摘录显示了如何编写切面以使用与完全限定类名称匹配的原型 bean 定义来配置域模型中定义的所有对象实例:

public aspect DomainObjectConfiguration extends AbstractBeanConfigurerAspect {

    public DomainObjectConfiguration() {
        setBeanWiringInfoResolver(new ClassNameBeanWiringInfoResolver());
    }

    // the creation of a new bean (any object in the domain model)
    protected pointcut beanCreation(Object beanInstance) :
        initialization(new(..)) &&
        CommonPointcuts.inDomainModel() &&
        this(beanInstance);
}

使用 Spring IoC 配置 AspectJ 切面

AspectJ 运行时本身负责创建切面,而通过 Spring 配置 AspectJ 创建切面的方式取决于切面使用的 AspectJ 实例化模型( per-xxx 子句)。

大多数 AspectJ 方面都是单例切面。这些切面的配置很容易。您可以创建一个 bean 定义,该定义正常引用切面类型并包含 bean 属性 factory-method="aspectOf" 。这确保 Spring 通过向 AspectJ 请求而不是尝试自己创建实例来获取切面实例

<bean id="profiler" class="com.xyz.profiler.Profiler" factory-method="aspectOf"> 
    <property name="profilingStrategy" ref="jamonProfilingStrategy"/>
</bean>

非单例方面更难配置。但是,可以通过创建原型 bean 定义并使用 spring-aspects.jar 里的 @Configurable 支持来配置切面实例,一旦它们具有由 AspectJ 运行时创建的 bean ,就可以做到这一点。

如果你有一些 @AspectJ 切面想用 AspectJ 编织(例如,对领域模型类型使用加载时编织)和其他 @AspectJ 切面想用 Spring AOP,而这些切面都在 Spring 中配置,您需要告诉 Spring AOP @AspectJ 自动代理支持配置中定义的 @AspectJ 切面的哪个确切子集应该用于自动代理。您可以通过在 <aop:aspectj-autoproxy/> 声明中使用 <include/> 元素来实现这一点。每个 include/> 元素指定一个名称模式,只有名称至少与其中一个模式匹配的 bean 才会用于 Spring AOP 自动代理配置。

<aop:aspectj-autoproxy>
    <aop:include name="thisBean"/>
    <aop:include name="thatBean"/>
</aop:aspectj-autoproxy>

不要被 <aop:aspectj-autoproxy/> 元素的名称误导。使用可以创建 Spring AOP 代理。这里使用了@AspectJ 风格的切面声明,但不涉及 AspectJ 运行时。

在 Spring 框架中使用 AspectJ 进行加载时编织

加载时编织 (LTW) 是指在将 AspectJ 切面加载到 Java 虚拟机 (JVM) 时将它们编织到应用程序的类文件中的过程。

本节的重点是在 Spring Framework 的特定上下文中配置和使用 LTW 。

第一个例子

可以使用 @EnableLoadTimeWeaving 注解作为 <context:load-time-weaver/> 替代方案

package foo;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.util.StopWatch;
import org.springframework.core.annotation.Order;

@Aspect
public class ProfilingAspect {

    @Around("methodsToBeProfiled()")
    public Object profile(ProceedingJoinPoint pjp) throws Throwable {
        StopWatch sw = new StopWatch(getClass().getSimpleName());
        try {
            sw.start(pjp.getSignature().getName());
            return pjp.proceed();
        } finally {
            sw.stop();
            System.out.println(sw.prettyPrint());
        }
    }

    @Pointcut("execution(public * foo..*.*(..))")
    public void methodsToBeProfiled(){}
}

我们还需要创建 META-INF/aop.xml 文件,以通知 AspectJ weaver 我们想要将 ProfilingAspect 编织到我们的类中。

<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "https://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>

    <weaver>
        <!-- only weave classes in our application-specific packages -->
        <include within="foo.*"/>
    </weaver>

    <aspects>
        <!-- weave in just this aspect -->
        <aspect name="foo.ProfilingAspect"/>
    </aspects>

</aspectj>

w偶们需要配置一个LoadTimeWeaver 。这个加载时编织器是负责将 META-INF/aop.xml 文件中的切面配置编织到应用中的类的基本组件。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <!-- a service object; we will be profiling its methods -->
    <bean id="entitlementCalculationService"
            class="foo.StubEntitlementCalculationService"/>

    <!-- this switches on the load-time weaving -->
    <context:load-time-weaver/>
</beans>

现在所有必需的组件(切面、META-INF/aop.xml 文件和 Spring 配置)都已准备就绪,我们可以创建以下驱动程序类,并使用 main(..) 方法来演示 LTW 的运行情况:

package foo;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public final class Main {

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml", Main.class);

        EntitlementCalculationService entitlementCalculationService =
                (EntitlementCalculationService) ctx.getBean("entitlementCalculationService");

        // the profiling aspect is 'woven' around this method execution
        entitlementCalculationService.calculateEntitlement();
    }
}

使用以下命令来运行 Main 类:

java -javaagent:C:/projects/foo/lib/global/spring-instrument.jar foo.Main

-javaagent 是一个标志,用于启用代理来检测在 JVM 上运行的程序。Spring Framework 附带了这样一个代理,InstrumentationSavingAgent ,它被打包在 spring-instrument.jar 中。

输出:

Calculating entitlement

StopWatch 'ProfilingAspect': running time (millis) = 1234
------ ----- ----------------------------
ms     %     Task name
------ ----- ----------------------------
01234  100%  calculateEntitlement
posted @ 2022-06-09 21:15  流星<。)#)))≦  阅读(29)  评论(0编辑  收藏  举报