一文搞定spring aop面向切面编程【高级使用篇】

一文搞定spring aop面向切面编程【高级使用篇】

Aspect Oriented Programming with Spring

核心关注点:一个是通知类型,一个是切点表达式

https://docs.spring.io/spring-framework/reference/core/aop.html

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

Spring框架的一个关键组件之一是AOP框架。虽然Spring IoC容器不依赖AOP(意味着如果不想使用AOP,就不需要使用它),但AOP与Spring IoC相辅相成,提供了一个非常有能力的中间件解决方案。

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

  1. 切面(Aspect):横跨多个类的关注点的模块化。事务管理是企业Java应用程序中横切关注点的一个很好的例子。在Spring AOP中,切面可以通过使用常规类(基于模式的方法)或者使用带有@Aspect注解的常规类(@AspectJ风格)来实现。

  2. 连接点(Join point):程序执行过程中的一个点,比如方法的执行或者异常的处理。在Spring AOP中,连接点始终代表方法的执行

  3. 通知(Advice):切面在特定连接点执行的操作。不同类型的通知包括“around”、“before”和“after”通知(通知类型将在后面讨论)。许多AOP框架,包括Spring,将通知建模为拦截器,并维护围绕连接点的拦截器链。

  4. 切点(Pointcut):匹配连接点的谓词。通知与切点表达【pointcut expression】相关联,并在由切点匹配的任何连接点上运行(例如,执行具有特定名称的方法)。连接点的概念由切点表达式匹配是AOP的核心,Spring默认使用AspectJ切点表达式语言。

  5. 引入(Introduction):代表类型声明附加方法或字段。Spring AOP允许您向任何受通知的对象引入新的接口(以及相应的实现)。例如,您可以使用引入使一个bean实现一个IsModified接口,以简化缓存。(在AspectJ社区中,引入被称为inter-type声明。)

  6. 目标对象(Target object):受一个或多个切面通知的对象。也称为“被通知对象”。由于Spring AOP是通过使用运行时代理实现的,因此该对象始终是一个代理对象。

  7. AOP代理(AOP proxy):AOP框架创建的对象,用于实现切面合同(通知方法执行等)。在Spring框架中,AOP代理是JDK动态代理或CGLIB代理。

  8. 织入(Weaving):将切面与其他应用程序类型或对象链接以创建一个被通知的对象。这可以在编译时(例如使用AspectJ编译器)、加载时或运行时完成。Spring AOP,像其他纯Java AOP框架一样,会在运行时进行织入。

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

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

  • 返回后通知(After returning advice):在连接点正常完成后运行的通知(例如,如果方法返回而没有抛出异常)。

  • 抛出后通知(After throwing advice):如果方法通过抛出异常退出,则运行的通知。

  • 最终通知(After (finally) advice):无论连接点以何种方式退出(正常或异常返回),都要运行的通知。

  • 环绕通知(Around advice):围绕连接点(比如方法调用)的通知。这是最强大的通知类型。环绕通知可以在方法调用之前和之后执行自定义行为

    它还负责是否选择继续执行到连接点【或】通过返回自己的返回值或抛出异常来快速执行受通知的方法。

环绕通知是最通用的通知类型。由于Spring AOP(如AspectJ)提供了全面的通知类型,我们建议您使用最不强大的通知类型来实现所需的行为。例如,如果您只需要使用方法的返回值更新缓存,则最好实现一个“返回后通知”,而不是“环绕通知”,尽管“环绕通知”也可以实现相同的功能。使用最具体的通知类型提供了更简单的编程模型,减少了错误的可能性。例如,您不需要在用于环绕通知的JoinPoint上调用proceed()方法,因此您不会因为没有调用它而失败。

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

由切入点匹配的连接点的概念是AOP的关键,这使其区别于只提供拦截的旧技术。

切入点使得通知可以独立于面向对象的层次结构进行定位。例如,您可以将提供声明性事务管理的环绕通知应用于跨多个对象的一组方法(例如服务层中的所有业务操作)。

Spring AOP Capabilities and Goals

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

Spring AOP 目前仅支持方法执行连接点(对 Spring bean 上的方法执行进行增强)。尽管可以添加对字段拦截的支持,但目前尚未实现字段拦截。如果需要对字段访问和更新连接点进行增强,可以考虑使用 AspectJ 等语言。

Spring AOP 的 AOP 方法与大多数其他 AOP 框架不同。其目标不是提供最完整的 AOP 实现(尽管 Spring AOP 很有能力)。而是提供 AOP 实现和 Spring IoC 之间的紧密集成,以帮助解决企业应用中的常见问题。

例如,Spring 框架的 AOP 功能通常与 Spring IoC 容器一起使用。通过使用正常的 bean 定义语法来配置切面(尽管这允许强大的“自动代理”功能)。这与其他 AOP 实现有着重要的区别。使用 Spring AOP 不能轻松或有效地做一些事情,比如对非常细粒度的对象进行增强(通常是领域对象)。在这种情况下,AspectJ 是最佳选择。但是,我们的经验表明,Spring AOP 对于企业 Java 应用程序中大多数可适用 AOP 的问题提供了出色的解决方案。

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

Spring框架的一个核心原则【tenets】之一是非侵入性【non-invasiveness】。这意味着你不应该被强制将特定于框架的类和接口引入到你的业务或领域模型中。然而,在一些地方,Spring框架确实给了你选择的机会,可以在你的代码库中引入Spring框架特定的依赖。提供这样的选择是因为在某些场景下,以某种方式阅读或编写特定功能可能更加简单明了。然而,Spring框架(几乎)总是给你选择的自由:你有权利做出明智的决定,选择最适合你特定使用情况或场景的选项。

在这一章中,一个相关的选择是选择哪种AOP框架(以及哪种AOP风格)。你可以选择AspectJ、Spring AOP,或者两者兼用。你还可以选择@AspectJ注解风格方法或Spring XML配置风格方法。本章选择首先介绍@AspectJ风格方法,并不意味着Spring团队偏爱@AspectJ注解风格方法而不是Spring XML配置风格。

请参阅《选择使用哪种AOP声明风格》以获取更全面的讨论,了解每种风格的优缺点。

AOP Proxies

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

也就是说:如果一个spring bean 实现了某个接口,那么spring将使用JDK动态代理作为AOP代理

Spring AOP也可以使用CGLIB代理。这对于代理类而不是接口是必要的。默认情况下,如果业务对象没有实现接口,就会使用CGLIB。由于最好的做法是针对接口而不是类进行编程,业务类通常实现一个或多个业务接口。在那些(希望很少见的)情况下,你需要通知一个未在接口上声明的方法,或者需要将一个代理对象作为具体类型传递给一个方法,你可以强制使用CGLIB。

重要的是要理解Spring AOP是基于代理的。请参阅《理解AOP代理》

对这个实现细节的彻底分析。

Enabling @AspectJ Support

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

@AspectJ支持可以通过XML或Java样式的配置来启用。无论哪种情况,您还需要确保AspectJ的aspectjweaver.jar库在应用程序的类路径上(版本为1.9或更高)。该库可以在AspectJ发行版的lib目录中找到,也可以从Maven中央仓库获取。

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}

启用@AspectJ支持后,应用程序上下文中定义的任何bean具有@Aspect注释的类,都会被Spring自动检测并用于配置Spring AOP

@Aspect
@Component//让spring bean 管理
public class NotVeryUsefulAspect {
}

切面(使用@Aspect注解的类)可以拥有方法和字段,与其他类一样。它们还可以包含切点、通知和引入(互补类型)声明。

Declaring a Pointcut

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

举个例子可能有助于清楚地说明切入点签名和切入点表达式之间的区别。以下示例定义了一个名为anyOldTransfer的切入点,它匹配任何名为transfer的方法的执行。

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

@Pointcut注解的值是一个常规的AspectJ切入点表达式。有关AspectJ切入点语言的详细讨论,请参阅

https://www.eclipse.org/aspectj/doc/released/progguide/index.html

(以及扩展内容,请参阅AspectJ 5开发者手册)或AspectJ的相关书籍(例如Colyer等人的《Eclipse AspectJ》或Ramnivas Laddad的《AspectJ实战》)。

Supported Pointcut Designators支持的切面标识符24个

org.aspectj.weaver.tools.PointcutPrimitive

public final class PointcutPrimitive extends TypeSafeEnum {

	public static final PointcutPrimitive CALL = new PointcutPrimitive("call",1);
	public static final PointcutPrimitive EXECUTION = new PointcutPrimitive("execution",2);
	public static final PointcutPrimitive GET = new PointcutPrimitive("get",3);
	public static final PointcutPrimitive SET = new PointcutPrimitive("set",4);
	public static final PointcutPrimitive INITIALIZATION = new PointcutPrimitive("initialization",5);
	public static final PointcutPrimitive PRE_INITIALIZATION = new PointcutPrimitive("preinitialization",6);
	public static final PointcutPrimitive STATIC_INITIALIZATION = new PointcutPrimitive("staticinitialization",7);
	public static final PointcutPrimitive HANDLER = new PointcutPrimitive("handler",8);
	public static final PointcutPrimitive ADVICE_EXECUTION = new PointcutPrimitive("adviceexecution",9);
	public static final PointcutPrimitive WITHIN = new PointcutPrimitive("within",10);
	public static final PointcutPrimitive WITHIN_CODE = new PointcutPrimitive("withincode",11);
	public static final PointcutPrimitive CFLOW = new PointcutPrimitive("cflow",12);
	public static final PointcutPrimitive CFLOW_BELOW = new PointcutPrimitive("cflowbelow",13);
	public static final PointcutPrimitive IF = new PointcutPrimitive("if",14);
	public static final PointcutPrimitive THIS = new PointcutPrimitive("this",15);
	public static final PointcutPrimitive TARGET = new PointcutPrimitive("target",16);
	public static final PointcutPrimitive ARGS = new PointcutPrimitive("args",17);
	public static final PointcutPrimitive REFERENCE = new PointcutPrimitive("reference pointcut",18);
	public static final PointcutPrimitive AT_ANNOTATION = new PointcutPrimitive("@annotation",19);
	public static final PointcutPrimitive AT_THIS = new PointcutPrimitive("@this",20);
	public static final PointcutPrimitive AT_TARGET = new PointcutPrimitive("@target",21);
	public static final PointcutPrimitive AT_ARGS = new PointcutPrimitive("@args",22);
	public static final PointcutPrimitive AT_WITHIN = new PointcutPrimitive("@within",23);
	public static final PointcutPrimitive AT_WITHINCODE = new PointcutPrimitive("@withincode",24);

	private PointcutPrimitive(String name, int key) {
		super(name, key);
	}
}

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时,这是用于匹配传递的实际参数的运行时类型具有给定类型注解的方法执行的切点标识符)。
  • @within:将匹配限制在具有给定注解的类型内的连接点(在使用Spring AOP时,这是用于匹配在具有给定注解的类型中声明的方法执行的切点标识符)。
  • @annotation:将匹配限制在主题连接点(准备被Spring AOP代理执行的方法头上)具有给定注解的连接点。【方法头上具有指定连接点】

由于Spring AOP仅限于方法执行连接点的匹配,所以前面讨论的切入点设计符比你在AspectJ编程指南中找到的定义更狭窄。此外,AspectJ本身具有基于类型的语义,在执行连接点时,this和target都指向同一个对象:执行方法的对象。Spring AOP是基于代理的系统,并区分代理对象本身(绑定到this)和代理后面的目标对象(绑定到target)。

由于Spring的AOP是基于代理的框架,目标对象内部的调用在定义上是不会被拦截的。对于JDK代理,只有代理上的公共接口方法调用才能被拦截。而对于CGLIB,代理上的公共和受保护的方法调用都会被拦截(甚至是包可见的方法,如果有必要的话)。然而,通过代理进行的常见交互应该始终通过公共签名来设计。

需要注意的是,切入点定义通常会匹配任何被拦截的方法。如果一个切入点严格意味着只能是公共的,即使在CGLIB代理场景下可能存在通过代理的非公共交互,也需要相应地进行定义。

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

bean PCD【point cut designators】仅在Spring AOP中得到支持,而不是在原生的AspectJ编织中。

它是AspectJ定义的标准PCD的Spring特定扩展,因此不适用于在@Aspect模型中声明的切面。

bean PCD在实例级别操作(基于Spring bean名称概念),而不仅仅是在类型级别操作(这是编织式AOP所限制的)。

基于实例的切入点设计符是Spring基于代理的AOP框架的特殊能力,以及它与Spring bean工厂的紧密集成,因此可以自然而直接地通过名称识别特定的bean。

Combining Pointcut Expressions【组合切点表达式】

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

@Aspect
public class Pointcuts {

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

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

	@Pointcut("publicMethod() && inTrading()")
	public void tradingOperation() {}
    
    //省略这里声明通知
}
  • publicMethod匹配如果方法执行连接点代表任何public方法的执行。
  • inTrading匹配如果方法执行在交易模块中。
  • tradingOperation匹配如果方法执行代表交易模块中的任何公共方法。

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

Sharing Named Pointcut Definitions

在使用企业应用程序时,开发人员通常需要从几个切面引用应用程序的模块和特定的操作集。

我们建议为此目的定义一个专门的类来捕获常用的命名切入点表达式。这样的类通常类似于下面的CommonPointcuts示例(尽管您可以为类命名):

import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class CommonPointcuts {

	/**
	 * 如果方法定义在com.xyz.web包或其子包中的类型中,则连接点位于Web层。
	 */
	@Pointcut("within(com.xyz.web..*)")
	public void inWebLayer() {}

	/**
	 * 如果方法定义在com.xyz.service包或其子包中的类型中,则连接点位于服务层。
	 */
	@Pointcut("within(com.xyz.service..*)")
	public void inServiceLayer() {}

	/**
	 * 如果方法定义在com.xyz.dao包或其子包中的类型中,则连接点位于数据访问层。
	 */
	@Pointcut("within(com.xyz.dao..*)")
	public void inDataAccessLayer() {}

	/**
	 * 业务服务是在服务接口上定义的任何方法的执行。该定义假定接口放置在"service"包中,实现类型放置在子包中。
	 *
	 * 如果您按功能区域对服务接口进行分组(例如,在包com.xyz.abc.service和com.xyz.def.service中),
	 * 则可以使用切入点表达式"execution(* com.xyz..service.*.*(..))"。
	 *
	 * 或者,您可以使用'bean' PCD编写表达式,如"bean(*Service)"。(这假设您以一致的方式命名了Spring服务bean。)
	 */
	@Pointcut("execution(* com.xyz..service.*.*(..))")
	public void businessService() {}

	/**
	 * 数据访问操作是在DAO接口上定义的任何方法的执行。该定义假定接口放置在"dao"包中,实现类型放置在子包中。
	 */
	@Pointcut("execution(* com.xyz.dao.*.*(..))")
	public void dataAccessOperation() {}

}

Writing Good Pointcuts

在编译期间,AspectJ处理切入点以优化匹配性能。检查代码并确定每个连接点是否与给定的切入点匹配(静态或动态)是一个代价高昂的过程。(动态匹配意味着不能从静态分析中完全确定匹配,并且在代码中放置测试以确定代码运行时是否存在实际匹配)。在第一次遇到切入点声明时,AspectJ将其重写为匹配过程的最佳形式。这是什么意思?基本上,用DNF(析取范式)重写切入点,并对切入点的组件进行排序,以便首先检查那些计算成本较低的组件。这意味着您不必担心理解各种切入点指示符的性能,并且可以在切入点声明中以任何顺序提供它们。

然而,AspectJ只能使用它被告知的内容。为了获得最佳的匹配性能,您应该考虑要实现的目标,并在定义中尽可能缩小匹配的搜索空间。现有的指示符自然可以分为三类:kind、scoping和contextual:

kind类型化指示符选择特定类型的连接点:【 execution, get, set, call, handler

scoping作用域指示符选择一组感兴趣的连接点(可能有多种类型): within withincode

contextual:上下文指示符基于上下文匹配(并可选择绑定): this, target, @annotation`

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

Examples

Spring AOP 用户可能最常使用的是execution 执行点标记。执行表达式【execution expression】的格式如下:

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

除了返回类型模式【returning type pattern】(前面代码段中的 ret-type-pattern)、名称模式和参数模式外,其他部分都是可选的。
返回类型模式决定方法的返回类型,以便与连接点匹配。返回类型模式最常用的是 。它匹配任何返回类型。
全称类型名称只有在方法返回给定类型时才匹配。名称模式与方法名称匹配。可以将 * 通配符作为名称模式的全部或部分使用。
如果指定了声明类型模式,请在后面加上 .,以便将其连接到名称模式组件。
参数模式稍微复杂一些:
()匹配不带参数的方法,
(…)匹配任意数量(零或更多)的参数。
(*) 模式匹配只接受一个任意类型参数的方法。
(
,String)匹配接收两个参数的方法。第一个参数可以是任何类型,而第二个参数必须是字符串
更多信息,请参阅《AspectJ 编程指南》中的 "语言语义 "部分。

//执行任何公共方法:
execution(public * *(...))
    
//执行任何名称以 set 开头的方法:
execution(* set*(..))
    
//执行由 AccountService 接口定义的任何方法:
execution(* com.xyz.service.AccountService.*(..))
    
//执行服务包中定义的任何方法:
execution(* com.xyz.service.*.*(..))
    
//执行服务包或其子包中定义的任何方法
execution(* com.xyz.service..*.*(...)
          
//服务包内的任何连接点(仅在 Spring AOP 中执行方法):
within(com.xyz.service.*)
          
//服务包或其子包中的任何连接点(仅在 Spring AOP 中执行方法):
within(com.xyz.service..*)
          
//代理实现 AccountService 接口的任何连接点(仅在 Spring AOP 中执行方法):
this(com.xyz.service.AccountService)

这更常用于绑定形式。有关如何在通知正文中提供代理对象,请参阅 "声明通知 "一节。
目标对象实现 AccountService 接口的任何连接点(仅在 Spring AOP 中执行方法):

  • target(com.xyz.service.AccountService)

    target 更常用于绑定形式。有关如何在通知正文中提供目标对象,请参阅 "声明通知 "部分。

    任何接收单个参数且运行时传递的参数为 Serializable 的连接点(仅在 Spring AOP 中执行方法):

  • args(java.io.Serializable)

    args 更常用于绑定形式。有关如何在通知正文中提供方法参数,请参阅 "声明通知 "部分。

    请注意,本例中给出的快捷方式与 execution(* *(java.io.Serializable)) 不同。如果运行时传递的参数是 Serializable,args 版本就会匹配;如果方法签名声明了 Serializable 类型的单个参数,执行版本就会匹配。

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

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

    您也可以在绑定形式中使用 @target。有关如何在通知正文中使用注解对象,请参阅 "声明通知 "部分。

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

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

    您也可以在绑定形式中使用 @within。有关如何在通知体中使用注解对象,请参阅 "声明通知 "部分。

    执行方法具有 @Transactional 注解的任何连接点(仅在 Spring AOP 中执行方法):

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

    您还可以在绑定形式中使用 @annotation 。有关如何在通知正文中提供注解对象,请参阅 "声明通知 "部分。

    任何接收单个参数的连接点(仅在 Spring AOP 中执行方法),且所传递参数的运行时类型具有 @Classified 注解:

  • @args(com.xyz.security.Classified)

    您还可以以绑定形式使用 @args。请参阅 "声明通知 "部分,了解如何在通知正文中使用注解对象。

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

  • bean(tradeService)
    名称符合通配符表达式 *Service 的 Spring Bean 上的任何连接点(仅在 Spring AOP 中执行方法):

    bean(*Service)

Declaring Advice

Advice is associated with a pointcut expression and runs before, after, or around method executions matched by the pointcut. The pointcut expression may be either an inline pointcut or a reference to a named pointcut.

通知与切入点表达式相关联,并在与切入点匹配的方法执行before,after或around通知。切入点表达式可以是内联切入点【即行内切入点】,也可以是对命名切入点的引用【配合@Pointcut注解,引用被其注解的方法签名】

Before Advice

@Aspect
public class BeforeExample {

	@Before("execution(* com.xyz.dao.*.*(..))")
	public void doAccessCheck() {
		// ...
	}
}

引用命名切入点【配合@Pointcut注解,引用被其注解的方法签名】

@Aspect
public class BeforeExample {

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

After Returning Advice

After Returning当方法没有抛出异常:即正常返回执行

@Aspect
public class AfterReturningExample {

	@AfterReturning("execution(* com.xyz.dao.*.*(..))")
	public void doAccessCheck() {
		// ...
	}
}

有时候,在通知体中需要访问实际返回的值。你可以使用@AfterReturning的returning属性将返回值绑定,以便获取访问权限,就像以下示例所示的那样。

@Aspect
public class AfterReturningExample {

	@AfterReturning(pointcut="execution(* com.xyz.dao.*.*(..))",returning="returnVal")
	public void doAccessCheck(Object returnVal) {
		// returnVal 为接收方法返回值
	}
}

返回属性中使用的名称returnVal必须对应于建议方法中的参数名称。当方法执行返回时,返回值将作为相应的参数值传递给建议方法。返回子句还限制了只匹配那些返回指定类型值的方法执行(在这种情况下,returnVal类型 为Object时,匹配任何返回值)。

请注意,在使用返回通知后,不可能返回完全不同的引用

After Throwing Advice

当方法抛出异常后执行

@Aspect
public class AfterThrowingExample {

	@AfterThrowing("execution(* com.xyz.dao.*.*(..))")
	public void doRecoveryActions() {
		// ...
	}
}

通常,您希望通知仅在抛出特定类型的异常时运行,并且您经常需要在通知体中访问抛出的异常。您可以使用throwing属性来限制匹配(如果需要的话 - 否则使用Throwable作为异常类型)并将抛出的异常绑定到通知方法签名参数。下面的示例显示了如何这样做:

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

@Aspect
public class AfterThrowingExample {

	@AfterThrowing(pointcut="execution(* com.xyz.dao.*.*(..))",throwing="ex")//名字要对应上
	public void doRecoveryActions(DataAccessException ex) {//名字要对应上
		// ...
	}
}

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

请注意,@AfterThrowing并不表示一个通用的异常处理回调。具体来说,@AfterThrowing通知方法只应该接收来自连接点(用户声明的目标方法)本身的异常,而不是来自伴随的@After/@AfterReturning方法的异常。【也就是说只有被相应的@AfterThrowing切点表达式匹配到才行】

Around Advice

最后一种通知是关于通知。环绕通知“环绕”匹配方法的执行。它有机会在方法运行之前和之后进行工作,并确定方法实际上何时、如何,甚至是否运行。环绕通知通常用于在线程安全的方式下需要在方法执行之前和之后共享状态的情况,例如启动和停止计时器。

使用@Around注解来声明环绕通知。该方法应将Object声明为其返回类型,方法的第一个参数必须是ProceedingJoinPoint类型。在通知方法的主体内,您必须在ProceedingJoinPoint上调用proceed()方法,以便运行基础方法。在调用proceed()时不带参数将导致调用者的原始参数在调用基础方法时被提供。对于高级用例,有一个重载的proceed()方法,它接受一个参数数组(Object[])。数组中的值将在调用基础方法时用作参数。

当使用Object[]调用proceed时,其行为与使用AspectJ编译器编译的环绕通知的proceed行为略有不同。对于使用传统AspectJ语言编写的环绕通知,传递给proceed的参数数量必须与环绕通知传递的参数数量匹配(而不是基础连接点所需的参数数量),并且在给定参数位置传递给proceed的值将替换原始值,不必担心如果现在这样做没有意义的话。

Spring采用的方法更简单,更符合其基于代理的仅执行语义。只有在为Spring编译@AspectJ切面编写的切面并使用AspectJ编译器和织入器使用参数调用proceed时,才需要了解这种差异。有一种方法可以编写这样的切面,它在Spring AOP和AspectJ之间是100%兼容的,这在以下关于通知参数的部分中进行了讨论。

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

如果将环绕通知方法的返回类型声明为void,则始终将null返回给调用者,从而有效地忽略对proceed()的任何调用的结果。因此,建议环绕通知方法声明返回类型为Object。通知方法通常应返回从调用proceed()返回的值,即使基础方法具有void返回类型。但是,根据用例,该通知可以选择返回缓存值、包装值或其他值。

@Aspect
public class AroundExample {

	@Around("execution(* com.xyz..service.*.*(..))")
	public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
		// start stopwatch
		Object returningVal = pjp.proceed();
		// stop stopwatch
		return returningVal;
	}
}

Advice Parameters通知参数

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

Access to the Current JoinPoint访问当前连接点

任何通知方法都可以声明一个 org.aspectj.lang.JoinPoint 类型的参数作为其第一个参数。

请注意,环绕通知需要声明一个类型为 ProceedingJoinPoint 的第一个参数,它是 JoinPoint 的子类。

JoinPoint 接口提供了许多有用的方法:

getArgs(): 返回方法参数。

getThis(): 返回代理对象。

getTarget(): 返回目标对象。

getSignature(): 返回被通知的方法的描述。

toString(): 打印被通知方法的有用描述。

请参阅 javadoc 以获取更多详细信息。

Passing Parameters to Advice向通知传递参数

我们已经看到如何绑定返回值或异常值(使用 after returning 和 after throwing 通知)。为了使参数值在通知主体中可用,您可以使用 args 的绑定形式。如果您在 args 表达式中使用参数名代替类型名,当调用通知时,相应参数的值将作为参数值传递。一个示例应该能更清楚地说明这一点。假设您想要通知以 Account 对象作为第一个参数进行 DAO 操作的执行,并且您需要在通知主体中访问该账户。您可以编写如下内容:

@Before("execution(* com.xyz.dao.*.*(..)) && args(account,..)")
public void validateAccount(JoinPoint jp,Account account) {
	// ...
}

pointcut表达式中的args(accoun,…)部分有两个目的。首先,它限制匹配只有那些方法执行,其中方法至少需要一个参数,且传递给该参数的参数是Account的实例。其次,它通过account参数使实际的Account对象可用于通知。

另一种编写方式是声明一个提供Account对象值的切入点,当它匹配连接点时,然后从通知中引用命名的切入点。这将如下所示:

@Pointcut("execution(* com.xyz.dao.*.*(..)) && args(account,..)")
private void accountDataAccessOperation(Account account) {}

//任何通知方法都可以声明一个 org.aspectj.lang.JoinPoint 类型的参数作为其第一个参数。
//请注意,环绕通知需要声明一个类型为 ProceedingJoinPoint 的第一个参数,它是 JoinPoint 的子类
@Before("accountDataAccessOperation(account)")
public void validateAccount(JoinPoint jp,Account account) {
	// ...
}

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

以下是@Auditable注解的定义:

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

以下是匹配方法头上有自定义注解@Auditable的执行的通知 :

@Before("com.xyz.Pointcuts.publicMethod() && @annotation(auditable)") 
public void audit(JoinPoint jp,Auditable auditable) {
	String code = auditable.value();
	// Auditable是一个自定义注解
    //...
}

引用了在组合切入点表达式中定义的名为publicMethod的切入点。

Advice Parameters and Generics泛型通知参数处理

Spring AOP 可以处理在类声明和方法参数中使用的泛型。假设您有一个如下所示的泛型类型:

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

您可以通过将通知参数绑定到要拦截方法的参数类型,限制对某些参数类型的方法类型的拦截:

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

这种方法对泛型集合不起作用。因此,您不能定义如下的切入点:

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

要使这个工作,我们必须检查集合的每个元素,这是不合理的,因为我们也不能决定如何处理空值。

要实现类似的效果,您必须将参数类型定义为 Collection<?> 并手动检查元素的类型。

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

Determining Argument Names确定参数名称

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

本节中将参数argument 和参数parameter 互换使用,因为AspectJ API将参数名称parameter 称为参数名称argument。
Spring AOP使用以下ParameterNameDiscoverer实现来确定参数名称。每个发现者将有机会发现参数名称,并且第一个成功的发现者获胜。如果没有注册的发现者能够确定参数名称,则会抛出异常。

AspectJAnnotationParameterNameDiscoverer
使用用户通过相应的通知或切点注解的argNames属性明确指定的参数名称。有关详细信息,请参见显式参数名称。

KotlinReflectionParameterNameDiscoverer
使用Kotlin反射API来确定参数名称。仅在类路径上存在这些API时才使用此发现者。

StandardReflectionParameterNameDiscoverer
使用标准的java.lang.reflect.Parameter API来确定参数名称。要求使用javac编译代码时使用-parameters标志。建议在Java 8+上使用。

LocalVariableTableParameterNameDiscoverer
从调试信息中的advice类的字节码中的局部变量表分析以确定参数名称。要求使用调试符号(-g:vars至少)编译代码。自Spring Framework 6.0起已弃用,计划在Spring Framework 6.1中移除,推荐使用使用-parameters编译代码。在GraalVM本机映像中不受支持。

AspectJAdviceParameterNameDiscoverer
从切点表达式、返回和抛出子句中推断参数名称。有关所使用算法的详细信息,请参见javadoc。

Explicit Argument Names显式参数名称

@AspectJ通知和切点注解具有可选的argNames属性,您可以使用它来指定注释方法的参数名称。

如果@AspectJ切面已由AspectJ编译器(ajc)编译,即使没有调试信息,您也不需要添加argNames属性,因为编译器保留了所需的信息。

同样,如果@AspectJ切面已使用javac使用-parameters标志编译,则无需添加argNames属性,因为编译器保留了所需的信息。

以下示例展示了如何使用argNames属性:

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

引用了在组合切点表达式中定义的publicMethod切点。
声明了bean和auditable作为参数名称。
如果第一个参数的类型是JoinPoint,ProceedingJoinPoint或JoinPoint.StaticPart,您可以在argNames属性的值中省略参数的名称。例如,如果您修改前面的建议以接收连接点对象,则argNames属性不需要包括它:

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

引用了在组合切点表达式中定义的publicMethod切点。
声明了bean和auditable作为参数名称。
对于类型JoinPoint,ProceedingJoinPoint或JoinPoint.StaticPart作为第一个参数,特殊处理特别方便于不收集任何其他连接点上下文的通知方法。在这种情况下,您可以省略argNames属性。例如,以下通知不需要声明argNames属性:

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

引用了在组合切点表达式中定义的publicMethod切点。

Proceeding with Arguments调用带参数的proceed方法

我们之前提到过,我们将描述如何调用带参数的proceed方法,可以在Spring AOP和AspectJ中保持一致。解决方案是确保通知签名按顺序绑定每个方法参数。以下示例显示了如何做到这一点:

@Around("execution(List<Account> find*(..)) && com.xyz.CommonPointcuts.inDataAccessLayer() && args(accountHolderNamePattern)") 
public Object preProcessQueryPattern(ProceedingJoinPoint pjp,
		String accountHolderNamePattern) throws Throwable {
	String newPattern = preProcess(accountHolderNamePattern);
	return pjp.proceed(new Object[] {newPattern});
}

引用了在共享命名切入点定义中定义的名为inDataAccessLayer的切入点。
在许多情况下,您无论如何都会进行这种绑定(就像前面的示例中一样)。

Advice Ordering通知顺序

当多个通知都希望在同一连接点运行时会发生什么?Spring AOP遵循与AspectJ相同的优先级规则来确定通知执行的顺序。优先级最高的通知首先在“进入时”运行(因此,给定两个前置通知,优先级最高的通知首先运行)。从连接点“退出时”,优先级最高的通知最后运行(因此,给定两个后置通知,具有最高优先级的通知将第二次运行)。

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

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

截至Spring Framework 5.2.7,在同一@Aspect类中定义的通知方法需要在相同连接点运行时,根据其通知类型分配优先级,顺序如下,从最高到最低优先级:@Around、@Before、@After、@AfterReturning、@AfterThrowing。但是请注意,同一切面中的@After通知方法将在任何@AfterReturning或@AfterThrowing通知方法之后有效地被调用,遵循@After的AspectJ“最后通知”语义。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值