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

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.