Spring5框架之AOP-Pointcut底層實現(六)

1. 前言

在上一篇博客中我們學習了基於ProxyFactory添加前置通知、後置通知、後置返回通知、異常通知以及更爲強大的環繞通知的學習。ProxyFactory通過addAdvice方法用於配置代理的通知,其底層是將此方法委託給addAdvisor方法如下所示,並且ProxyFactory通過setTarget方法對目標的所有方法進行代理。

在這裏插入圖片描述
上面默認創建都是DefaultPointAdvisor實例,其通知默認將會適用於代理目標對象的所有方法。但有時候我們並不想讓通知對一個類的所有方法都進行適配。這個時候我們就可以使用Pointcut這個接口的實現去決定哪些類的那些方法將會適配通知。

2. Pointcut接口

Spring通過切點來實現對那些方法進行通知適配而切入點是通過Pointcut接口實現來完成的。

public interface Pointcut {

	/**
	 * Return the ClassFilter for this pointcut.
	 * @return the ClassFilter (never {@code null})
	 */
	ClassFilter getClassFilter();

	/**
	 * Return the MethodMatcher for this pointcut.
	 * @return the MethodMatcher (never {@code null})
	 */
	MethodMatcher getMethodMatcher();
}

Pointcut接口定義了兩個方法getClassFilter()getMethodMatcher(),通過``ClassFilter對象的match` 方法來判斷切點是否適用於某些類其方法如下所示:

@FunctionalInterface
public interface ClassFilter {

	/**
	 * Should the pointcut apply to the given interface or target class?
	 * @param clazz the candidate target class
	 * @return whether the advice should apply to the given target class
	 */
	boolean matches(Class<?> clazz);
}

可以看到ClassFilter接口的 match方法傳入一個Class實例用來檢查該實例是否適用於通知,如果方法返回true則表示該類適用。MethodMatcher 接口稍微複雜一些其接口方法如下:

public interface MethodMatcher {

	/**
	 * Perform static checking whether the given method matches.
	 * <p>If this returns {@code false} or if the {@link #isRuntime()}
	 * method returns {@code false}, no runtime check (i.e. no
	 * {@link #matches(java.lang.reflect.Method, Class, Object[])} call)
	 * will be made.
	 * @param method the candidate method
	 * @param targetClass the target class
	 * @return whether or not this method matches statically
	 */
	boolean matches(Method method, Class<?> targetClass);

	/**
	 * Is this MethodMatcher dynamic, that is, must a final call be made on the
	 * {@link #matches(java.lang.reflect.Method, Class, Object[])} method at
	 * runtime even if the 2-arg matches method returns {@code true}?
	 * <p>Can be invoked when an AOP proxy is created, and need not be invoked
	 * again before each method invocation,
	 * @return whether or not a runtime match via the 3-arg
	 * {@link #matches(java.lang.reflect.Method, Class, Object[])} method
	 * is required if static matching passed
	 */
	boolean isRuntime();

	/**
	 * Check whether there a runtime (dynamic) match for this method,
	 * which must have matched statically.
	 * <p>This method is invoked only if the 2-arg matches method returns
	 * {@code true} for the given method and target class, and if the
	 * {@link #isRuntime()} method returns {@code true}. Invoked
	 * immediately before potential running of the advice, after any
	 * advice earlier in the advice chain has run.
	 * @param method the candidate method
	 * @param targetClass the target class
	 * @param args arguments to the method
	 * @return whether there's a runtime match
	 * @see MethodMatcher#matches(Method, Class)
	 */
	boolean matches(Method method, Class<?> targetClass, Object... args);
}

Spring支持兩種類型的MethodMatcher ,如上面isRuntime確定MethodMatcher是靜態的還是動態,若方法返回值爲true則表示動態的否則將會是靜態的。對於靜態的切入點Spring會對每一個目標類的方法調用一次MethodMatchermatches方法並將返回的返回值進行緩存。這樣後面方法再次調用的時候便會取這個緩存。對於動態的切入點Spring會每一次調用matches方法。上面我們可以看到matches重載方法中 matches(Method method, Class<?> targetClass, Object… args) 這個可以對方法的參數進行檢查以確定目標方法是否適用於通知,例如可以用這個方法實現:當參數是一個String類型且以execute字符串開頭的目標方法才適用於通知。

Spring提供瞭如下Pointcut接口的實現如下圖所示:接下來將對其中幾個重要的實現進行演示說明。
在這裏插入圖片描述

2.1 NameMatchMethodPointcut

在創建切入點的時候我們可以指定匹配方法名然後使通知在這些匹配的方法上執行,這個時候就可以使用NameMatchMethodPointcut這個類下面是這個類的簡單實現如下所示:

繼續使用之前的SimpleBeforeAdvice類作爲通知其相關測試類方法代碼如下:

    @Test
    public void testNameMatchMethodPointcut() {
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
      	// 匹配add方法
        pointcut.addMethodName("add");
      	// 匹配sub方法
        pointcut.addMethodName("sub");
      	// 使用默認的advisor
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, new SimpleBeforeAdvice());
        ProxyFactory proxyFactory = new ProxyFactory();
        proxyFactory.addAdvisor(advisor);
        proxyFactory.setTarget(new CalculatorImpl());
        Calculator proxy = (Calculator) proxyFactory.getProxy();
        int add = proxy.add(1, 5);
        log.info("加法計算值:{}", add);
        double divide = proxy.divide(10, 10);
        log.info("除法計算值:{}", divide);
    }

測試方法輸出結果如下所示:

before......advice....start
執行的方法是:add
執行的參數是:[1, 5]
執行的對象是:com.codegeek.aop.day1.CalculatorImpl@6cc558c6
before......advice...end
2020-06-01 23:43:17 [main] [INFO] [PointCutTest.java:34] 加法計算值:6
2020-06-01 23:43:17 [main] [INFO] [PointCutTest.java:36] 除法計算值:1.0

可以看到``NameMatchMethodPointcut` 這個類的addMethodName 方法添加了兩個方法,然後纔會有前置通知運行只會匹配上述兩個方法。所以Calaulator這個類的divide方法並沒有執行前置通知。

2.2 JdkRegexpMethodPointcut

上面介紹了NameMatchMethodPointcut類可以對指定的方法進行匹配,但是一個個添加也確實麻煩一些。如果使用正則匹配是不是更方便一些呢?例如想匹配以指定前綴開頭的所有方法呢?我們還是使用之前的前置通知類測試代碼如下所示:

    public void  testJdkRegexPointCut() {
        JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut();
        // 設置匹配所以以get開頭的方法
        pointcut.setPattern(".*get");
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, new SimpleBeforeAdvice());
        ProxyFactory proxyFactory = new ProxyFactory();
        proxyFactory.addAdvisor(advisor);
        proxyFactory.setTarget(new CalculatorImpl());
        Calculator proxy = (Calculator) proxyFactory.getProxy();
        int add = proxy.add(1, 5);
        log.info("加法計算值:{}", add);
        double divide = proxy.divide(10, 10);
        log.info("除法計算值:{}", divide);
    }

測試運行結果如下:

2020-06-02 00:16:05 [main] [INFO] [PointCutTest.java:52] 加法計算值:6
2020-06-02 00:16:05 [main] [INFO] [PointCutTest.java:54] 除法計算值:1.0

我們可以看到前置通知並沒有執行,這是因爲我們執行的方法並沒有匹配到切點通知的正則。

2.3 DyanmicMethodMatcherPointcut

上面介紹兩種切點方式都是靜態切入點的實現,下面將演示如何使用動態方法切入點,我們設置當調用Calculatoradd 方法 參數之和大於50才執行相應的通知。

新增DynamicMethodMatcherPointcut類實現,需要注意的是需要重寫matches 方法。

public class SimpleDynamicMethodPointcut extends DynamicMethodMatcherPointcut {
    /**
     * 此抽象方法必須被重寫
     *
     * @param method
     * @param targetClass
     * @param args
     * @return
     */
    @Override
    public boolean matches(Method method, Class<?> targetClass, Object... args) {
        // 匹配add方法
        Integer sum = 0;
        for (Object arg : args) {
            Integer a = (Integer) arg;
            sum += a;
        }
        if (method.getName().equals("add") && sum > 50) return true;
        return false;
    }
}

測試類如下所示:

    @Test
    public void testDynamicMethod() {
        SimpleDynamicMethodPointcut pointcut = new SimpleDynamicMethodPointcut();
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, new SimpleBeforeAdvice());
        ProxyFactory proxyFactory = new ProxyFactory();
        proxyFactory.addAdvisor(advisor);
        proxyFactory.setTarget(new CalculatorImpl());
        Calculator proxy = (Calculator) proxyFactory.getProxy();
        System.out.println();
        int add = proxy.add(55, 5);
        log.info("加法計算值:{}", add);
    }

可以看到當方法參數大於50即執行了前置通知如下所示:

before......advice....start
執行的方法是:add
執行的參數是:[55, 5]
執行的對象是:com.codegeek.aop.day1.CalculatorImpl@132e0cc
before......advice...end
2020-06-02 00:39:46 [main] [INFO] [PointCutTest.java:67] 加法計算值:60

當我們將方法的參數改成如下:

  int add = proxy.add(5, 5);

測試運行結果如下:

2020-06-02 00:41:39 [main] [INFO] [PointCutTest.java:67] 加法計算值:10

可以很清晰看到當方法入參沒有滿足參數之和大於50就不會執行前置通知方法。

2.4 AspectJExpressionPointcut

Spring也內置了基於AspectJ切入點表達式支持的類,如果需要使用AspectJ切入點表達式需要在項目中添加如下依賴:

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>1.8.10</version>
        </dependency>

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.10</version>
        </dependency>

測試類代碼如下所示:

    @Test
    public void testAspectExpressionPointcut() {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression("execution(* *..aop.*day1..*(..))");
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, new SimpleBeforeAdvice());
        ProxyFactory proxyFactory = new ProxyFactory();
        proxyFactory.addAdvisor(advisor);
        proxyFactory.setTarget(new CalculatorImpl());
        Calculator proxy = (Calculator) proxyFactory.getProxy();
        System.out.println();
        int add = proxy.add(5, 5);
        log.info("加法計算值:{}", add);
    }

輸出結果如下:

before......advice....start
執行的方法是:add
執行的參數是:[5, 5]
執行的對象是:com.codegeek.aop.day1.CalculatorImpl@5c44c582
before......advice...end
2020-06-02 09:18:42 [main] [INFO] [PointCutTest.java:82] 加法計算值:10

可以使用AspectJExpressionPointcutsetExpression方法設置匹配類方法規則, 上面測試類的表達式意味着通知只在包含了aop包下任意一個包含day1的子包的任何類的任何方法。

2.5 AnnotationMatchingPointcut

如果在某些類的某些方法添加了指定的註解,如果需要基於自己的註解來實現特定的通知需要使用到AnnotationMatchingPointcut 類的支持,接下來將演示這個類的使用:

首先定一個註解:

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

}

然後我們將其添加Calculator實現類的sub方法上:

@Service
public class CalculatorImpl implements Calculator {
    @Override
    public int add(int a, int b) {
        return a + b;
    }

    @Override
    @AdviceRequired
    public int sub(int a, int b) {
        return a - b;
    }

    @Override
    public double divide(int a, int b) {
        return a / b;
    }
}

測試類代碼如下:

    @Test
    public void testAnnotationPointcut() {
        AnnotationMatchingPointcut pointcut =  AnnotationMatchingPointcut.forMethodAnnotation(AdviceRequired.class);
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut,new SimpleBeforeAdvice());
        ProxyFactory proxyFactory = new ProxyFactory();
        proxyFactory.setTarget(new CalculatorImpl());
        proxyFactory.addAdvisor(advisor);
        Calculator proxy = (Calculator) proxyFactory.getProxy();
        int sub = proxy.sub(5, 5);
        log.info("減法計算值:{}", sub);
        int add = proxy.add(5, 5);
        log.info("加法計算值:{}", add);
    }

運行結果如下所示:

before......advice....start
執行的方法是:sub
執行的參數是:[5, 5]
執行的對象是:com.codegeek.aop.day1.CalculatorImpl@600b90df
before......advice...end
2020-06-02 09:58:26 [main] [INFO] [PointCutTest.java:95] 減法計算值:0

我們可以清晰看到加了@AdviceRequired註解的sub方法實現了通知執行,而add方法並不會執行該通知。

3.總結

在Spring AOP中,有3個常用的概念,Advices、Pointcut、Advisor,解釋如下,
Advices:表示一個method執行前或執行後的動作。
Pointcut:表示根據method的名字或者正則表達式去攔截一個method。
Advisor:Advice和Pointcut組成的獨立的單元,並且能夠傳給proxy factory 對象。
以上代碼均可在 codegeekgao.git 下載查看。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章