深圳幻海软件技术有限公司 欢迎您!

Spring AOP之切入点Pointcut API详细介绍及使用

2023-02-28

概述在工作中用的最多的就是通过@Aspect实现AOP功能;要在Spring配置中使用@Aspect切面,需要启用Spring支持,以便基于@Aspect切面配置SpringAOP,并根据条件自动代理bean。通过自动代理,如果Spring确定某个bean符合一个或多个切面的建议,它会自动为该bea

概述

在工作中用的最多的就是通过@Aspect实现AOP功能;要在Spring配置中使用@Aspect切面,需要启用Spring支持,以便基于@Aspect切面配置Spring AOP,并根据条件自动代理bean。通过自动代理,如果Spring确定某个bean符合一个或多个切面的建议,它会自动为该bean生成一个代理来拦截方法调用,并确保按需运行通知。

可以通过XML或java风格的配置启用@AspectJ支持。在这两种情况下,还需要确保AspectJ的aspectjweaver.jar库位于应用程序的类路径上(版本1.8或更高)。

通过注解方式开启@Aspect支持

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
  • 1.
  • 2.
  • 3.
  • 4.

通过XML开启@Aspect支持。

<aop:aspectj-autoproxy/>
  • 1.

定义AspectJ切面。

package com.pack.aspect;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class CustomAspect {
  // 定义切入点
  @Pointcut("execution(* com.pack.service..*.(..))")
  private void log() {}
  // 定义通知
  @Before("log()")
  // @Before("execution(* com.pack.service..*.(..))")  也可以直接这样写
  public void recordLogBefore() {  
    // ...
  }
  @AfterReturning("log()")
  public void recordLogAfter(){
    // ...
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

上面简单回顾了在工作中使用@Aspect定义切面实现AOP功能。

Spring AOP API

  • Spring中的切入点API

Spring的切入点模型支持独立于通知类型的切入点重用。可以使用相同的切入点定位不同的通知。

pointcut接口是中心接口,用于为特定类和方法提供建议。完整的接口如下:

public interface Pointcut {

  ClassFilter getClassFilter();

  MethodMatcher getMethodMatcher();
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

将切入点接口拆分为两个部分允许重用类和方法匹配部分以及细粒度的组合操作。

ClassFilter接口用于将切入点限制为给定的目标类集。如果matches()方法总是返回true,则匹配所有目标类。ClassFilter接口的定义如下列代码清单所示:

public interface ClassFilter {

  boolean matches(Class clazz);
}
  • 1.
  • 2.
  • 3.
  • 4.

该类专门用来匹配每一个Bean是否符合条件,只有匹配了才可为其创建代理。

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

public interface MethodMatcher {
  /**
   * 在运行目标类的方法时判断当前的执行的方法是否匹配,如果匹配才会执行
   * 相关的通知
   */
  boolean matches(Method m, Class<?> targetClass);
  /**
   * 上面2个参数的matches返回true才会执行isRuntime
   * 该方法的返回值决定了下面3个参数的matches方法是否会被执行
   * 如果返回true,才会进行下面3个参数的执行。返回false将不会执行下面方法
   */
  boolean isRuntime();
  /**
   * 该方法是否会被执行是由上面的isRuntime方法决定,只有返回true才会执行
   * 如果isRuntime方法返回true,那么会将每一个Advisor中定义的通知
   * (这些通知会被转换为MethodInterceptor)包装为InterceptorAndDynamicMethodMatcher
   * 最后在通过ReflectiveMethodInvocation执行时会判断当前对象如果为ReflectiveMethodInvocation
   * 则进行MethodMatcher3个参数的matches调用,这里就可以对参数进行相应的校验判断,
   * 是否进行通知的继续调用,如果匹配则调用当前的MethodInterceptor,否则直接调用下一个
   */
  boolean matches(Method m, Class<?>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 对切入点的操作

Spring支持切入点上的操作(特别是union和intersection)。Union表示任意一个切入点匹配的方法。交集意味着两个切入点匹配的方法。Union通常更有用。可以使用org.springframework.aop.support.Pointcuts类中的静态方法来组合切入点,也可以使用同一个包中的 ComposablePointcut类。然而,使用AspectJ切入点表达式通常是一种更简单的方法。

Union表示了多个Pointcut都需要匹配才算匹配。

public abstract class Pointcuts {
  public static Pointcut union(Pointcut pc1, Pointcut pc2){
    return new ComposablePointcut(pc1).union(pc2);
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

ComposablePointcut

public class ComposablePointcut implements Pointcut, Serializable {
  private ClassFilter classFilter;
  private MethodMatcher methodMatcher;
  public ComposablePointcut(Pointcut pointcut) {
    this.classFilter = pointcut.getClassFilter();
    this.methodMatcher = pointcut.getMethodMatcher();
  }
  public ComposablePointcut union(Pointcut other) {
    this.methodMatcher = MethodMatchers.union(this.methodMatcher, this.classFilter, other.getMethodMatcher(), other.getClassFilter());
    this.classFilter = ClassFilters.union(this.classFilter, other.getClassFilter());
    return this;
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

MethodMatchers.union

static MethodMatcher union(MethodMatcher mm1, ClassFilter cf1, MethodMatcher mm2, ClassFilter cf2){
  return (mm1 instanceof IntroductionAwareMethodMatcher || mm2 instanceof IntroductionAwareMethodMatcher ?
          new ClassFilterAwareUnionIntroductionAwareMethodMatcher(mm1, cf1, mm2, cf2) :
          new ClassFilterAwareUnionMethodMatcher(mm1, cf1, mm2, cf2));
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

如上假设返回ClassFilterAwareUnionMethodMatcher。

private static class ClassFilterAwareUnionMethodMatcher extends UnionMethodMatcher {

  private final ClassFilter cf1;

  private final ClassFilter cf2;

  public ClassFilterAwareUnionMethodMatcher(MethodMatcher mm1, ClassFilter cf1, MethodMatcher mm2, ClassFilter cf2){
    super(mm1, mm2);
    this.cf1 = cf1;
    this.cf2 = cf2;
  }
}
private static class UnionMethodMatcher implements MethodMatcher, Serializable {
  // 最终的核心就是分别判断两个Pointcut对应的ClassFilter,MethodMatcher
  // 只要其中一个返回true即可
  public boolean matches(Method method, Class<?> targetClass){
    return (matchesClass1(targetClass) && this.mm1.matches(method, targetClass)) || 
      (matchesClass2(targetClass) && this.mm2.matches(method, targetClass));
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 方便的切入点实现

Spring为我们提供了几个便捷的切入点实现类可以直接使用。

静态切入点,静态切入点基于方法和目标类,不能考虑方法的参数。对于大多数用法,静态切入点就足够了,而且是最好的。Spring只能在方法第一次被调用时对静态切入点进行一次评估。之后,就不需要对每个方法调用再次评估切入点了。

正则表达式切点

指定静态切入点的一个明显方法是正则表达式。除了Spring之外,还有几个AOP框架使之成为可能。org.springframework.aop.support.JdkRegexpMethodPointcut是一个通用正则表达式切入点,它使用JDK中的正则表达式支持。

使用JdkRegexpMethodPointcut类,可以提供一组模式字符串。如果其中任何一个匹配,切入点计算为true。

<bean id="staticRegexpPoint"class="org.springframework.aop.support.JdkRegexpMethodPointcut">
  <property name="patterns">
    <list>
      <value>.*set.*</value>
      <value>.*save</value>
    </list>
  </property>
  <property name="advice">
    <ref bean="logAdvice"/>
  </property>
</bean>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

动态切入点

动态切入点的评估成本比静态切入点高。它们既考虑了方法参数,也考虑了静态信息。这意味着每次方法调用都必须计算它们,而且结果不能缓存,因为参数不同。

核心切入点类:ControlFlowPointcut​。

public class ControlFlowPointcut implements Pointcut, ClassFilter, MethodMatcher, Serializable {
  private final Class<?> clazz;
  private final String methodName;
  /**
   * 构造一个新的切入点,它匹配给定类中给定方法下面的所有调用。
   * 如果没有给出方法名,则匹配给定类下的所有控制流。
    */
  public ControlFlowPointcut(Class<?> clazz, @Nullable String methodName) {
    this.clazz = clazz;
    this.methodName = methodName;
  }

  public boolean matches(Method method, Class<?> targetClass) {
    return true;
  }
  @Override
  public boolean isRuntime() {
    return true;
  }
  @Override
  public boolean matches(Method method, Class<?> targetClass, Object... args) {
    // 遍历当前的执行栈中的所有方法是否有匹配当前在构造方法中传入的方法名,有则进行相应的通知调用
    // 简单点说就是:拦截任何被创建代理类的方法,如果这些方法在执行过程中有调用构造参数中传入的Class 和Method那么就是匹配的
    for (StackTraceElement element : new Throwable().getStackTrace()) {
      if (element.getClassName().equals(this.clazz.getName()) && 
          (this.methodName == null || element.getMethodName().equals(this.methodName))) {
        return true;
      }
    }
    return false;
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.

Pointcut超类​

Spring提供了有用的切入点超类来帮助您实现自己的切入点。

因为静态切入点最有用,你可能应该子类化StaticMethodMatcherPointcut。这只需要实现一个抽象方法(尽管你可以覆盖其他方法来定制行为)。下面的例子展示了如何子类化StaticMethodMatcherPointcut:

public class CustomStaticPointcut extends StaticMethodMatcherPointcut {
  public boolean matches(Method m, Class targetClass){
    // return true if custom criteria match
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.