Spring AOP 中,切点有多少种定义方式?

2023年 7月 28日 145.9k 0

在 Spring AOP 中,我们最常用的切点定义方式主要是两种:

  • 使用 execution 进行无侵入拦截。
  • 使用注解进行拦截。
  • 这应该是是小伙伴们日常工作中使用最多的两种切点定义方式了。但是除了这两种还有没有其他的呢?今天松哥就来和大家聊一聊这个话题。

    1. Pointcut 分类

    来看下 Pointcut 的定义:

    public interface Pointcut {
     ClassFilter getClassFilter();
     MethodMatcher getMethodMatcher();
     Pointcut TRUE = TruePointcut.INSTANCE;
    }

    从方法名上就能看出来,getClassFilter 进行类的过滤,getMethodMatcher 进行方法过滤。通过过滤 Class 和过滤 Method,我们就能够锁定一个拦截对象了。

    再来看下 Pointcut 类的继承关系图:

    图片图片

    可以看到,其实实现类不算多,大部分看名字其实都能猜出来是干嘛的,这些实现类我们大致上又可以分为六大类:

  • 静态方法切点:StaticMethodMatcherPointcut 表示静态方法切点的抽象基类,默认情况下匹配所有的类,然后通过不同的规则去匹配不同的方法。
  • 动态方法切点:DynamicMethodMatcherPointcut 表示动态方法切点的抽象基类,默认情况下它匹配所有的类,然后通过不同的规则匹配不同的方法,这个有点类似于 StaticMethodMatcherPointcut,不同的是,StaticMethodMatcherPointcut 仅对方法签名进行匹配并且仅匹配一次,而 DynamicMethodMatcherPointcut 会在运行期间检查方法入参的值,由于每次传入的参数可能都不一样,所以没调用都要去判断,因此就导致 DynamicMethodMatcherPointcut 的性能差一些。
  • 注解切点:AnnotationMatchingPointcut。
  • 表达式切点:ExpressionPointcut。
  • 流程切点:ControlFlowPointcut。
  • 复合切点:ComposablePointcut。
  • 除了上面这六个之外,另外还有一个落单的 TruePointcut,这个从名字上就能看出来是拦截一切。

    所以满打满算,有七种类型的切点,接下来我们就来逐个分析一下。

    2. TruePointcut

    这个实现类从名字上看就是拦截所有,拦截一切,我们来看下这个类怎么做的:

    final class TruePointcut implements Pointcut, Serializable {
        //...
     @Override
     public ClassFilter getClassFilter() {
      return ClassFilter.TRUE;
     }
    
     @Override
     public MethodMatcher getMethodMatcher() {
      return MethodMatcher.TRUE;
     }
        //...
    }

    首先小伙伴们注意,这个类不是 public 的,所以意味着我们自己开发中没法直接使用这个切点。然后大家看到,在 getClassFilter 和 getMethodMatcher 方法中,这里都返回了对应的 TRUE,而这两个 TRUE 实现非常简单,就是在需要做比对的地方,不做任何比对,直接返回 true 即可,这就导致了最终将所有东西都拦截下来。

    3. StaticMethodMatcherPointcut

    StaticMethodMatcherPointcut 仅对方法名签名(包括方法名和入参类型及顺序)进行匹配,静态匹配仅判断一次。

    public abstract class StaticMethodMatcherPointcut extends StaticMethodMatcher implements Pointcut {
    
     private ClassFilter classFilter = ClassFilter.TRUE;
    
    
     /**
      * Set the {@link ClassFilter} to use for this pointcut.
      * Default is {@link ClassFilter#TRUE}.
      */
     public void setClassFilter(ClassFilter classFilter) {
      this.classFilter = classFilter;
     }
    
     @Override
     public ClassFilter getClassFilter() {
      return this.classFilter;
     }
    
    
     @Override
     public final MethodMatcher getMethodMatcher() {
      return this;
     }
    
    }

    可以看到,这里类的匹配默认就是返回 true,方法的匹配则返回当前对象,也就是看具体的实现。

    StaticMethodMatcherPointcut 有几个写好的实现类,我们来看下。

    3.1 SetterPointcut

    看名字就知道,这个可以用来拦截所有的 set 方法:

    private static class SetterPointcut extends StaticMethodMatcherPointcut implements Serializable {
     public static final SetterPointcut INSTANCE = new SetterPointcut();
     @Override
     public boolean matches(Method method, Class targetClass) {
      return (method.getName().startsWith("set") &&
        method.getParameterCount() == 1 &&
        method.getReturnType() == Void.TYPE);
     }
     private Object readResolve() {
      return INSTANCE;
     }
     @Override
     public String toString() {
      return "Pointcuts.SETTERS";
     }
    }

    可以看到,方法的匹配就是断定当前方法是否为 set 方法,要求方法名以 set 开始,方法只有一个参数并且方法返回值为 null,精准定位到一个 set 方法。

    举一个使用的例子,如下:

    ProxyFactory proxyFactory = new ProxyFactory();
    proxyFactory.setTarget(new CalculatorImpl());
    proxyFactory.addInterface(ICalculator.class);
    proxyFactory.addAdvisor(new PointcutAdvisor() {
        @Override
        public Pointcut getPointcut() {
            return Pointcuts.SETTERS;
        }
        @Override
        public Advice getAdvice() {
            return new MethodInterceptor() {
                @Override
                public Object invoke(MethodInvocation invocation) throws Throwable {
                    Method method = invocation.getMethod();
                    String name = method.getName();
                    System.out.println(name + " 方法开始执行了。。。");
                    Object proceed = invocation.proceed();
                    System.out.println(name + " 方法执行结束了。。。");
                    return proceed;
                }
            };
        }
        @Override
        public boolean isPerInstance() {
            return true;
        }
    });
    ICalculator calculator = (ICalculator) proxyFactory.getProxy();
    calculator.setA(5);

    由于 SetterPointcut 是私有的,无法直接 new,可以通过工具类 Pointcuts 获取到实例。

    3.2 GetterPointcut

    GetterPointcut 和 SetterPointcut 类似,如下:

    private static class GetterPointcut extends StaticMethodMatcherPointcut implements Serializable {
     public static final GetterPointcut INSTANCE = new GetterPointcut();
     @Override
     public boolean matches(Method method, Class targetClass) {
      return (method.getName().startsWith("get") &&
        method.getParameterCount() == 0);
     }
     private Object readResolve() {
      return INSTANCE;
     }
     @Override
     public String toString() {
      return "Pointcuts.GETTERS";
     }
    }

    我觉得这个应该就不用过多解释了吧,跟前面的 SetterPointcut 类似,对照理解就行了。

    3.3 NameMatchMethodPointcut

    这个是根据方法名来做匹配。

    public class NameMatchMethodPointcut extends StaticMethodMatcherPointcut implements Serializable {
    
     private List mappedNames = new ArrayList();
     public void setMappedName(String mappedName) {
      setMappedNames(mappedName);
     }
     public void setMappedNames(String... mappedNames) {
      this.mappedNames = new ArrayList(Arrays.asList(mappedNames));
     }
     public NameMatchMethodPointcut addMethodName(String name) {
      this.mappedNames.add(name);
      return this;
     }
    
    
     @Override
     public boolean matches(Method method, Class targetClass) {
      for (String mappedName : this.mappedNames) {
       if (mappedName.equals(method.getName()) || isMatch(method.getName(), mappedName)) {
        return true;
       }
      }
      return false;
     }
     protected boolean isMatch(String methodName, String mappedName) {
      return PatternMatchUtils.simpleMatch(mappedName, methodName);
        }
    }

    可以看到,这个就是从外部传一个方法名称列表进来,然后在 matches 方法中进行匹配即可,匹配的时候直接调用 equals 方法进行匹配,如果 equals 方法没有匹配上,则调用 isMatch 方法进行匹配,这个最终调用到 PatternMatchUtils.simpleMatch 方法,这是 Spring 中提供的一个工具类,支持通配符的匹配。

    举个简单例子:

    ProxyFactory proxyFactory = new ProxyFactory();
    proxyFactory.setTarget(new CalculatorImpl());
    proxyFactory.addInterface(ICalculator.class);
    proxyFactory.addAdvisor(new PointcutAdvisor() {
        @Override
        public Pointcut getPointcut() {
            NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
            pointcut.setMappedNames("add","set*");
            return pointcut;
        }
        @Override
        public Advice getAdvice() {
            return new MethodInterceptor() {
                @Override
                public Object invoke(MethodInvocation invocation) throws Throwable {
                    Method method = invocation.getMethod();
                    String name = method.getName();
                    System.out.println(name + " 方法开始执行了。。。");
                    Object proceed = invocation.proceed();
                    System.out.println(name + " 方法执行结束了。。。");
                    return proceed;
                }
            };
        }
        @Override
        public boolean isPerInstance() {
            return true;
        }
    });
    ICalculator calculator = (ICalculator) proxyFactory.getProxy();
    calculator.add(3,4);
    calculator.minus(3, 4);
    calculator.setA(5);

    这里我设置的是拦截方法名为 add 或者方法名以 set 开头的方法。

    3.4 JdkRegexpMethodPointcut

    这个是支持通过正则去匹配方法名,匹配上的方法就会被拦截下来。

    public class JdkRegexpMethodPointcut extends AbstractRegexpMethodPointcut {
     private Pattern[] compiledPatterns = new Pattern[0];
     private Pattern[] compiledExclusionPatterns = new Pattern[0];
     @Override
     protected void initPatternRepresentation(String[] patterns) throws PatternSyntaxException {
      this.compiledPatterns = compilePatterns(patterns);
     }
     @Override
     protected void initExcludedPatternRepresentation(String[] excludedPatterns) throws PatternSyntaxException {
      this.compiledExclusionPatterns = compilePatterns(excludedPatterns);
     }
     @Override
     protected boolean matches(String pattern, int patternIndex) {
      Matcher matcher = this.compiledPatterns[patternIndex].matcher(pattern);
      return matcher.matches();
     }
     @Override
     protected boolean matchesExclusion(String candidate, int patternIndex) {
      Matcher matcher = this.compiledExclusionPatterns[patternIndex].matcher(candidate);
      return matcher.matches();
     }
     private Pattern[] compilePatterns(String[] source) throws PatternSyntaxException {
      Pattern[] destination = new Pattern[source.length];
      for (int i = 0; i < source.length; i++) {
       destination[i] = Pattern.compile(source[i]);
      }
      return destination;
     }
    }

    可以看到,这里实际上就是传入正则表达式,然后通过正则表达式去匹配方法名是否满足条件。正则表达式可以传入多个,系统会在 JdkRegexpMethodPointcut 的父类中进行遍历逐个进行匹配,我举一个例子:

    ProxyFactory proxyFactory = new ProxyFactory();
    proxyFactory.setTarget(new CalculatorImpl());
    proxyFactory.addInterface(ICalculator.class);
    proxyFactory.addAdvisor(new PointcutAdvisor() {
        @Override
        public Pointcut getPointcut() {
            JdkRegexpMethodPointcut pc = new JdkRegexpMethodPointcut();
            pc.setPatterns("org.javaboy.bean.aop3.ICalculator.set.*");
            pc.setExcludedPattern("org.javaboy.bean.aop3.ICalculator.setA");
            return pc;
        }
        @Override
        public Advice getAdvice() {
            return new MethodInterceptor() {
                @Override
                public Object invoke(MethodInvocation invocation) throws Throwable {
                    Method method = invocation.getMethod();
                    String name = method.getName();
                    System.out.println(name + " 方法开始执行了。。。");
                    Object proceed = invocation.proceed();
                    System.out.println(name + " 方法执行结束了。。。");
                    return proceed;
                }
            };
        }
        @Override
        public boolean isPerInstance() {
            return true;
        }
    });
    ICalculator calculator = (ICalculator) proxyFactory.getProxy();
    calculator.add(3,4);
    calculator.minus(3, 4);
    calculator.setA(5);

    上面这个例子也是拦截 setXXX 方法,不过需要注意的是,方法名匹配的时候使用的是方法的全路径。

    另外还需要注意,在配置匹配规则的时候,还可以设置 ExcludedPattern,实际上在匹配的时候,首先进行正向匹配,就是先看下方法名是否满足规则,如果满足,则方法名再和 ExcludedPattern 进行比对,如果不满足,则这个方法才会被确定下来要拦截。

    StaticMethodMatcherPointcut 中主要就给我们提供了这些规则。

    4. DynamicMethodMatcherPointcut

    这个是动态方法匹配的切点,默认也是匹配所有类,但是在方法匹配这个问题,每次都会匹配,我们来看下:

    public abstract class DynamicMethodMatcherPointcut extends DynamicMethodMatcher implements Pointcut {
    
     @Override
     public ClassFilter getClassFilter() {
      return ClassFilter.TRUE;
     }
    
     @Override
     public final MethodMatcher getMethodMatcher() {
      return this;
     }
    
    }

    可以看到,getClassFilter 直接返回 TRUE,也就是类就直接匹配了,getMethodMatcher 返回的则是当前对象,那是因为当前类实现了 DynamicMethodMatcher 接口,这就是一个方法匹配器:

    public abstract class DynamicMethodMatcher implements MethodMatcher {
    
     @Override
     public final boolean isRuntime() {
      return true;
     }
     @Override
     public boolean matches(Method method, Class targetClass) {
      return true;
     }
    
    }

    小伙伴们看到,这里 isRuntime 方法返回 true,该方法为 true,意味着三个参数的 matches 方法将会被调用,所以这里两个参数的 matches 方法可以直接返回 true,不做任何控制。

    当然,也可以在两个参数的 matches 方法中做一些前置的判断。

    我们来看一个简单例子:

    public class MyDynamicMethodMatcherPointcut extends DynamicMethodMatcherPointcut {
        @Override
        public boolean matches(Method method, Class targetClass) {
            return method.getName().startsWith("set");
        }
    
        @Override
        public boolean matches(Method method, Class targetClass, Object... args) {
            return method.getName().startsWith("set") && args.length == 1 && Integer.class.isAssignableFrom(args[0].getClass());
        }
    }

    在实际执行过程中,两个参数的 matches 方法返回 true,三个参数的 matches 方法才会执行,如果两个参数的 matches 方法返回 false,则三个参数的 matches 就不会执行了。所以也可以两个参数的 matches 方法直接固定返回 true,只在三个参数的 matches 方法中做匹配操作即可。

    然后使用这个切点:

    ProxyFactory proxyFactory = new ProxyFactory();
    proxyFactory.setTarget(new CalculatorImpl());
    proxyFactory.addInterface(ICalculator.class);
    proxyFactory.addAdvisor(new PointcutAdvisor() {
        @Override
        public Pointcut getPointcut() {
            return new MyDynamicMethodMatcherPointcut();
        }
        @Override
        public Advice getAdvice() {
            return new MethodInterceptor() {
                @Override
                public Object invoke(MethodInvocation invocation) throws Throwable {
                    Method method = invocation.getMethod();
                    String name = method.getName();
                    System.out.println(name + " 方法开始执行了。。。");
                    Object proceed = invocation.proceed();
                    System.out.println(name + " 方法执行结束了。。。");
                    return proceed;
                }
            };
        }
        @Override
        public boolean isPerInstance() {
            return true;
        }
    });
    ICalculator calculator = (ICalculator) proxyFactory.getProxy();
    calculator.add(3,4);
    calculator.minus(3, 4);
    calculator.setA(5);

    5. AnnotationMatchingPointcut

    这个就是判断类或者方法上是否存在某个注解,如果存在,则将之拦截下来,否则不拦截。

    先来看下这个类的定义:

    public class AnnotationMatchingPointcut implements Pointcut {
    
     private final ClassFilter classFilter;
    
     private final MethodMatcher methodMatcher;
    
     public AnnotationMatchingPointcut(Class clazz) {
      return (this.checkInherited ? AnnotatedElementUtils.hasAnnotation(clazz, this.annotationType) :
        clazz.isAnnotationPresent(this.annotationType));
     }
        //...
    }

    这里省略了一些代码,关键地方就是这个匹配方法了,如果需要检查父类是否包含该注解,则调用 AnnotatedElementUtils.hasAnnotation 方法进行查找,否则直接调用 clazz.isAnnotationPresent 方法判断当前类上是否包含指定注解即可。

    5.2 AnnotationCandidateClassFilter

    private static class AnnotationCandidateClassFilter implements ClassFilter {
    private final Class

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论