Skip to content

Spring 中的切入点 (Pointcut) API

本节描述了 Spring 如何处理至关重要的切入点(Pointcut)概念。

核心概念

Spring 的切入点模型能够使切入点重用独立于通知(Advice)类型。你可以使用相同的切入点来定位不同的通知。

org.springframework.aop.Pointcut 是核心接口,用于将通知定位到特定的类和方法。完整接口如下:

java
public interface Pointcut {

	ClassFilter getClassFilter();

	MethodMatcher getMethodMatcher();
}

Pointcut 接口拆分为两部分允许重用类和方法匹配部分,并进行细粒度的组合操作(例如与另一个方法匹配器执行“并集”操作)。

ClassFilter 接口用于将切入点限制在一组给定的目标类中。如果 matches() 方法始终返回 true,则匹配所有目标类。以下列表显示了 ClassFilter 接口定义:

java
public interface ClassFilter {

	boolean matches(Class clazz);
}

MethodMatcher 接口通常更为重要。完整接口如下:

java
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,并且 MethodMatcherisRuntime() 方法返回 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。(其结果是,最终得到的切入点实际上是指定模式的并集。)

java
@Configuration
public class JdkRegexpConfiguration {

	@Bean
	public JdkRegexpMethodPointcut settersAndAbsquatulatePointcut() {
		JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut();
		pointcut.setPatterns(".*set.*", ".*absquatulate");
		return pointcut;
	}
}
kotlin
@Configuration
class JdkRegexpConfiguration {

	@Bean
	fun settersAndAbsquatulatePointcut() = JdkRegexpMethodPointcut().apply {
		setPatterns(".*set.*", ".*absquatulate")
	}
}
xml
<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 就封装了切入点和通知。

java
@Configuration
public class RegexpConfiguration {

	@Bean
	public RegexpMethodPointcutAdvisor settersAndAbsquatulateAdvisor(Advice interceptor) {
		RegexpMethodPointcutAdvisor advisor = new RegexpMethodPointcutAdvisor();
		advisor.setAdvice(interceptor);
		advisor.setPatterns(".*set.*", ".*absquatulate");
		return advisor;
	}
}
kotlin
@Configuration
class RegexpConfiguration {

	@Bean
	fun settersAndAbsquatulateAdvisor(interceptor: Advice) = RegexpMethodPointcutAdvisor().apply {
		advice = interceptor
		setPatterns(".*set.*", ".*absquatulate")
	}
}
xml
<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。这只需要实现一个抽象方法(尽管你可以重写其他方法来定制行为):

java
class TestStaticPointcut extends StaticMethodMatcherPointcut {

	public boolean matches(Method m, Class targetClass) {
		// 如果自定义标准匹配,则返回 true
		return m.getName().startsWith("get");
	}
}
kotlin
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) 快速创建一个“仅拦截特定包下的日志”的新切点,而无需重新写表达式。

Based on Spring Framework.