跟着小马哥学系列之 Spring AOP(Pointcut 组件详解)

学好路更宽,钱多少加班。 ——小马哥

版本修订

  • 2021.5.19:去除目录
  • 2021.5.21:引用 Spring 官方 Pointcut 概念,修改 Pointcut 功能表述

简介

大家好,我是小马哥成千上万粉丝中的一员!2019年8月有幸在叩丁狼教育举办的猿圈活动中知道有这么一位大咖,从此结下了不解之缘!此系列在多次学习极客时间《小马哥讲Spring AOP 编程思想》基础上形成的个人一些总结。希望能帮助各位小伙伴, 祝小伙伴早日学有所成。

Pointcut 功能

  1. Pointcut 接口是中心接口,用于将 advice 定向到特定的类和方法。
  2. pointcut 可以重用,而不依赖于 advice 类型。可以使用相同的 pointcut 来针对不同的 advice。
  3. 将 Pointcut 接口分成两个部分允许重复使用类(ClassFilter)和方法(MethodMatcher)匹配部件以及细粒度的组合操作(例如与另一个 MethodMatcher/ClassFilter 执行交集或者并集)。

类图

在这里插入图片描述
从类图可知 Pointcut 分为 6 大类:

  • ComposablePointcut:把多个过滤条件(ClassFilter 或 MethodMatcher 亦是 Pointcut)通过交、并组合起来变成一个 Pointcut
  • StaticMethodMatcherPointcut:StaticMethodMatcher + Pointcut 组合在一起,不需要关心运行时参数
  • DynamicMethodMatcherPointcut:DynamicMethodMatcher + Pointcut 组合在一起,关心运行时参数
  • AnnotationMatchingPointcut:注解类型 Pointcut
  • ControlFlowPointcut:用于简单的 cflow(CallTree 调用链) 风格 Pointcut
  • ExpressionPointcut:表达式类型 Pointcut

相关类/接口介绍

ClassFilter

  1. 限制 Pointcut 匹配或对一组给定目标类的 Introduction 的过滤器。作为 Pointcut 一部分使用也可以作为一个 IntroductionAdvisor 的整个目标,该接口的具体实现通常应该提供 Object.equals(Object) 和Object. hashcode() 的适当实现,以便允许过滤器在缓存场景中使用 —— 例如,在CGLIB生成的代理中。
  2. 对类进行过滤所以只提供了:boolean matches(Class<?> clazz) 方法

MethodMatcher

  1. 用于检查目标方法是否有资格获得 advice。
  2. MathodMatcher 可以静态地或在运行时(动态地)计算。静态匹配涉及方法和(可能)方法属性。动态匹配还使特定调用的参数可用,以及将先前的 advice 应用到 joinpoint 的任何效果。
  3. 如果实现 isRuntime() 方法返回 false,则可以静态执行求值,并且该方法的所有调用的结果都是相同的,无论它们的参数是什么。这意味着如果 isRuntime()方法返回 false,则 3 参数 matches(Method method, Class<?> targetClass, Object... args) 方法将永远不会被调用。
  4. 如果一个实现从它的 2 参数 matches(Method method, Class<?> targetClass)返回 true和它的 isRuntime() 方法返回 true, 3 参数 matches(Method method, Class<?> targetClass, Object... args) 方法将在相关 advice 的每次潜在执行之前被立即调用,以决定该 advice 是否应该运行。之前的所有 adivce (比如拦截器链中的早期拦截器)都将运行,因此它们在参数或 ThreadLocal 状态中产生的任何状态更改都将在计算时可用。
  5. 该接口的具体实现通常应该提供 Object.equals(Object) 和 Object. hashcode() 的适当实现,以便允许匹配器在缓存场景中使用——例如,在 CGLIB 生成的代理中。

StaticMethodMatcherPointcut

StaticMethodMatcher + Pointcut 二合一。 StaticMethodMatcher:静态方法匹配器的方便抽象超类,它不关心运行时的参数。由于不关心运行时参数所以 isRuntime() 方法返回 false,3个参数的 matches 方法直接抛出 UnsupportedOperationException 异常。ClassFilter:默认值是 ClassFilter.TRUE(匹配所有类),可以通过 setClassFilter 方法设置 ClassFilter 或者覆写 getClassFilter 方法

示例

如何对 EchoService 接口的 echo 方法进行拦截?

// 继承 StaticMethodMatcherPointcut 覆写 matches 方法
public class SimpleStaticPointcut extends StaticMethodMatcherPointcut {
    @Override
    public boolean matches(@NonNull Method method,@NonNull Class<?> targetClass) {
        return "echo".equals(method.getName());
    }

    @Override
    @NonNull
    public ClassFilter getClassFilter() {
        return EchoService.class::isAssignableFrom;
    }
}

public interface EchoService {
    String echo(String message);
}
public class DefaultEchoServiceImpl implements EchoService {
    @Override
    public String echo(String message) {
        return message;
    }
}

public interface EchoService2 {
    String echo(String message);
}
public class DefaultEcho2ServiceImpl implements EchoService2 {
    @Override
    public String echo(String message) {
        return message;
    }
}


public class StaticPointcutDemo {
	public static void main(String[] args) {
	    EchoService echoService = new DefaultEchoServiceImpl();
	    EchoService2 echoService2 = new DefaultEcho2ServiceImpl();
	    Pointcut pc = new SimpleStaticPointcut();
	    Advisor advisor = new DefaultPointcutAdvisor(pc, (MethodBeforeAdvice) (method, arg, target) -> System.out.println("MethodBeforeAdvice execute: " + method));
	    ProxyFactory proxyFactory = new ProxyFactory();
	    proxyFactory.addAdvisor(advisor);
	    proxyFactory.setTarget(echoService);
	    EchoService proxyOne = (EchoService) proxyFactory.getProxy();
	    ProxyFactory proxyFactory2 = new ProxyFactory();
	    proxyFactory2.setTarget(echoService2);
	    proxyFactory2.addAdvisor(advisor);
	    EchoService2 proxyTwo = (EchoService2) proxyFactory2.getProxy();
	    // 会被拦截并执行 MethodBeforeAdvice
	    proxyOne.echo("文海");
	    proxyTwo.echo("文海");
	}
}

关于 adivce 详情请参阅 跟着小马哥学系列之 Spring AOP( Advice 组件详解)

DynamicMethodMatcherPointcut

DynamicMethodMatcher + Pointcut 2合一。 DynamicMethodMatcher:动态方法匹配器的方便抽象超类,它关心运行时的参数。由于关心运行时参数所以 isRuntime() 方法返回 true,将要覆写3个参数的 matches 方法。ClassFilter:默认值是 ClassFilter.TRUE(匹配所有类),可以通过覆写 getClassFilter 方法设置 ClassFilter

示例

如何实现对 EchoService 接口的 echo 方法参数为 文海大叔时才进行拦截 ?


public class SimpleDynamicPointcut extends DynamicMethodMatcherPointcut {
  @Override
  public boolean matches(@NonNull Method method, @NonNull Class<?> targetClass, @NonNull Object... args) {
      System.out.println("Dynamic check method: " + method.getName());
      System.out.println("Method args: " + Arrays.toString(args));
      String message = (String) args[0];
      boolean match = "文海大叔".equals(message);
      System.out.println("是否匹配:" + match);
      return match;
  }

  @Override
  public boolean matches(Method method, Class<?> targetClass) {
      System.out.println("Static check method: " + method.getName());
      return "echo".equals(method.getName());
  }

  @Override
  @NonNull
  public ClassFilter getClassFilter() {
      return EchoService.class::isAssignableFrom;
  }
}

public interface EchoService {
    String echo(String message);
}
public class DefaultEchoServiceImpl implements EchoService {
    @Override
    public String echo(String message) {
        return message;
    }
}

public interface EchoService2 {
    String echo(String message);
}
public class DefaultEcho2ServiceImpl implements EchoService2 {
    @Override
    public String echo(String message) {
        return message;
    }
}


public class DynamicPointcutDemo {
   public static void main(String[] args) {
       EchoService echoService = new DefaultEchoServiceImpl();
       EchoService2 echoService2 = new DefaultEcho2ServiceImpl();
       Advisor advisor = new DefaultPointcutAdvisor(new SimpleDynamicPointcut(),
               (MethodBeforeAdvice) (method, args1, target) -> System.out.printf("被拦截的方法为:%s,方法参数为:%s\n", method, Arrays.toString(args1))
       );
       ProxyFactory proxyFactory = new ProxyFactory();
       proxyFactory.setTarget(echoService);
       proxyFactory.addAdvisor(advisor);
       EchoService echoServiceProxy = (EchoService) proxyFactory.getProxy();
       ProxyFactory proxyFactory2 = new ProxyFactory();
       proxyFactory2.setTarget(echoService2);
       proxyFactory2.addAdvisor(advisor);
       EchoService2 echoService2Proxy = (EchoService2) proxyFactory2.getProxy();
       // 先类型匹配了,再进行静态匹配,后面在进行动态匹配参数,参数不符合,不执行后面方法;
       echoServiceProxy.echo("文海");
       // 先类型匹配了,不匹配。不再进行后面匹配
       echoService2Proxy.echo("文海");
       // 先类型匹配了,再进行静态匹配,后面在进行动态匹配参数,参数符合,执行 advice 方法;
       echoServiceProxy.echo("文海大叔");
       // 即使是参数匹配,但是也是先类型匹配了,不匹配。不再进行后面匹配
       echoService2Proxy.echo("文海大叔");

   }
}

ComposablePointcut

把多个过滤条件(ClassFilter 或 MethodMatcher 亦是 Pointcut)通过交、并组合起来变成一个 Pointcut

示例

如果实现对 EchoService 或 EchoService2 类中 echo 方法进行拦截?

public class ComposablePointcutDemo {
    public static void main(String[] args) {
        EchoService echoService = new DefaultEchoServiceImpl();
        EchoService2 echoService2 = new DefaultEcho2ServiceImpl();
        // 对 EchoService 实例过滤
        ComposablePointcut pointcut = new ComposablePointcut((EchoService.class::isAssignableFrom));
        // 或对 EchoService2 实例过滤
        pointcut.union((EchoService2.class::isAssignableFrom));
        // 并对 echo 方法过滤
        pointcut.intersection(new MethodMatcher() {
            @Override
            public boolean matches(Method method, Class<?> targetClass) {
                return "echo".equals(method.getName());
            }

            @Override
            public boolean isRuntime() {
                return false;
            }

            @Override
            public boolean matches(Method method, Class<?> targetClass, Object... args) {
                return false;
            }
        });

        Advisor advisor = new DefaultPointcutAdvisor(pointcut,
                (MethodBeforeAdvice) (method, args1, target) -> System.out.printf("被拦截的方法为:%s,方法参数为:%s\n", method, Arrays.toString(args1))
        );
        ProxyFactory proxyFactory = new ProxyFactory();
        proxyFactory.setTarget(echoService);
        proxyFactory.addAdvisor(advisor);
        EchoService echoServiceProxy = (EchoService) proxyFactory.getProxy();
        ProxyFactory proxyFactory2 = new ProxyFactory();
        proxyFactory2.setTarget(echoService2);
        proxyFactory2.addAdvisor(advisor);
        EchoService2 echoService2Proxy = (EchoService2) proxyFactory2.getProxy();
        echoServiceProxy.echo("文海");
        echoService2Proxy.echo("文海");
        echoServiceProxy.echo("文海大叔");
        echoService2Proxy.echo("文海大叔");
        echoServiceProxy.echo2("文海");

    }
}


ExpressionPointcut

支持表达式类型 Pointcut,最具有代表性的就是 AspectJExpressionPointcut。对表达式类型 Pointcut 表达式值是一个 AspectJ 字符串表达式。这可以引用其他 pointcut 并使用组合和其他操作。

示例

对 EchoService echo 方法进行拦截

public interface EchoService {
    String echo(String message);
    default String echo2(String message){
        return message;
    }
}
public class DefaultEchoServiceImpl implements EchoService {
    @Override
    public String echo(String message) {
        return message;
    }
}

public class AspectJExpressionPointcutDemo {
    public static void main(String[] args) {
        EchoService echoService = new DefaultEchoServiceImpl();
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression("execution(* echo(..))");
        Advisor advisor = new DefaultPointcutAdvisor(pointcut,
                (MethodBeforeAdvice) (method, args1, target) -> System.out.printf("被拦截的方法为:%s,方法参数为:%s\n", method, Arrays.toString(args1))
        );
        ProxyFactory proxyFactory = new ProxyFactory();
        proxyFactory.setTarget(echoService);
        proxyFactory.addAdvisor(advisor);
        EchoService proxy = (EchoService) proxyFactory.getProxy();
        proxy.echo("文海");
        // 不会拦截
        proxy.echo2("文海");
    }
}

AnnotationMatchingPointcut

寻找类(forClassAnnotation)或方法(forMethodAnnotation)上的特定 Java 5 注释的简单 Pointcut。通过 AnnotationClassFilter 和 AnnotionMethodMatcher 来实现。

示例

找到标注自定义 @Advice 方法进行拦截

public interface EchoService {
    String echo(String message);
    default String echo2(String message){
        return message;
    }
}
public class DefaultEchoServiceImpl implements EchoService {
    @AnnotationPointcut.Advice
    @Override
    public String echo(String message) {
        return message;
    }
}

public class AnnotationPointcut {
    public static void main(String[] args) {
        EchoService echoService = new DefaultEchoServiceImpl();
        AnnotationMatchingPointcut pointcut = AnnotationMatchingPointcut.forMethodAnnotation(Advice.class);
        Advisor advisor = new DefaultPointcutAdvisor(pointcut,
                (MethodBeforeAdvice) (method, args1, target) -> System.out.printf("被拦截的方法为:%s,方法参数为:%s\n", method, Arrays.toString(args1))
        );
        ProxyFactory proxyFactory = new ProxyFactory();
        proxyFactory.setTarget(echoService);
        proxyFactory.addAdvisor(advisor);
        EchoService proxy = (EchoService) proxyFactory.getProxy();
        // 拦截并发生前置 Advice
        proxy.echo("文海");
        proxy.echo2("文海");
    }


    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE, ElementType.METHOD})
    public @interface Advice {
    }

}


ControlFlowPointcut

用于简单的 cflow(CallTree 调用链) 风格 Pointcut。如果要拦截某个类调用某个类的某个方法或者类里面的任何方法是非常有用的。

示例

拦截 Client 类调用 EchoService 的 echo 方法。


public class Client {
    public void call(EchoService echoService) {
        echoService.echo("文海");
    }
}


public class ControlFlowDemo {

    public static void main(String[] args) {
        EchoService echoService = new DefaultEchoServiceImpl();
        Pointcut pc = new ControlFlowPointcut(Client.class, "call");
        Advisor advisor = new DefaultPointcutAdvisor(pc,
                (MethodBeforeAdvice) (method, args1, target) -> System.out.printf("被拦截的方法为:%s,方法参数为:%s\n", method, Arrays.toString(args1))
        );
        ProxyFactory pf = new ProxyFactory();
        pf.setTarget(echoService);
        pf.addAdvisor(advisor);
        EchoService proxy = (EchoService) pf.getProxy();
        new Client().call(proxy);

    }
}

matches 方法解密

@Override
public boolean matches(Method method, Class<?> targetClass, Object... args) {
	this.evaluations.incrementAndGet();
	// 通过遍历代理对象调用栈找到指定类和方法(不指定是类的任何方法)
	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;
}

总结(一张图搞定)

在这里插入图片描述

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序猿大叔文海

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值