声明切入点 (Declaring a Pointcut)
切入点(Pointcuts)决定了我们感兴趣的连接点(Join points),从而使我们能够控制通知(Advice)在何时运行。Spring AOP 仅支持 Spring Bean 的方法执行连接点,因此你可以将切入点视为匹配 Spring Bean 上方法的执行。
切入点声明由两部分组成:
- 切入点签名(Signature):包含名称和任何参数。
- 切入点表达式(Expression):确定我们究竟对哪些方法执行感兴趣。
在 @AspectJ 注解风格的 AOP 中,切入点签名由一个常规的方法定义提供,而切入点表达式通过使用 @Pointcut 注解来表示(作为切入点签名的方法必须具有 void 返回类型)。
以下示例定义了一个名为 anyOldTransfer 的切入点,它匹配任何名为 transfer 的方法的执行:
@Pointcut("execution(* transfer(..))") // 切入点表达式
private void anyOldTransfer() {} // 切入点签名@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 本身具有基于类型的语义,在执行连接点处,this 和 target 都指向同一个对象:执行方法的对象。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 的形式如下:
bean(idOrNameOfBean)idOrNameOfBean 可以是任何 Spring Bean 的名称。支持使用 * 字符的有限通配符。与其他 PCD 一样,bean PCD 也可以与 &&(与)、||(或)和 !(非)运算符一起使用。
TIP
bean PCD 仅在 Spring AOP 中受支持,在原生 AspectJ 织入中不受支持。它是 Spring 对 AspectJ 定义的标准 PCD 的特定扩展。
组合切入点表达式
你可以使用 &&, || 和 ! 来组合切入点表达式。你也可以通过名称引用切入点表达式。以下示例展示了三个切入点表达式:
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() {}
}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 示例:
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() {}
}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 层具备事务性:
<aop:config>
<aop:advisor
pointcut="com.xyz.CommonPointcuts.businessService()"
advice-ref="tx-advice"/>
</aop:config>常见示例
Spring AOP 用户最常使用的是 execution 切入点指示符。其格式如下:
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) - 传递的运行时参数是
Serializable:args(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(上下文):
- Kinded 指示符:选择特定类型的连接点,如
execution。 - Scoping 指示符:选择一组感兴趣的连接点,如
within。 - 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 开发中,更推荐使用注解拦截:
// 拦截所有带有自定义权限注解的方法
@Pointcut("@annotation(com.example.security.RequiresPermission)")
public void permissionCheck() {}这种方式具有极强的自解释性,且代码与切面之间的耦合是显性的。
5. 自调用的“坑”:AOP 为什么不生效?
这是 AOP 初学者最常遇到的问题。
public class MyService {
public void a() {
this.b(); // 这里的 b() 调用不会被 AOP 拦截!
}
@Log
public void b() { ... }
}原因:this.b() 是通过原始对象直接调用的,没有经过 Spring 创建的代理对象。AOP 的逻辑织入在代理对象中,绕过代理就绕过了 AOP。 对策:
- 重构代码,将
b()逻辑移到另一个 Bean。 - 使用
AopContext.currentProxy()(不推荐,具有侵入性)。 - 切换到 AspectJ 静态织入。