Spring 中的切入点 (Pointcut) API
本节描述了 Spring 如何处理至关重要的切入点(Pointcut)概念。
核心概念
Spring 的切入点模型能够使切入点重用独立于通知(Advice)类型。你可以使用相同的切入点来定位不同的通知。
org.springframework.aop.Pointcut 是核心接口,用于将通知定位到特定的类和方法。完整接口如下:
public interface Pointcut {
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
}将 Pointcut 接口拆分为两部分允许重用类和方法匹配部分,并进行细粒度的组合操作(例如与另一个方法匹配器执行“并集”操作)。
ClassFilter 接口用于将切入点限制在一组给定的目标类中。如果 matches() 方法始终返回 true,则匹配所有目标类。以下列表显示了 ClassFilter 接口定义:
public interface ClassFilter {
boolean matches(Class clazz);
}MethodMatcher 接口通常更为重要。完整接口如下:
public interface MethodMatcher {
boolean matches(Method m, Class<?> targetClass);
boolean isRuntime();
boolean matches(Method m, Class<?> targetClass, Object... args);
}matches(Method, Class) 方法用于测试此切入点是否匹配目标类上的给定方法。该评估可以在创建 AOP 代理时执行,以避免在每次方法调用时都需要进行测试。如果双参数 matches 方法对给定方法返回 true,并且 MethodMatcher 的 isRuntime() 方法返回 true,那么三参数 matches 方法将在每次方法调用时被调用。这让切入点在目标通知开始之前,能够查看传递给方法调用的参数。
大多数 MethodMatcher 实现都是静态的,这意味着它们的 isRuntime() 方法返回 false。在这种情况下,三参数 matches 方法永远不会被调用。
提示
如果可能,尽量使切入点成为静态的,这样 AOP 框架就可以在创建 AOP 代理时缓存切入点评估的结果。
切入点操作
Spring 支持对切入点进行操作(特别是:并集和交集)。
- 并集(Union):匹配任何一个切入点所匹配的方法。
- 交集(Intersection):匹配两个切入点同时匹配的方法。
并集通常更有用。你可以使用 org.springframework.aop.support.Pointcuts 类中的静态方法,或者使用同一包中的 ComposablePointcut 类来组合切入点。然而,使用 AspectJ 切入点表达式通常是更简单的方法。
AspectJ 表达式切入点
自 2.0 以来,Spring 使用的最重要切入点类型是 org.springframework.aop.aspectj.AspectJExpressionPointcut。这是一个使用 AspectJ 提供的库来解析 AspectJ 切入点表达式字符串的切入点。
有关受支持的 AspectJ 切入点原语的讨论,请参阅前一章。
便捷的切入点实现
Spring 提供了几个便捷的切入点实现。你可以直接使用其中一些;另一些则旨在作为应用特定切入点的超类。
静态切入点 (Static Pointcuts)
静态切入点基于方法和目标类,不考虑方法的参数。对于大多数用途,静态切入点已经足够——而且是最好的。Spring 仅在第一次调用方法时评估静态切入点。此后,无需在每次方法调用时再次评估。
以下是 Spring 包含的一些静态切入点实现:
正则表达式切入点
指定静态切入点的一种显而易见的方法是正则表达式。除 Spring 之外,还有几个 AOP 框架也实现了这一点。org.springframework.aop.support.JdkRegexpMethodPointcut 是一个使用 JDK 正则表达式支持的通用正则表达式切入点。
你可以提供一系列模式字符串。如果其中任何一个匹配,则切入点评估为 true。(其结果是,最终得到的切入点实际上是指定模式的并集。)
@Configuration
public class JdkRegexpConfiguration {
@Bean
public JdkRegexpMethodPointcut settersAndAbsquatulatePointcut() {
JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut();
pointcut.setPatterns(".*set.*", ".*absquatulate");
return pointcut;
}
}@Configuration
class JdkRegexpConfiguration {
@Bean
fun settersAndAbsquatulatePointcut() = JdkRegexpMethodPointcut().apply {
setPatterns(".*set.*", ".*absquatulate")
}
}<bean id="settersAndAbsquatulatePointcut"
class="org.springframework.aop.support.JdkRegexpMethodPointcut">
<property name="patterns">
<list>
<value>.*set.*</value>
<value>.*absquatulate</value>
</list>
</property>
</bean>Spring 提供了一个名为 RegexpMethodPointcutAdvisor 的便捷类,它允许我们引用一个 Advice。在底层,Spring 使用 JdkRegexpMethodPointcut。使用 RegexpMethodPointcutAdvisor 简化了装配,因为一个 Bean 就封装了切入点和通知。
@Configuration
public class RegexpConfiguration {
@Bean
public RegexpMethodPointcutAdvisor settersAndAbsquatulateAdvisor(Advice interceptor) {
RegexpMethodPointcutAdvisor advisor = new RegexpMethodPointcutAdvisor();
advisor.setAdvice(interceptor);
advisor.setPatterns(".*set.*", ".*absquatulate");
return advisor;
}
}@Configuration
class RegexpConfiguration {
@Bean
fun settersAndAbsquatulateAdvisor(interceptor: Advice) = RegexpMethodPointcutAdvisor().apply {
advice = interceptor
setPatterns(".*set.*", ".*absquatulate")
}
}<bean id="settersAndAbsquatulateAdvisor"
class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<property name="advice">
<ref bean="beanNameOfAopAllianceInterceptor"/>
</property>
<property name="patterns">
<list>
<value>.*set.*</value>
<value>.*absquatulate</value>
</list>
</property>
</bean>属性驱动的切入点
静态切入点的一个重要类型是元数据驱动(通常是注解驱动)的切入点。它使用元数据属性的值(通常是源码级注解)。
动态切入点 (Dynamic pointcuts)
动态切入点的评估成本比静态切入点高。它们既考虑静态信息,也考虑方法参数。这意味着它们必须在每次方法调用时全量评估,且由于参数会变化,结果无法缓存。
主要示例是 控制流 (Control Flow) 切入点。
控制流切入点
Spring 的控制流切入点在概念上类似于 AspectJ 的 cflow 切入点,但功能较弱。控制流切入点匹配当前的调用栈。例如,如果连接点是由 com.mycompany.web 包中的方法或 SomeCaller 类调用的,它可能会触发。控制流切入点通过 org.springframework.aop.support.ControlFlowPointcut 类指定。
TIP
控制流切入点在运行时的评估成本甚至显著高于其他动态切入点。
切入点超类
Spring 提供了有用的切入点超类,帮助你实现自己的切入点。
由于静态切入点最有用,你可能应该继承 StaticMethodMatcherPointcut。这只需要实现一个抽象方法(尽管你可以重写其他方法来定制行为):
class TestStaticPointcut extends StaticMethodMatcherPointcut {
public boolean matches(Method m, Class targetClass) {
// 如果自定义标准匹配,则返回 true
return m.getName().startsWith("get");
}
}class TestStaticPointcut : StaticMethodMatcherPointcut() {
override fun matches(method: Method, targetClass: Class<*>): Boolean {
// 如果自定义标准匹配,则返回 true
return method.name.startsWith("get")
}
}自定义切入点
由于 Spring AOP 中的切入点是 Java 类,而不是语言特性(如 AspectJ),因此你可以声明自定义切入点,无论是静态还是动态。Spring 中的自定义切入点可以极其复杂。然而,如果可以的话,我们推荐使用 AspectJ 切入点表达式语言。
补充教学
1. 深度剖析:Static vs Dynamic 点切入
这是性能调优的关键点:
- 静态 (Static):在代理创建阶段(或方法第一次调用时)只匹配一次。Spring 会将匹配结果缓存起来。下次调用该方法,直接执行拦截器链。
- 动态 (Dynamic):每次执行方法都会调用
matches(Method, Class, Object[] args)。因为它要看参数。- 业务陷阱:除非业务逻辑真的依赖于入参值来决定是否拦截(例如:仅拦截当
id > 1000的方法),否则绝不要使用动态点切入。
- 业务陷阱:除非业务逻辑真的依赖于入参值来决定是否拦截(例如:仅拦截当
2. 控制流切入点 (ControlFlowPointcut) 的“雷区”
ControlFlowPointcut 允许你实现这样的逻辑:“仅当 ServiceA 调用 ServiceB 时才触发拦截”。
- 原理:它必须在每次执行时通过
new Throwable().getStackTrace()(或内部类似机制)去检查线程调用栈。 - 后果:获取调用栈是一个极其重的 CPU 操作。在生产环境的高并发接口中使用它,性能可能会下降数倍。
3. ClassFilter 的妙用
虽然我们大多关注 MethodMatcher,但 ClassFilter 在大规模 AOP 中非常有用。 如果你想跳过整个包或某个特定接口的所有实现类,先在 ClassFilter 里返回 false,Spring 就不会去扫描该类里的任何方法,这能显著加快代理创建的速度。
4. 组合模式 (Composite Pattern)
Spring 提供了 Pointcuts.union() 和 Pointcuts.intersection()。 如果你已经定义了一个 PointcutA(拦截日志)和一个 PointcutB(拦截特定包),你可以通过 Pointcuts.intersection(A, B) 快速创建一个“仅拦截特定包下的日志”的新切点,而无需重新写表达式。