詳解Spring的AOP切面編程

一 、基本理解

AOP,面向切面編程,做爲Spring的核心思想之一,度娘上有太多的教程啊、解釋啊,但博主仍是要本身按照本身的思路和理解再來闡釋一下。緣由很簡單,別人的思想終究是別人的,本身的理解纔是本身的,尤爲當用文字、代碼來闡述一遍事後,理解層面上又彷佛變得不同了。java

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

二 、核心概念

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

一、橫切關注點

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

二、鏈接點(Join Point)

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

三、通知(Advice)

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

  • 前置通知(Before):被代理對象目標方法被調用以前執行通知代碼;
  • 後置通知(After):被代理對象目標方法執行完成以後執行通知代碼,無論方法是否成功執行(這至關於異常捕獲中的finally塊,老是會執行的意思,因此博主以爲若是將其命名爲最終通知要更好理解些);
  • 異常通知(After-throwing):被代理對象目標方法拋出異常後執行通知代碼;
  • 返回通知(After-returning):被代理對象目標方法成功執行後執行通知代碼;
  • 環繞通知(Around) :包裹被代理對象的目標方法,至關於結合了以上的全部通知類型。

四、切點(Pointcut)

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

## 六、切面(Aspect)多線程

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

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

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

3、基礎代碼示例

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

① 測試依賴的包及其版本(注:不少教程中都提到須要 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("華爲手機,遊戲玩得也這麼暢快");
    }
}

一、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(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:main/resource/applicationContext.xml")
public class SpringTest {

    @Autowired
    HuaWeiPhone huaWeiPhone;

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

輸出結果

二、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(SpringJUnit4ClassRunner.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。

4、需求升級

在第三部分中,博主只是展現了最最簡單的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(SpringJUnit4ClassRunner.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配置中切點表達式描述形參類型的地方博主採用了不一樣的方式,由於 .. 就表示任意類型,能夠不用指明。

5、切點表達式經常使用圖解

讀者福利:

分享免費學習資料

針對於Java程序員,我這邊準備免費的Java架構學習資料(裏面有高可用、高併發、高性能及分佈式、Jvm性能調優、MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)

爲何某些人會一直比你優秀,是由於他自己就很優秀還一直在持續努力變得更優秀,而你是否是還在知足於現狀心裏在竊喜!但願讀到這的您能點個小贊和關注下我,之後還會更新技術乾貨,謝謝您的支持!

資料領取方式:加入Java技術交流羣963944895點擊加入羣聊,私信管理員便可免費領取

相關文章
相關標籤/搜索