詳細解讀 Spring AOP 面向切面編程(二)

本文是《詳細解讀 Spring AOP 面向切面編程(一)》的續集。 php

在上篇中,咱們從寫死代碼,到使用代理;從編程式 Spring AOP 到聲明式 Spring AOP。一切都朝着簡單實用主義的方向在發展。沿着 Spring AOP 的方向,Rod Johnson(老羅)花了很多心思,都是爲了讓咱們使用 Spring 框架時不會感覺到麻煩,但事實卻並不是如此。那麼,後來老羅究竟對 Spring AOP 作了哪些改進呢? java

如今繼續! 程序員

9. Spring AOP:切面 正則表達式

以前談到的 AOP 框架其實能夠將它理解爲一個攔截器框架,但這個攔截器彷佛很是武斷。好比說,若是它攔截了一個類,那麼它就攔截了這個類中全部的方法。相似地,當咱們在使用動態代理的時候,其實也遇到了這個問題。須要在代碼中對所攔截的方法名加以判斷,才能過濾出咱們須要攔截的方法,想一想這種作法確實不太優雅。在大量的真實項目中,彷佛咱們只須要攔截特定的方法就好了,不必攔截全部的方法。因而,老羅同志藉助了 AOP 的一個很重要的工具,Advisor(切面),來解決這個問題。它也是 AOP 中的核心!是咱們關注的重點! spring

也就是說,咱們能夠經過切面,將加強類與攔截匹配條件組合在一塊兒,而後將這個切面配置到 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.xsd">
    <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 技術都在這裏了,固然還有一些更爲高級的特性,因爲我的精力有限,這裏就再也不深刻了。

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

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

加強類型 基於 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 的總體架構:



關注微信公衆號【程序員的夢想】,專一於Java,SpringBoot,SpringCloud,微服務,Docker以及先後端分離等全棧技術。

在這裏插入圖片描述
相關文章
相關標籤/搜索