Spring中的Pointcut API
本节描述了Spring如何处理关键的切点(pointcut)概念。
概念
Spring的切点模型(pointcut model)使得切点的复用独立于通知类型(advice types)。你可以使用同一个切点来针对不同的通知。
org.springframework.aop.Pointcut接口是核心接口,用于将通知(advise)应用到特定的类和方法上。该接口的完整定义如下:
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方法永远不会被调用。
如果可能的话,尽量使切点(pointcuts)保持静态,这样当创建AOP代理时,AOP框架就可以缓存切点评估的结果。
对切点的操作
Spring支持对切点(pointcuts)进行操作(尤其是并集和交集操作)。
“Union”指的是至少有一个切点匹配到的方法。“Intersection”指的是两个切点都匹配到的方法。通常情况下,“Union”更为实用。你可以通过使用org.springframework.aop.support.Pointcuts类中的静态方法,或者使用同一包中的ComposablePointcut类来组合切点。不过,使用AspectJ的切点表达式通常是一种更简单的做法。
AspectJ 表达式切点
从Spring 2.0开始,Spring使用的最重要的类型点切(pointcut)是org.springframework.aop.aspectj.AspectJExpressionPointcut。这是一种点切,它利用AspectJ提供的库来解析AspectJ点切表达式字符串。
有关支持的 AspectJ 切点原语的讨论,请参阅前一章。
便利性切点实现
Spring提供了几种方便的切点(pointcut)实现。你可以直接使用其中的一些;其他的则是为了在特定于应用的切点中被继承而设计的。
静态切点
静态切点是基于方法和目标类来定义的,因此无法考虑方法的参数。对于大多数使用场景来说,静态切点已经足够了,也是最佳选择。Spring 只能在方法首次被调用时评估一次静态切点;之后,在每次方法调用时都不需要再次评估该切点。
本节的其余部分将描述Spring中包含的一些静态切点实现。
正则表达式切点
指定静态切点的一种明显方法是使用正则表达式。除了Spring之外,还有几个AOP框架也支持这种方式。org.springframework.aop.support.JdkRegexpMethodPointcut是一种通用的正则表达式切点,它利用了JDK中的正则表达式支持功能。
使用JdkRegexpMethodPointcut类,你可以提供一组模式字符串。如果其中有任何一个匹配,那么该切点(pointcut)的评估结果就为true。(因此,最终的切点实际上就是所指定模式的并集。)
以下示例展示了如何使用 JdkRegexpMethodPointcut:
- Java
- Kotlin
- Xml
@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(记住,Advice可以是拦截器、前置通知、后置通知等)。在幕后,Spring实际上使用的是JdkRegexpMethodPointcut。使用RegexpMethodPointcutAdvisor可以简化配置,因为这个Bean封装了切点和通知两个功能,如下例所示:
- Java
- Kotlin
- Xml
@Configuration
public class RegexpConfiguration {
@Bean
public RegexpMethodPointcutAdvisor settersAndAbsquatulateAdvisor(Advice beanNameOfAopAllianceInterceptor) {
RegexpMethodPointcutAdvisor advisor = new RegexpMethodPointcutAdvisor();
advisor.setAdvice(beanNameOfAopAllianceInterceptor);
advisor.setPatterns(".*set.*", ".*absquatulate");
return advisor;
}
}
@Configuration
class RegexpConfiguration {
@Bean
fun settersAndAbsquatulateAdvisor(beanNameOfAopAllianceInterceptor: Advice) = RegexpMethodPointcutAdvisor().apply {
advice = beanNameOfAopAllianceInterceptor
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>
你可以将RegexpMethodPointcutAdvisor与任何类型的Advice一起使用。
属性驱动的切点
一种重要的静态切点类型是元数据驱动的切点。这种切点利用元数据属性的值(通常是源代码级别的元数据)。
动态切点
动态切点(dynamic pointcuts)的评估成本比静态切点(static pointcuts)更高。它们不仅考虑静态信息,还考虑方法参数。这意味着每次方法调用时都必须进行评估,且评估结果无法被缓存,因为参数会发生变化。
主要例子是“控制流”切点(control flow pointcut)。
控制流切点
Spring的控制流切点(control flow pointcuts)在概念上与AspectJ的cflow切点类似,尽管功能稍弱。(目前还没有办法指定一个切点在另一个切点匹配的连接点(join point)之后执行。)控制流切点会匹配当前的调用栈(call stack)。例如,如果连接点是由com.mycompany.web包中的方法或SomeCaller类调用的,那么控制流切点就可能被触发。控制流切点是使用org.springframework.aop.support.ControlFlowPointcut类来指定的。
在运行时评估控制流断点(control flow pointcuts)的成本,甚至比其他动态断点(dynamic pointcuts)的代价还要高。在Java 1.4中,其成本大约是其他动态断点的五倍。
Pointcut Superclasses
Spring提供了有用的切点超类(pointcut superclasses),以帮助你实现自己的切点(pointcuts)。
因为静态切点(static pointcuts)最为实用,你可能应该继承StaticMethodMatcherPointcut类。这样做只需要实现一个抽象方法(尽管你也可以覆盖其他方法来定制行为)。以下示例展示了如何继承StaticMethodMatcherPointcut:
- Java
- Kotlin
class TestStaticPointcut extends StaticMethodMatcherPointcut {
public boolean matches(Method m, Class targetClass) {
// return true if custom criteria match
}
}
class TestStaticPointcut : StaticMethodMatcherPointcut() {
override fun matches(method: Method, targetClass: Class<*>): Boolean {
// return true if custom criteria match
}
}
还有用于动态切点的超类。你可以将自定义切点与任何类型的建议(advice)一起使用。
自定义切点
因为在Spring AOP中,切点(pointcuts)是Java类而不是语言特性(如AspectJ中的那样),所以你可以声明自定义的切点,无论是静态的还是动态的。Spring中的自定义切点可以具有任意复杂的结构。然而,如果可能的话,我们建议使用AspectJ的切点表达式语言。
Spring的后续版本可能会支持JAC提供的“语义切入点(semantic pointcuts)”——例如,“所有修改目标对象实例变量的方法”。