博客 / 詳情

返回

Spring AOP 中,切點有多少種定義方式?

在 Spring AOP 中,我們最常用的切點定義方式主要是兩種:

  1. 使用 execution 進行無侵入攔截。
  2. 使用註解進行攔截。

這應該是是小夥伴們日常工作中使用最多的兩種切點定義方式了。但是除了這兩種還有沒有其他的呢?今天鬆哥就來和大家聊一聊這個話題。

1. Pointcut 分類

來看下 Pointcut 的定義:

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

這個實現類從名字上看就是攔截所有,攔截一切,我們來看下這個類怎麼做的:

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<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 中提供的一個工具類,支持通配符的匹配。

舉個簡單例子:

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<? 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

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

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

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 實踐

首先我自定義一個註解,如下:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAction {
}

然後將之添加到某一個方法上:

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);
    }
}

最後來實踐一下:

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 類的實現比較長也比較複雜,我這裏貼其中一些關鍵的代碼來看下:

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 主要是指目標方法從某一個指定類的指定方法中執行,切點才生效,否則不生效。

舉個簡單例子,如下:

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 的原理也很簡單,就是比較一下類名和方法名,如下:

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 方法。

如下案例:

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 註解標記的方法。如下:

@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 為例,我們來簡單看下源碼:

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 這是框架內部使用的一個切點,表示攔截一切。

哦了~

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.