天天看點

詳細解讀 Spring AOP 面向切面程式設計(二)

本文是《 詳細解讀 Spring AOP 面向切面程式設計(一)

》的續集。

在上篇中,我們從寫死代碼,到使用代理;從程式設計式 Spring AOP 到聲明式 Spring AOP。一切都朝着簡單實用主義的方向在發展。沿着 Spring AOP 的方向,Rod Johnson(老羅)花了不少心思,都是為了讓我們使用 Spring 架構時不會感受到麻煩,但事實卻并非如此。那麼,後來老羅究竟對 Spring AOP 做了哪些改進呢?

現在繼續!

9. Spring AOP:切面

之前談到的 AOP 架構其實可以将它了解為一個攔截器架構,但這個攔截器似乎非常武斷。比如說,如果它攔截了一個類,那麼它就攔截了這個類中所有的方法。類似地,當我們在使用動态代理的時候,其實也遇到了這個問題。需要在代碼中對所攔截的方法名加以判斷,才能過濾出我們需要攔截的方法,想想這種做法确實不太優雅。在大量的真實項目中,似乎我們隻需要攔截特定的方法就行了,沒必要攔截所有的方法。于是,老羅同志借助了 AOP 的一個很重要的工具,Advisor(切面),來解決這個問題。它也是 AOP 中的核心!是我們關注的重點!

也就是說,我們可以通過切面,将增強類與攔截比對條件組合在一起,然後将這個切面配置到 ProxyFactory 中,進而生成代理。

這裡提到這個“攔截比對條件”在 AOP 中就叫做 Pointcut(切點),其實說白了就是一個基于表達式的攔截條件罷了。

歸納一下,Advisor(切面)封裝了 Advice(增強)與 Pointcut(切點 )。當您了解了這句話後,就往下看吧。

我在 GreetingImpl 類中故意增加了兩個方法,都以“good”開頭。下面要做的就是攔截這兩個新增的方法,而對 sayHello() 方法不作攔截。

@Component
public class GreetingImpl implements Greeting {
    @Override
    public void sayHello(String name) {
        System.out.println("Hello! " + name);
    }
    public void goodMorning(String name) {
        System.out.println("Good Morning! " + name);
    }
    public void goodNight(String name) {
        System.out.println("Good Night! " + name);
    }
}           

在 Spring AOP 中,老羅已經給我們提供了許多切面類了,這些切面類我個人感覺最好用的就是基于正規表達式的切面類。看看您就明白了:

<?xml version="1.0" encoding="UTF-8"?>
<beans ...">
    <context:component-scan base-package="aop.demo"/>
    <!-- 配置一個切面 -->
    <bean id="greetingAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
        <property name="advice" ref="greetingAroundAdvice"/>            <!-- 增強 -->
        <property name="pattern" value="aop.demo.GreetingImpl.good.*"/> <!-- 切點(正規表達式) -->
    </bean>
    <!-- 配置一個代理 -->
    <bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="target" ref="greetingImpl"/>                <!-- 目标類 -->
        <property name="interceptorNames" value="greetingAdvisor"/> <!-- 切面 -->
        <property name="proxyTargetClass" value="true"/>            <!-- 代理目标類 -->
    </bean>
</beans>           

注意以上代理對象的配置中的 interceptorNames,它不再是一個增強,而是一個切面,因為已經将增強封裝到該切面中了。此外,切面還定義了一個切點(正規表達式),其目的是為了隻将滿足切點比對條件的方法進行攔截。

需要強調的是,這裡的切點表達式是基于正規表達式的。示例中的“aop.demo.GreetingImpl.good.”表達式後面的“.”表示比對所有字元,翻譯過來就是“比對 aop.demo.GreetingImpl 類中以 good 開頭的方法”。

除了 RegexpMethodPointcutAdvisor 以外,在 Spring AOP 中還提供了幾個切面類,比如:

  • DefaultPointcutAdvisor:預設切面(可擴充它來自定義切面)
  • NameMatchMethodPointcutAdvisor:根據方法名稱進行比對的切面
  • StaticMethodMatcherPointcutAdvisor:用于比對靜态方法的切面

總的來說,讓使用者去配置一個或少數幾個代理,似乎還可以接受,但随着項目的擴大,代理配置就會越來越多,配置的重複勞動就多了,麻煩不說,還很容易出錯。能否讓 Spring 架構為我們自動生成代理呢?

10. Spring AOP:自動代理(掃描 Bean 名稱)

Spring AOP 提供了一個可根據 Bean 名稱來自動生成代理的工具,它就是 BeanNameAutoProxyCreator。是這樣配置的:

<?xml version="1.0" encoding="UTF-8"?>
<beans ...>
    ...
    <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
        <property name="beanNames" value="*Impl"/>                       <!-- 隻為字尾是“Impl”的 Bean 生成代理 -->
        <property name="interceptorNames" value="greetingAroundAdvice"/> <!-- 增強 -->
        <property name="optimize" value="true"/>                         <!-- 是否對代理生成政策進行優化 -->
    </bean>
</beans>           

以上使用 BeanNameAutoProxyCreator 隻為字尾為“Impl”的 Bean 生成代理。需要注意的是,這個地方我們不能定義代理接口,也就是 interfaces 屬性,因為我們根本就不知道這些 Bean 到底實作了多少接口。此時不能代理接口,而隻能代理類。是以這裡提供了一個新的配置項,它就是 optimize。若為 true 時,則可對代理生成政策進行優化(預設是 false 的)。也就是說,如果該類有接口,就代理接口(使用 JDK 動态代理);如果沒有接口,就代理類(使用 CGLib 動态代理)。而并非像之前使用的 proxyTargetClass 屬性那樣,強制代理類,而不考慮代理接口的方式。可見 Spring AOP 确實為我們提供了很多很好地服務!

既然 CGLib 可以代理任何的類了,那為什麼還要用 JDK 的動态代理呢?肯定您會這樣問。

根據多年來實際項目經驗得知:CGLib 建立代理的速度比較慢,但建立代理後運作的速度卻非常快,而 JDK 動态代理正好相反。如果在運作的時候不斷地用 CGLib 去建立代理,系統的性能會大打折扣,是以建議一般在系統初始化的時候用 CGLib 去建立代理,并放入 Spring 的 ApplicationContext 中以備後用。

以上這個例子隻能比對目标類,而不能進一步比對其中指定的方法,要比對方法,就要考慮使用切面與切點了。Spring AOP 基于切面也提供了一個自動代理生成器:DefaultAdvisorAutoProxyCreator。

11. Spring AOP:自動代理(掃描切面配置)

為了比對目标類中的指定方法,我們仍然需要在 Spring 中配置切面與切點:

<?xml version="1.0" encoding="UTF-8"?>
<beans ...>
    ...
    <bean id="greetingAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
        <property name="pattern" value="aop.demo.GreetingImpl.good.*"/>
        <property name="advice" ref="greetingAroundAdvice"/>
    </bean>
    <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator">
        <property name="optimize" value="true"/>
    </bean>
</beans>           

這裡無需再配置代理了,因為代理将會由 DefaultAdvisorAutoProxyCreator 自動生成。也就是說,這個類可以掃描所有的切面類,并為其自動生成代理。

看來不管怎樣簡化,老羅始終解決不了切面的配置,這件繁重的手工勞動。在 Spring 配置檔案中,仍然會存在大量的切面配置。然而在有很多情況下 Spring AOP 所提供的切面類真的不太夠用了,比如:想攔截指定注解的方法,我們就必須擴充 DefaultPointcutAdvisor 類,自定義一個切面類,然後在 Spring 配置檔案中進行切面配置。不做不知道,做了您就知道相當麻煩了。

老羅的解決方案似乎已經掉進了切面類的深淵,這還真是所謂的“面向切面程式設計”了,最重要的是切面,最麻煩的也是切面。

必須要把切面配置給簡化掉,Spring 才能有所突破! 

神一樣的老羅總算認識到了這一點,接受了網友們的建議,內建了 AspectJ,同時也保留了以上提到的切面與代理配置方式(為了相容老的項目,更為了維護自己的面子)。将 Spring 與 AspectJ 內建與直接使用 AspectJ 是不同的,我們不需要定義 AspectJ 類(它是擴充了 Java 文法的一種新的語言,還需要特定的編譯器),隻需要使用 AspectJ 切點表達式即可(它是比正規表達式更加友好的表現形式)。

12. Spring + AspectJ(基于注解:通過 AspectJ execution 表達式攔截方法)

下面以一個最簡單的例子,實作之前提到的環繞增強。先定義一個 Aspect 切面類:

@Aspect
@Component
public class GreetingAspect {
    @Around("execution( aop.demo.GreetingImpl.(..))")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        before();
        Object result = pjp.proceed();
        after();
        return result;
    }
    private void before() {
        System.out.println("Before");
    }
    private void after() {
        System.out.println("After");
    }
}           

注意:類上面标注的 @Aspect 注解,這表明該類是一個 Aspect(其實就是 Advisor)。該類無需實作任何的接口,隻需定義一個方法(方法叫什麼名字都無所謂),隻需在方法上标注 @Around 注解,在注解中使用了 AspectJ 切點表達式。方法的參數中包括一個 ProceedingJoinPoint 對象,它在 AOP 中稱為 Joinpoint(連接配接點),可以通過該對象擷取方法的任何資訊,例如:方法名、參數等。

下面重點來分析一下這個切點表達式:

execution( aop.demo.GreetingImpl.(..))

  • execution():表示攔截方法,括号中可定義需要比對的規則。
  • 第一個“*”:表示方法的傳回值是任意的。
  • 第二個“*”:表示比對該類中所有的方法。
  • (..):表示方法的參數是任意的。

是不是比正規表達式的可讀性更強呢?如果想比對指定的方法,隻需将第二個“*”改為指定的方法名稱即可。

如何配置呢?看看是有多簡單吧:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
               http://www.springframework.org/schema/beans/spring-beans.xsd            http://www.springframework.org/schema/context            http://www.springframework.org/schema/context/spring-context.xsd            http://www.springframework.org/schema/aop            http://www.springframework.org/schema/aop/spring-aop.xs               d">
    <context:component-scan base-package="aop.demo"/>
    <aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>           

兩行配置就行了,不需要配置大量的代理,更不需要配置大量的切面,真是太棒了!需要注意的是 proxy-target-class="true" 屬性,它的預設值是 false,預設隻能代理接口(使用 JDK 動态代理),當為 true 時,才能代理目标類(使用 CGLib 動态代理)。

Spring 與 AspectJ 結合的威力遠遠不止這些,我們來點時尚的吧,攔截指定注解的方法怎麼樣?

13. Spring + AspectJ(基于注解:通過 AspectJ @annotation 表達式攔截方法) 

為了攔截指定的注解的方法,我們首先需要來自定義一個注解:

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

以上定義了一個 @Tag 注解,此注解可标注在方法上,在運作時生效。

隻需将前面的 Aspect 類的切點表達式稍作改動:

@Aspect
@Component
public class GreetingAspect {
    @Around("@annotation(aop.demo.Tag)")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        ...
    }
    ...
}           

這次使用了 @annotation() 表達式,隻需在括号内定義需要攔截的注解名稱即可。

直接将 @Tag 注解定義在您想要攔截的方法上,就這麼簡單:

@Component
public class GreetingImpl implements Greeting {
    @Tag
    @Override
    public void sayHello(String name) {
        System.out.println("Hello! " + name);
    }
}           

以上示例中隻有一個方法,如果有多個方法,我們隻想攔截其中某些時,這種解決方案會更加有價值。

除了 @Around 注解外,其實還有幾個相關的注解,稍微歸納一下吧:

  • @Before:前置增強
  • @After:後置增強
  • @Around:環繞增強
  • @AfterThrowing:抛出增強
  • @DeclareParents:引入增強

此外還有一個 @AfterReturning(傳回後增強),也可了解為 Finally 增強,相當于 finally 語句,它是在方法結束後執行的,也就說說,它比 @After 還要晚一些。

最後一個 @DeclareParents 竟然就是引入增強!為什麼不叫做 @Introduction 呢?我也不知道為什麼,但它幹的活就是引入增強。

14. Spring + AspectJ(引入增強)

為了實作基于 AspectJ 的引入增強,我們同樣需要定義一個 Aspect 類:

@Aspect
@Component
public class GreetingAspect {
    @DeclareParents(value = "aop.demo.GreetingImpl", defaultImpl = ApologyImpl.class)
    private Apology apology;
}           

隻需要在 Aspect 類中定義一個需要引入增強的接口,它也就是運作時需要動态實作的接口。在這個接口上标注了 @DeclareParents 注解,該注解有兩個屬性:

  • value:目标類
  • defaultImpl:引入接口的預設實作類

我們隻需要對引入的接口提供一個預設實作類即可完成引入增強:

public class ApologyImpl implements Apology {
    @Override
    public void saySorry(String name) {
        System.out.println("Sorry! " + name);
    }
}           

以上這個實作會在運作時自動增強到 GreetingImpl 類中,也就是說,無需修改 GreetingImpl 類的代碼,讓它去實作 Apology 接口,我們單獨為該接口提供一個實作類(ApologyImpl),來做 GreetingImpl 想做的事情。

還是用一個用戶端來嘗試一下吧:

public class Client {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("aop/demo/spring.xml");
        Greeting greeting = (Greeting) context.getBean("greetingImpl");
        greeting.sayHello("Jack");
        Apology apology = (Apology) greeting; // 強制轉型為 Apology 接口
        apology.saySorry("Jack");
    }
}           

從 Spring ApplicationContext 中擷取 greetingImpl 對象(其實是個代理對象),可轉型為自己靜态實作的接口 Greeting,也可轉型為自己動态實作的接口 Apology,切換起來非常友善。

使用 AspectJ 的引入增強比原來的 Spring AOP 的引入增強更加友善了,而且還可面向接口程式設計(以前隻能面向實作類),這也算一個非常巨大的突破。

這一切真的已經非常強大也非常靈活了!但仍然還是有使用者不能嘗試這些特性,因為他們還在使用 JDK 1.4(根本就沒有注解這個東西),怎麼辦呢?沒想到 Spring AOP 為那些遺留系統也考慮到了。

15. Spring + AspectJ(基于配置)

除了使用 @Aspect 注解來定義切面類以外,Spring AOP 也提供了基于配置的方式來定義切面類:

<?xml version="1.0" encoding="UTF-8"?>
<beans ...">
    <bean id="greetingImpl" class="aop.demo.GreetingImpl"/>
    <bean id="greetingAspect" class="aop.demo.GreetingAspect"/>
    <aop:config>
        <aop:aspect ref="greetingAspect">
            <aop:around method="around" pointcut="execution( aop.demo.GreetingImpl.(..))"/>
        </aop:aspect>
    </aop:config>
</beans>           

使用 <aop:config> 元素來進行 AOP 配置,在其子元素中配置切面,包括增強類型、目标方法、切點等資訊。

無論您是不能使用注解,還是不願意使用注解,Spring AOP 都能為您提供全方位的服務。

好了,我所知道的比較實用的 AOP 技術都在這裡了,當然還有一些更為進階的特性,由于個人精力有限,這裡就不再深入了。

還是依照慣例,給一張牛逼的高清無碼思維導圖,總結一下以上各個知識點:

詳細解讀 Spring AOP 面向切面程式設計(二)

再來一張表格,總結一下各類增強類型所對應的解決方案:

增強類型 基于 AOP 接口 基于 @Aspect 基于 <aop:config>
Before Advice(前置增強) MethodBeforeAdvice @Before <aop:before>
AfterAdvice(後置增強) AfterReturningAdvice @After <aop:after>
AroundAdvice(環繞增強) MethodInterceptor @Around <aop:around>
ThrowsAdvice(抛出增強 ThrowsAdvice @AfterThrowing <aop:after-throwing>
IntroductionAdvice(引入增強) DelegatingIntroductionInterceptor @DeclareParents <aop:declare-parents>

最後給一張 UML 類圖描述一下 Spring AOP 的整體架構:

詳細解讀 Spring AOP 面向切面程式設計(二)
關注微信公衆号【 程式員的夢想 】,專注于Java,SpringBoot,SpringCloud,微服務,Docker以及前後端分離等全棧技術。
詳細解讀 Spring AOP 面向切面程式設計(二)