Skip to content

声明切入点 (Declaring a Pointcut)

切入点(Pointcuts)决定了我们感兴趣的连接点(Join points),从而使我们能够控制通知(Advice)在何时运行。Spring AOP 仅支持 Spring Bean 的方法执行连接点,因此你可以将切入点视为匹配 Spring Bean 上方法的执行。

切入点声明由两部分组成:

  1. 切入点签名(Signature):包含名称和任何参数。
  2. 切入点表达式(Expression):确定我们究竟对哪些方法执行感兴趣。

在 @AspectJ 注解风格的 AOP 中,切入点签名由一个常规的方法定义提供,而切入点表达式通过使用 @Pointcut 注解来表示(作为切入点签名的方法必须具有 void 返回类型)。

以下示例定义了一个名为 anyOldTransfer 的切入点,它匹配任何名为 transfer 的方法的执行:

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

作为 @Pointcut 注解值的切入点表达式是常规的 AspectJ 切入点表达式。有关 AspectJ 切入点语言的详细讨论,请参阅 AspectJ 编程指南

受支持的切入点指示符 (PCD)

Spring AOP 支持以下用于切入点表达式的 AspectJ 切入点指示符(Pointcut Designators,简称 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 中执行的方法)具有给定注解的情况。

其他切入点类型

完整的 AspectJ 切入点语言支持 Spring 不支持的额外切入点指示符:call, get, set, preinitialization, staticinitialization, initialization, handler, adviceexecution, withincode, cflow, cflowbelow, if, @this, 和 @withincode。如果在 Spring AOP 解析的切入点表达式中使用这些指示符,将抛出 IllegalArgumentException

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

由于 Spring AOP 仅限于方法执行连接点,因此上述切入点指示符的定义比 AspectJ 编程指南中的定义更窄。此外,AspectJ 本身具有基于类型的语义,在执行连接点处,thistarget 都指向同一个对象:执行方法的对象。Spring AOP 是一个基于代理的系统,它区分了代理对象本身(绑定到 this)和代理背后的目标对象(绑定到 target)。

代理限制

由于 Spring AOP 框架基于代理的性质,目标对象内的内部调用定义上是不被拦截的

  • 对于 JDK 代理,只能拦截代理上的公共(public)接口方法调用。
  • 对于 CGLIB,可以拦截代理上的公共(public)和受保护(protected)方法调用(必要时甚至可以拦截包可见的方法)。然而,通过代理进行的常规交互应始终通过公共签名设计。

如果你的拦截需求包括目标类中的方法自调用甚至构造函数拦截,请考虑使用 Spring 驱动的原生 AspectJ 织入,而不是 Spring 基于代理的 AOP 框架。

Spring AOP 还支持一个名为 bean 的补充 PCD。该 PCD 允许你将连接点的匹配限制为特定的 Spring Bean 名称或一组 Bean 名称(使用通配符时)。bean PCD 的形式如下:

text
bean(idOrNameOfBean)

idOrNameOfBean 可以是任何 Spring Bean 的名称。支持使用 * 字符的有限通配符。与其他 PCD 一样,bean PCD 也可以与 &&(与)、||(或)和 !(非)运算符一起使用。

TIP

bean PCD 仅在 Spring AOP 中受支持,在原生 AspectJ 织入中不受支持。它是 Spring 对 AspectJ 定义的标准 PCD 的特定扩展。

组合切入点表达式

你可以使用 &&, ||! 来组合切入点表达式。你也可以通过名称引用切入点表达式。以下示例展示了三个切入点表达式:

java
package com.xyz;

public class Pointcuts {

	// 匹配任何公共方法的执行
	@Pointcut("execution(public * *(..))")
	public void publicMethod() {}

	// 匹配 trading 包及其子包下的任何执行
	@Pointcut("within(com.xyz.trading..*)")
	public void inTrading() {}

	// 匹配 trading 包下的公共方法执行
	@Pointcut("publicMethod() && inTrading()")
	public void tradingOperation() {}
}
kotlin
package com.xyz

class Pointcuts {

	// 匹配任何公共方法的执行
	@Pointcut("execution(public * *(..))")
	fun publicMethod() {}

	// 匹配 trading 包及其子包下的任何执行
	@Pointcut("within(com.xyz.trading..*)")
	fun inTrading() {}

	// 匹配 trading 包下的公共方法执行
	@Pointcut("publicMethod() && inTrading()")
	fun tradingOperation() {}
}

最佳实践是利用较小的命名切入点(Named pointcuts)构建更复杂的切入点表达式,如上所示。按名称引用切入点时,适用常规的 Java 可见性规则(你可以看到同类型中的 private 切入点,继承体系中的 protected 切入点,任何地方的 public 切入点等)。可见性不影响切入点匹配。

共享命名的切入点定义

在开发企业应用时,开发人员经常需要从多个切面引用应用的模块和特定的操作集。我们建议定义一个专用的类来捕获常用的命名切入点表达式。此类通常类似于以下 CommonPointcuts 示例:

java
package com.xyz;

import org.aspectj.lang.annotation.Pointcut;

public class CommonPointcuts {

	/**
	 * Web 层:匹配 com.xyz.web 包及其子包下的类型中定义的方法
	 */
	@Pointcut("within(com.xyz.web..*)")
	public void inWebLayer() {}

	/**
	 * Service 层:匹配 com.xyz.service 包及其子包下的类型中定义的方法
	 */
	@Pointcut("within(com.xyz.service..*)")
	public void inServiceLayer() {}

	/**
	 * 数据访问层:匹配 com.xyz.dao 包及其子包下的类型中定义的方法
	 */
	@Pointcut("within(com.xyz.dao..*)")
	public void inDataAccessLayer() {}

	/**
	 * 业务服务:匹配 service 接口上定义的任何方法的执行。
	 * 假设接口在 service 包中,实现类在子包中。
	 */
	@Pointcut("execution(* com.xyz..service.*.*(..))")
	public void businessService() {}

	/**
	 * 数据访问操作:匹配 DAO 接口上定义的任何方法的执行。
	 */
	@Pointcut("execution(* com.xyz.dao.*.*(..))")
	public void dataAccessOperation() {}

}
kotlin
package com.xyz

import org.aspectj.lang.annotation.Pointcut

class CommonPointcuts {

	@Pointcut("within(com.xyz.web..*)")
	fun inWebLayer() {}

	@Pointcut("within(com.xyz.service..*)")
	fun inServiceLayer() {}

	@Pointcut("within(com.xyz.dao..*)")
	fun inDataAccessLayer() {}

	@Pointcut("execution(* com.xyz..service.*.*(..))")
	fun businessService() {}

	@Pointcut("execution(* com.xyz.dao.*.*(..))")
	fun dataAccessOperation() {}

}

你可以通过引用类的全限定名加上 @Pointcut 方法的名称,在任何需要切入点表达式的地方引用此类中定义的切入点。例如,要使 Service 层具备事务性:

xml
<aop:config>
	<aop:advisor
		pointcut="com.xyz.CommonPointcuts.businessService()"
		advice-ref="tx-advice"/>
</aop:config>

常见示例

Spring AOP 用户最常使用的是 execution 切入点指示符。其格式如下:

text
execution(modifiers-pattern? 
          ret-type-pattern 
          declaring-type-pattern?name-pattern(param-pattern) 
          throws-pattern?)
  • 修饰符模式modifiers-pattern):可选,如 public
  • 返回类型模式ret-type-pattern):必选,匹配方法的返回类型。* 匹配任何返回类型。
  • 声明类型模式declaring-type-pattern):可选,后接 .
  • 名称模式name-pattern):必选,匹配方法名。可以使用 * 通配符。
  • 参数模式param-pattern):必选。() 匹配无参方法;(..) 匹配任意数量参数;(*) 匹配一个任意类型参数;(*,String) 匹配两个参数,第一个任意类型,第二个必须是 String
  • 异常模式throws-pattern):可选。

以下是一些常见的切入点表达式示例:

  • 任意公共方法的执行: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.*)
  • 代理对象实现了 AccountService 接口:this(com.xyz.service.AccountService)
  • 目标对象实现了 AccountService 接口:target(com.xyz.service.AccountService)
  • 传递的运行时参数是 Serializableargs(java.io.Serializable)
  • 目标对象具有 @Transactional 注解:@target(org.springframework.transaction.annotation.Transactional)
  • 方法上具有 @Transactional 注解:@annotation(org.springframework.transaction.annotation.Transactional)
  • 名为 tradeService 的 Spring Bean:bean(tradeService)
  • 名称匹配 *Service 的 Spring Bean:bean(*Service)

编写优秀的切入点

在编译期间,AspectJ 会处理切入点以优化匹配性能。检查代码并确定每个连接点是否匹配切入点是一个代价高昂的过程。为了获得最佳性能,你应该尽可能缩小搜索空间。

切入点指示符自然分为三组:Kinded(类型化)、Scoping(作用域)和 Contextual(上下文):

  1. Kinded 指示符:选择特定类型的连接点,如 execution
  2. Scoping 指示符:选择一组感兴趣的连接点,如 within
  3. Contextual 指示符:基于上下文匹配,如 this, target@annotation

编写良好的切入点应至少包含前两种类型(Kinded 和 Scoping)。Scoping 指示符匹配非常快,使用它们意味着 AspectJ 可以非常快地排除不应进一步处理的连接点组。仅提供 Kinded 指示符或仅提供 Contextual 指示符也是可以工作的,但由于额外的处理和分析,可能会影响性能(内存和时间)。


补充教学

1. 掌握 execution 表达式的通配符技巧

  • *:匹配一个任意部分(如一级包名、一个方法名、一个返回类型)。
  • ..
    • 在包路径中:匹配当前包及其所有子包。
    • 在参数列表中:匹配零个或多个任意类型的参数。

2. within vs execution:谁更高效?

  • within颗粒度较粗的作用域拦截。它直接定位到“类”级别。
  • execution颗粒度较细的方法拦截。 在性能敏感的场景下,先使用 within 缩小范围是非常明智的。例如:@Pointcut("within(com.example.service..*) && execution(* *(..))")

3. this vs target 的本质区别(面试常客)

由于 Spring AOP 基于代理:

  • this(Type):匹配的是代理对象(Proxy)是否为 Type 类型。
  • target(Type):匹配的是目标对象(Target,即你写的那个原始类)是否为 Type 类型。 在 JDK 代理下,代理对象和目标对象都实现了相同的接口,表现一致;但在 CGLIB 代理下或复杂的继承体系中,二者会有细微差异。

4. 注解拦截:现代开发的“标准姿势”

虽然按包名拦截很常见,但这种“一刀切”的方式往往不够灵活。现代 Spring 开发中,更推荐使用注解拦截

java
// 拦截所有带有自定义权限注解的方法
@Pointcut("@annotation(com.example.security.RequiresPermission)")
public void permissionCheck() {}

这种方式具有极强的自解释性,且代码与切面之间的耦合是显性的。

5. 自调用的“坑”:AOP 为什么不生效?

这是 AOP 初学者最常遇到的问题。

java
public class MyService {
    public void a() {
        this.b(); // 这里的 b() 调用不会被 AOP 拦截!
    }
    @Log
    public void b() { ... }
}

原因this.b() 是通过原始对象直接调用的,没有经过 Spring 创建的代理对象。AOP 的逻辑织入在代理对象中,绕过代理就绕过了 AOP。 对策

  1. 重构代码,将 b() 逻辑移到另一个 Bean。
  2. 使用 AopContext.currentProxy()(不推荐,具有侵入性)。
  3. 切换到 AspectJ 静态织入。

Based on Spring Framework.