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

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

  1. 使用 execution 进行无侵入拦截。
  2. 使用注解进行拦截。

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

1. Pointcut 分类

来看下 Pointcut 的定义:

java 复制代码
public interface Pointcut {
	ClassFilter getClassFilter();
	MethodMatcher getMethodMatcher();
	Pointcut TRUE = TruePointcut.INSTANCE;
}

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

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

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

  1. 静态方法切点:StaticMethodMatcherPointcut 表示静态方法切点的抽象基类,默认情况下匹配所有的类,然后通过不同的规则去匹配不同的方法。
  2. 动态方法切点:DynamicMethodMatcherPointcut 表示动态方法切点的抽象基类,默认情况下它匹配所有的类,然后通过不同的规则匹配不同的方法,这个有点类似于 StaticMethodMatcherPointcut,不同的是,StaticMethodMatcherPointcut 仅对方法签名进行匹配并且仅匹配一次,而 DynamicMethodMatcherPointcut 会在运行期间检查方法入参的值,由于每次传入的参数可能都不一样,所以没调用都要去判断,因此就导致 DynamicMethodMatcherPointcut 的性能差一些。
  3. 注解切点:AnnotationMatchingPointcut。
  4. 表达式切点:ExpressionPointcut。
  5. 流程切点:ControlFlowPointcut。
  6. 复合切点:ComposablePointcut。

除了上面这六个之外,另外还有一个落单的 TruePointcut,这个从名字上就能看出来是拦截一切。

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

2. TruePointcut

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

java 复制代码
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 仅对方法名签名(包括方法名和入参类型及顺序)进行匹配,静态匹配仅判断一次。

java 复制代码
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 方法:

java 复制代码
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 方法。

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

java 复制代码
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 类似,如下:

java 复制代码
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

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

java 复制代码
public class NameMatchMethodPointcut extends StaticMethodMatcherPointcut implements Serializable {

	private List<String> 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 中提供的一个工具类,支持通配符的匹配。

举个简单例子:

java 复制代码
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

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

java 复制代码
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 的父类中进行遍历逐个进行匹配,我举一个例子:

java 复制代码
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

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

java 复制代码
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 接口,这就是一个方法匹配器:

java 复制代码
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 方法中做一些前置的判断。

我们来看一个简单例子:

java 复制代码
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 方法中做匹配操作即可。

然后使用这个切点:

java 复制代码
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

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

先来看下这个类的定义:

java 复制代码
public class AnnotationMatchingPointcut implements Pointcut {

	private final ClassFilter classFilter;

	private final MethodMatcher methodMatcher;

	public AnnotationMatchingPointcut(Class<? extends Annotation> classAnnotationType) {
		this(classAnnotationType, false);
	}

	public AnnotationMatchingPointcut(Class<? extends Annotation> classAnnotationType, boolean checkInherited) {
		this.classFilter = new AnnotationClassFilter(classAnnotationType, checkInherited);
		this.methodMatcher = MethodMatcher.TRUE;
	}

	public AnnotationMatchingPointcut(@Nullable Class<? extends Annotation> classAnnotationType,
			@Nullable Class<? extends Annotation> methodAnnotationType) {

		this(classAnnotationType, methodAnnotationType, false);
	}

	public AnnotationMatchingPointcut(@Nullable Class<? extends Annotation> classAnnotationType,
			@Nullable Class<? extends Annotation> methodAnnotationType, boolean checkInherited) {

		if (classAnnotationType != null) {
			this.classFilter = new AnnotationClassFilter(classAnnotationType, checkInherited);
		}
		else {
			this.classFilter = new AnnotationCandidateClassFilter(methodAnnotationType);
		}

		if (methodAnnotationType != null) {
			this.methodMatcher = new AnnotationMethodMatcher(methodAnnotationType, checkInherited);
		}
		else {
			this.methodMatcher = MethodMatcher.TRUE;
		}
	}


	@Override
	public ClassFilter getClassFilter() {
		return this.classFilter;
	}

	@Override
	public MethodMatcher getMethodMatcher() {
		return this.methodMatcher;
	}

	public static AnnotationMatchingPointcut forClassAnnotation(Class<? extends Annotation> annotationType) {
		Assert.notNull(annotationType, "Annotation type must not be null");
		return new AnnotationMatchingPointcut(annotationType);
	}
	public static AnnotationMatchingPointcut forMethodAnnotation(Class<? extends Annotation> annotationType) {
		Assert.notNull(annotationType, "Annotation type must not be null");
		return new AnnotationMatchingPointcut(null, annotationType);
	}

}

首先小伙伴们注意到,这个类一共有四个构造方法,从上往下分别是:

  1. 传入类上的注解名称,根据类上的注解去判断是否需要拦截。
  2. 在 1 的基础之上,再增加一个 checkInherited,这个表示是否需要检查父类上是否存在相关的注解。
  3. 传入类上和方法上的注解类型,根据这个注解类型去判断是否需要拦截。
  4. 在 3 的基础之上,再增加一个 checkInherited,这个表示是否需要检查父类上或者方法上是否存在相关的注解。

其中,第四个构造方法中处理的情况类型比较多,如果用户传入了 classAnnotationType,则构建 AnnotationClassFilter 类型的 ClassFilter,否则构建 AnnotationCandidateClassFilter 类型的 ClassFilter;如果用户传入了 methodAnnotationType,则构建 AnnotationMethodMatcher 类型的 MethodMatcher,否则方法匹配器就直接返回匹配所有方法。

那么接下来我们就来看下这几种不同的匹配器。

5.1 AnnotationClassFilter

java 复制代码
public class AnnotationClassFilter implements ClassFilter {
    //...
	@Override
	public boolean matches(Class<?> clazz) {
		return (this.checkInherited ? AnnotatedElementUtils.hasAnnotation(clazz, this.annotationType) :
				clazz.isAnnotationPresent(this.annotationType));
	}
    //...
}

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

5.2 AnnotationCandidateClassFilter

java 复制代码
private static class AnnotationCandidateClassFilter implements ClassFilter {
	private final Class<? extends Annotation> annotationType;
	AnnotationCandidateClassFilter(Class<? extends Annotation> annotationType) {
		this.annotationType = annotationType;
	}
	@Override
	public boolean matches(Class<?> clazz) {
		return AnnotationUtils.isCandidateClass(clazz, this.annotationType);
	}
}

这里就是调用了 AnnotationUtils.isCandidateClass 方法进行判断,这个方法用来判断指定类是不是可以承载指定注解的候选类,返回 true 的规则是:

  1. java. 开头的注解,所有的类都能承载,这种情况会返回 true。
  2. 目标类不能以 java. 开头,也就是说 JDK 中的类都不行,不是以 java. 开头的类就可以返回 true。
  3. 给定类也不能是 Ordered 类。

满足如上条件,这个类就是符合规定的。

AnnotationCandidateClassFilter 主要是针对用户没有传入类上注解的情况,这种情况一般都是根据方法上的注解进行匹配的,所以这里主要是排除一些系统类。

5.3 AnnotationMethodMatcher

java 复制代码
public class AnnotationMethodMatcher extends StaticMethodMatcher {
	@Override
	public boolean matches(Method method, Class<?> targetClass) {
		if (matchesMethod(method)) {
			return true;
		}
		// Proxy classes never have annotations on their redeclared methods.
		if (Proxy.isProxyClass(targetClass)) {
			return false;
		}
		// The method may be on an interface, so let's check on the target class as well.
		Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
		return (specificMethod != method && matchesMethod(specificMethod));
	}
	private boolean matchesMethod(Method method) {
		return (this.checkInherited ? AnnotatedElementUtils.hasAnnotation(method, this.annotationType) :
				method.isAnnotationPresent(this.annotationType));
	}
}

方法匹配就是首先检查方法上是否有注解,如果开启了 checkInherited,则去检查一下父类对应的方法上是否有相关的注解,如果有,则表示方法匹配上了,返回 true。

否则先去检查一下当前类是否是一个代理对象,代理对象中对应的方法肯定是没有注解的,直接返回 false。

如果前面两步还没返回,最后考虑到目前这个方法可能是在一个接口上,要检查一下它的实现类是否包含该注解。

这就是 AnnotationMatchingPointcut。松哥也举一个简单例子吧。

5.4 实践

首先我自定义一个注解,如下:

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAction {
}

然后将之添加到某一个方法上:

java 复制代码
public class CalculatorImpl implements ICalculator {
    @Override
    public void add(int a, int b) {
        System.out.println(a + "+" + b + "=" + (a + b));
    }

    @MyAction
    @Override
    public int minus(int a, int b) {
        return a - b;
    }

    @Override
    public void setA(int a) {
        System.out.println("a = " + a);
    }
}

最后来实践一下:

java 复制代码
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(new CalculatorImpl());
proxyFactory.addInterface(ICalculator.class);
proxyFactory.addAdvisor(new PointcutAdvisor() {
    @Override
    public Pointcut getPointcut() {
        return new AnnotationMatchingPointcut(null, MyAction.class);
    }
    @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);

只有 minus 方法被拦截下来了。

6. ExpressionPointcut

这个其实就是我们日常开发中使用最多的一种切点定义方式,可能项目中 99% 的切点定义都是使用的 ExpressionPointcut。这个具体用法我这里就不说了,因为比较丰富,都能单独整一篇文章了,如果小伙伴对 ExpressionPointcut 的基础用法还不熟悉的话,可以在公众号【江南一点雨】后台回复 ssm,有松哥之前录制的入门视频教程可以参考。

我这里就简单把它的实现思路来和小伙伴们梳理一下,ExpressionPointcut 的实现都在 AspectJExpressionPointcut 类中,该类支持使用切点语言来对要拦截的方法做一个非常精确的描述,精确到要拦截方法的返回值,AspectJExpressionPointcut 类的实现比较长也比较复杂,我这里贴其中一些关键的代码来看下:

java 复制代码
public class AspectJExpressionPointcut extends AbstractExpressionPointcut
		implements ClassFilter, IntroductionAwareMethodMatcher, BeanFactoryAware {

	private static final Set<PointcutPrimitive> SUPPORTED_PRIMITIVES = Set.of(
			PointcutPrimitive.EXECUTION,
			PointcutPrimitive.ARGS,
			PointcutPrimitive.REFERENCE,
			PointcutPrimitive.THIS,
			PointcutPrimitive.TARGET,
			PointcutPrimitive.WITHIN,
			PointcutPrimitive.AT_ANNOTATION,
			PointcutPrimitive.AT_WITHIN,
			PointcutPrimitive.AT_ARGS,
			PointcutPrimitive.AT_TARGET);

	@Override
	public ClassFilter getClassFilter() {
		obtainPointcutExpression();
		return this;
	}

	@Override
	public MethodMatcher getMethodMatcher() {
		obtainPointcutExpression();
		return this;
	}

	/**
	 * Check whether this pointcut is ready to match,
	 * lazily building the underlying AspectJ pointcut expression.
	 */
	private PointcutExpression obtainPointcutExpression() {
		if (getExpression() == null) {
			throw new IllegalStateException("Must set property 'expression' before attempting to match");
		}
		if (this.pointcutExpression == null) {
			this.pointcutClassLoader = determinePointcutClassLoader();
			this.pointcutExpression = buildPointcutExpression(this.pointcutClassLoader);
		}
		return this.pointcutExpression;
	}
}

其实关键还是要获取到 ClassFilter 和 MethodMatcher,然后调用其 matches 方法,当前类刚好就是实现了这两个,所以直接返回 this 就可以了。在 getClassFilter 或者 getMethodMatcher 方法执行之前,都会先调用 obtainPointcutExpression 方法,去解析我们传入的 expression 字符串,将之解析为一个 PointcutExpression 对象,接下来的 matches 方法就可以此为依据,进行匹配了。

7. ControlFlowPointcut

ControlFlowPointcut 主要是指目标方法从某一个指定类的指定方法中执行,切点才生效,否则不生效。

举个简单例子,如下:

java 复制代码
public class AopDemo04 {
    public static void main(String[] args) {
        ProxyFactory proxyFactory = new ProxyFactory();
        proxyFactory.setTarget(new CalculatorImpl());
        proxyFactory.addInterface(ICalculator.class);
        proxyFactory.addAdvisor(new PointcutAdvisor() {
            @Override
            public Pointcut getPointcut() {
                return new ControlFlowPointcut(AopDemo04.class, "evl");
            }

            @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);
        System.out.println("/");
        evl(calculator);
    }

    public static void evl(ICalculator iCalculator) {
        iCalculator.add(3, 4);
    }
}

这里切点的意思就是说,必须要从 AopDemo04 这个类的 evl 方法中调用 add 方法,这个切点才会生效,如果是拿到了 ICalculator 对象后直接调用 add 方法,那么切点是不会生效的。

ControlFlowPointcut 的原理也很简单,就是比较一下类名和方法名,如下:

java 复制代码
public class ControlFlowPointcut implements Pointcut, ClassFilter, MethodMatcher, Serializable {
	@Override
	public boolean matches(Class<?> clazz) {
		return true;
	}
	@Override
	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) {
		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;
	}
	@Override
	public ClassFilter getClassFilter() {
		return this;
	}

	@Override
	public MethodMatcher getMethodMatcher() {
		return this;
	}
}

大家可以看到,isRuntime 方法返回 true,表示这是一个动态的方法匹配器。关键的 matches 方法,就是根据调用栈中的信息,去比较给定的类名和方法名是否满足。

8. ComposablePointcut

看名字就知道,这个可以将多个切点组合到一起,组合关系有求交集和求并集两种,分别对应 ComposablePointcut 中的 intersection 方法和 union 方法。

如下案例:

java 复制代码
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(new CalculatorImpl());
proxyFactory.addInterface(ICalculator.class);
proxyFactory.addAdvisor(new PointcutAdvisor() {
    @Override
    public Pointcut getPointcut() {
        NameMatchMethodPointcut nameMatchMethodPointcut = new NameMatchMethodPointcut();
        nameMatchMethodPointcut.setMappedNames("add");
        ComposablePointcut pc = new ComposablePointcut((Pointcut) nameMatchMethodPointcut);
        pc.union(new AnnotationMatchingPointcut(null, MyAction.class));
        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);

在上面的案例中,就是把 NameMatchMethodPointcut 和 AnnotationMatchingPointcut 两个切点联合起来,既拦截方法名为 add 的方法,也拦截含有 @MyAction 注解的方法。

如果将 union 方法改为 intersection,就表示拦截方法名为 add 且被 @MyAction 注解标记的方法。如下:

java 复制代码
@Override
public Pointcut getPointcut() {
    NameMatchMethodPointcut nameMatchMethodPointcut = new NameMatchMethodPointcut();
    nameMatchMethodPointcut.setMappedNames("add");
    ComposablePointcut pc = new ComposablePointcut((Pointcut) nameMatchMethodPointcut);
    pc.intersection(new AnnotationMatchingPointcut(null, MyAction.class));
    return pc;
}

其实这种组合切点的原理很简单,先把我们提供的 ClassFilter 和 MethodMatcher 收集到一个集合中,如果是 union,就直接遍历集合,只要有一个满足,就返回 true;如果是 intersection,也是直接遍历,如果有一个返回 false 就直接返回 false 即可。

以 ClassFilter 为例,我们来简单看下源码:

java 复制代码
public ComposablePointcut union(ClassFilter other) {
	this.classFilter = ClassFilters.union(this.classFilter, other);
	return this;
}
public abstract class ClassFilters {
	public static ClassFilter union(ClassFilter cf1, ClassFilter cf2) {
		return new UnionClassFilter(new ClassFilter[] {cf1, cf2});
	}
	private static class UnionClassFilter implements ClassFilter, Serializable {

		private final ClassFilter[] filters;

		UnionClassFilter(ClassFilter[] filters) {
			this.filters = filters;
		}

		@Override
		public boolean matches(Class<?> clazz) {
			for (ClassFilter filter : this.filters) {
				if (filter.matches(clazz)) {
					return true;
				}
			}
			return false;
		}

	}
}

可以看到,传入的多个 ClassFilter 被组装到一起,在 matches 方法中逐个遍历,只要其中一个返回 true,就是 true。

9. 小结

好啦,这就是松哥今天和小伙伴们介绍的 7 中 Pointcut 了,希望借此小伙伴们对 Spring AOP 中切点的类型有一个完整的了解。再来回顾一下这其中切点:

  1. 静态方法切点:StaticMethodMatcherPointcut 表示静态方法切点的抽象基类,默认情况下匹配所有的类,然后通过不同的规则去匹配不同的方法。
  2. 动态方法切点:DynamicMethodMatcherPointcut 表示动态方法切点的抽象基类,默认情况下它匹配所有的类,然后通过不同的规则匹配不同的方法,这个有点类似于 StaticMethodMatcherPointcut,不同的是,StaticMethodMatcherPointcut 仅对方法签名进行匹配并且仅匹配一次,而 DynamicMethodMatcherPointcut 会在运行期间检查方法入参的值,由于每次传入的参数可能都不一样,所以没调用都要去判断,因此就导致 DynamicMethodMatcherPointcut 的性能差一些。
  3. 注解切点:AnnotationMatchingPointcut 根据制定注解拦截目标方法或者类。
  4. 表达式切点:ExpressionPointcut 这个是我们日常开发中使用最多的一种切点定义方式。
  5. 流程切点:ControlFlowPointcut 这个是要求必须从某一个位置调用目标方法,切点才会生效。
  6. 复合切点:ComposablePointcut 这个是把多个拦截器组装在一起使用,有交集和并集两种组装方式。
  7. TruePointcut 这是框架内部使用的一个切点,表示拦截一切。

哦了~

相关推荐
BD_Marathon1 小时前
【Flink】部署模式
java·数据库·flink
鼠鼠我捏,要死了捏4 小时前
深入解析Java NIO多路复用原理与性能优化实践指南
java·性能优化·nio
ningqw4 小时前
SpringBoot 常用跨域处理方案
java·后端·springboot
你的人类朋友4 小时前
vi编辑器命令常用操作整理(持续更新)
后端
superlls4 小时前
(Redis)主从哨兵模式与集群模式
java·开发语言·redis
胡gh4 小时前
简单又复杂,难道只能说一个有箭头一个没箭头?这种问题该怎么回答?
javascript·后端·面试
一只叫煤球的猫5 小时前
看到同事设计的表结构我人麻了!聊聊怎么更好去设计数据库表
后端·mysql·面试
uzong5 小时前
技术人如何对客做好沟通(上篇)
后端
叫我阿柒啊6 小时前
Java全栈工程师面试实战:从基础到微服务的深度解析
java·redis·微服务·node.js·vue3·全栈开发·电商平台
颜如玉6 小时前
Redis scan高位进位加法机制浅析
redis·后端·开源