詳解Spring的AOP切面編程

一 、基本理解

AOP,面向切面編程,作爲Spring的核心思想之一,度娘上有太多的教程啊、解釋啊,但博主還是要自己按照自己的思路和理解再來闡釋一下。原因很簡單,別人的思想終究是別人的,自己的理解纔是自己的,尤其當用文字、代碼來闡述一遍過後,理解層面上又似乎變得不一樣了。

博主就不概念化解釋AOP了,這裏只簡單說下爲啥要使用這樣一種編程思想和相關的AOP技術。其實很簡單,就是爲了業務模塊間的解耦,尤其在現代的軟件設計中強調高內聚、低耦合,要求我們的業務模塊化,各個功能模塊只關注自己的邏輯實現,而不用關注與主業務邏輯不相關的功能。然而,在面向對象的系統設計中,系統中不可或缺的一些功能如日誌、事務是散佈在應用各處與主邏輯代碼高度耦合的,這讓主業務代碼變得相當冗餘、難以複用。而在面向切面的編程思想中,我們是考慮將那些散佈在應用多處的重複性代碼抽離出來封裝成模塊化的功能類,一來讓主業務邏輯更加專注、簡單,二來模塊化的日誌、事務也便於複用和移植,這就是解耦的思想。但是,解耦並不等於斷耦,抽離的功能最終還是要以某種方式"還"(qie)回去,否則應用的功能就不完善了。這裏,"還"(qie)回去的技術就是AOP技術,而這種解耦的編程思想就是AOP的編程思想。在Java的生態中,提供AOP技術的框架也有不少,主要的運用就是Spring的AOP和Spring"借鑑"幷包含進了自己的生態體系的 AspectJ的AOP。

二 、核心概念

爲便於理解闡述,博主先嘮叨幾句。上面的基本闡述中,我們知道,AOP要乾的事情其實也很簡單,就是要將對象編程中,抽離出來的模塊代碼(權限、日誌、事務)還(qie)回去,但肯定不能是對象思維中的代碼冗雜的組合,而是應該更加高明一些,最好能在原來的業務代碼執行的過程中不知不覺的還(qie)回去——也就是說要在主業務邏輯執行的流程裏,動態的添加(權限、日誌、事務)代碼抽離前乾的那些事情。怎麼能做到呢?用代理啊,親!想想,我們對一個目標對象採用代理不就是爲了在目標對象邏輯執行時候通過在代理對象中乾點額外的事情嗎?這樣,雖然,原目標對象並沒有增加任何額外的功能,通過代理的一番暗中騷操作,展示給調用者的就好像目標對象有了代理對象中的那些額外的功能一樣。於是你也很好理解,爲什麼Spring的AOP中要用到動態代理了。好了,經過一番嘮叨,我們再來看AOP的相關術語就要好理解得多——

1、橫切關注點

如上描述,我們把日誌、事務、權限等代碼重複性極高卻散佈在應用程序各個地方的功能稱爲橫切關注點。

2、連接點(Join Point)

被代理的目標對象在業務邏輯執行的過程中,可以被代理對象動態切入代理功能的一些時機節點,比如方法執行前、後,異常時,成功返回時等等。當然,這只是針對Spring來說的,因爲Spring基於動態代理,只支持方法級別的AOP切入,實際上,AspectJ、JBoss等框架的AOP還能提供構造器以及更細粒度字段等的連接點支持。

3、通知(Advice)

如上描述,就是代理對象在什麼時機要爲目標對象額外增加的功能代碼,因而很多教程資料上稱之爲 增強。請注意博主對通知的描述裏有提到什麼時機,這很好理解,你的代理對象要給目標對象增加額外功能,總得清楚要增加在哪些時機吧,所以,我們的通知按照功能切入的時機分爲以下5個類型:

  • 前置通知(Before):被代理對象目標方法被調用之前執行通知代碼;

  • 後置通知(After):被代理對象目標方法執行完成之後執行通知代碼,不管方法是否成功執行(這相當於異常捕獲中的finally塊,總是會執行的意思,所以博主覺得如果將其命名爲最終通知要更好理解些);

  • 異常通知(After-throwing):被代理對象目標方法拋出異常後執行通知代碼;

  • 返回通知(After-returning):被代理對象目標方法成功執行後執行通知代碼;

  • 環繞通知(Around) :包裹被代理對象的目標方法,相當於結合了以上的所有通知類型。

4、切點(Pointcut)

被代理對象目標方法執行過程中真正的要執行通知代碼的一個或多個連接點,這會通過切點表達式語言進行匹配。

6、切面(Aspect)

通知和切點的結合,切面完整的包含了代理對象對目標對象進行通知的三個基本要素:何時(前、後、異常、環繞、返回等),何地(切點),幹什麼(通知切入的功能)。

7、織入(Weaving)

 
將切面應用到被代理對象並創建代理對象的的過程。切面會在指定的連接點(切點)被織入到被代理對象的執行方法中。其實,被代理對象的生命週期中有多個時機(編譯、類加載、運行)都可以進行織入,就 Spring 而言,是在被代理對象運行期進行代理對象的創建,織入切面邏輯的。

注:以上描述都是基於Spring 方法級別的AOP 來進行闡述

三、基礎代碼示例

說了那麼多,還是上代碼最簡單直接。準備工作:

① 測試依賴的包及其版本(注:很多教程中都提到需要 aopalliance包,但是博主測試過程中並沒有確認此包存在的必要性)

    aspectjweaver-1.9.2.jar
    commons-logging-1.2.jar
    spring-aop-4.3.18.RELEASE.jar
    spring-beans-4.3.18.RELEASE.jar
    spring-context-4.3.18.RELEASE.jar
    spring-core-4.3.18.RELEASE.jar
    spring-expression-4.3.18.RELEASE.jar
    spring-test-4.3.18.RELEASE.jar

② 定義兩個基礎模型類(如下),業務是:給只有打電話功能的手機動態的添加 拍照、玩遊戲這樣的非主業務功能

//主業務功能
public class HuaWeiPhone  {
    public void ring() {
        System.out.println("華爲手機,產銷第一");
    }
}
//額外添加的功能
public class Photograph {
    public void  takePictures(){
        System.out.println("華爲手機,拍照牛批");
    }
    public void  playGames(){
        System.out.println("華爲手機,遊戲玩得也這麼暢快");
    }
}

1、XML配置的方式

根據以上Java代碼,進行非常簡單的配置,就能看到動態的爲手機增加了拍照功能的效果了——

<bean  class="main.java.model.HuaWeiPhone"/>
    <bean id="photograph" class="main.model.Photograph"/>
    <aop:config>
        <aop:pointcut id="ring" expression="execution(* main.model.HuaWeiPhone.ring(..))"/>
        <aop:aspect  ref="photograph">
            <aop:before method="takePictures" pointcut-ref="ring"/>
            <aop:after method="playGames" pointcut-ref="ring"/>
        </aop:aspect>
    </aop:config>

在Spring環境下測試類XML配置——

@RunWith(SpringJUnit4Cla***unner.class)
@ContextConfiguration(locations = "classpath:main/resource/applicationContext.xml")
public class SpringTest {

    @Autowired
    HuaWeiPhone huaWeiPhone;

    @Test
    public void testXml(){
        huaWeiPhone.ring();
    }
}

輸出結果

詳解Spring的AOP切面編程

2、Java註解的方式

需要先說明的是,Spring的基於註解的 AOP 實際上是借鑑吸收了AspectJ的功能,所以你會看到很多類似 AspectJ 框架的註解。在之前的模型類上通過添加相應的註解改造成一個切面——

@Aspect  //將該類標註爲一個AOP切面
@Component
public class Photograph {
    @Pointcut("execution(* main.model.HuaWeiPhone.ring(..))")
        public void chenbenbuyi (){
    }
    @Before("chenbenbuyi()")
        public void  takePictures(){
        System.out.println("華爲手機,拍照牛批");
    }
    @After("chenbenbuyi()")
        public void  playGames(){
        System.out.println("華爲手機,遊戲玩得也這麼暢快");
    }
}

同樣的,目標類(HuaWeiPhone)上也要添加@Componet註解將其交給Spring 容器管理。然後,如果是純註解的話,還要一個配置類——

//配置註解掃描
@ComponentScan(basePackages = "main")
//啓用AspectJ的自動代理功能
@EnableAspectJAutoProxy
public class JavaConfig {
}

最後,在Spring的環境下測試——

@RunWith(SpringJUnit4Cla***unner.class)
//@ContextConfiguration(locations = "classpath:main/resource/applicationContext.xml")
@ContextConfiguration(classes = JavaConfig.class)
public class SpringTest {

    @Autowired
    HuaWeiPhone huaWeiPhone;

    @Test
    public void testAnno(){
        huaWeiPhone.ring();
    }
}

結果同上,這裏就不展示了。不過需要注意的是,不管什麼配置方式,基於Spring 的AOP編程實現的前提都是要將通知對象和被通知方法交給Spring IOC容器管理,也就是要聲明爲Spring 容器中的Bean。

四、需求升級

在第三部分中,博主只是展示了最最簡單的AOP功能實現,還有稍微複雜的技能點沒有列出。比如,5種通知類型中的環繞通知呢?再比如,我的切面代碼如果要傳參數怎麼辦呢?接下來博主依次講解。

① 關於環繞通知的運用

基於 二 中的闡述,5 種通知類型中 環繞通知 是功能最爲強大,實際上,我們可以在環繞通知中個性化的定製出前置 、後置、異常和返回的通知類型,而如果單獨的採用前置、後置等通知類型,如果業務涉及多線程對成員變量的修改,可能出現併發問題,所以環繞要比單獨的使用另外的幾種通知類型更加的安全。我們對上面的切面基於環繞通知進行修改,使之包含所有的通知類型的功能——

@Aspect
@Component
public class Photograph {
    @Pointcut("execution(* main.model.HuaWeiPhone.ring(..))")
        public void chenbenbuyi (){
    }
    @Around("chenbenbuyi()")
        public void  surround(ProceedingJoinPoint joinPoint){
        try {
            System.out.println("目標方法執行前執行,我就是前置通知");
            joinPoint.proceed();
            // ①
            //            int i =1/0;       // ②  製造異常
            System.out.println("正常返回,我就是返回通知");
        }
        catch (Throwable e) {
            System.out.println("出異常了,我就是異常通知");
        }
        finally {
            System.out.println("後置通知,我就是最終要執行的通知");
        }
    }
}

XML的配置和上面的其它通知類型一樣,只不過元素標籤爲 <aop:around />而已。上面的打印語句的位置就對應了其它幾種通知類型執行切面邏輯的時機。這裏注意,環繞通知方法體中需要有 ProceedingJoinPoint 接口作爲參數,在環繞通知中,通過執行該參數的 proceed() 方法來調用通知需要切入的目標方法。如果不執行 ① 處的調用,被通知方法實際上會被阻塞掉,所以你會看到,明明測試中執行了被通知的方法,實際卻沒有執行。該參數對象還可以獲取方法簽名、代理對象、目標對象等信息,可以自己測試着玩。

② 關於通知的傳參問題

切面雖然是通用邏輯,但實際在切入不同的目標方的時候,可能還是希望通知方法根據被通知方法的不同(比如參數不同)而執行不一樣的邏輯,這就要求我們的通知也能獲取到被通知方法傳入的參數。通過切點表達式,這也很容易辦到。首先我們修改被通知的方法可以傳參:

public void ring(String str) {
    System.out.println("華爲手機,產銷第一");
    int i =1/0;
}

然後切面中切點表達式和切面方法也做對應的修改——

@Aspect
@Component
public class Photograph {
    /**
     * Spring 藉助於 AspectJ的切點表達式語言中的arg()表達式執行參數的傳遞工作
     */
    @Pointcut("execution(* main.model.HuaWeiPhone.ring(String))&&args(name)")
        public void chenbenbuyi (String name){
    }
    /**
     *  ① 在引用空標方法的切點表達式時同時也就要傳入相應的參數
     *  ② 傳入的參數形參名字必須和切點表達式中的相同
     */
    @Before("chenbenbuyi(name)")
        public void  takePictures(String name){
        System.out.println("喂喂,你好我是 "+ name);
    }
    /**
     *  對於異常通知,有專門的異常參數可以直接獲取到被通知方法出現異常後信息的
     */
    @AfterThrowing(pointcut = "chenbenbuyi(name)",throwing = "e")
        public void  excep(String name,Throwable e){
        System.out.println("出異常了,異常信息是:"+e.getMessage());
    }
}

XML中配置參數傳遞

<bean  class="main.java.model.HuaWeiPhone"/>
    <bean id="photograph" class="main.java.model.Photograph"/>
    <aop:config>
        <aop:pointcut id="ring" expression="execution(* main.java.model.HuaWeiPhone.ring(..)) and args(name)"/>
        <aop:aspect  ref="photograph">
            <aop:before method="takePictures" pointcut-ref="ring" arg-names="name" />
            <aop:after-throwing method="excep"  throwing="e" arg-names="name,e" pointcut-ref="ring"/>
        </aop:aspect>
    </aop:config>

測試代碼——

@RunWith(SpringJUnit4Cla***unner.class)
//@ContextConfiguration(locations = "classpath:main/resource/applicationContext.xml")
@ContextConfiguration(classes = JavaConfig.class)
public class SpringTest {

    @Autowired
    HuaWeiPhone huaWeiPhone;

    @Test
    public void testAnno(){
        huaWeiPhone.ring("Java高級架構獅");
    }
}

注意點:

  • XML配置中由於 &符號有特殊含義,所以 切點表達式中 連接形參名的時候就不能再使用註解中的 && ,而應該使用 and 代替,同樣的如果有 或(|| )非 (!)操作,分別使用 or 和 not 代替。

  • 註解和XML配置中切點表達式描述形參類型的地方博主採用了不同的方式,因爲 .. 就表示任意類型,可以不用指明。

五、切點表達式常用圖解

詳解Spring的AOP切面編程

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