1. 程式人生 > >Spring AOP註解配置demo

Spring AOP註解配置demo

https://blog.csdn.net/yhl_jxy/article/details/78815636#commentBox

 

一 AOP的概念    AOP(Aspect Oriented Programming),即為面向切面程式設計。在軟體開發中,散佈於應用中多處的
功能被稱為橫切關注點(cross-cutting concern),通常來說,這些橫切關注點從概念上是與應用的業務
邏輯分離的。比如,宣告式事務、日誌、安全、快取等等,都與業務邏輯無關,可以將這些東西抽象成
為模組,採用面向切面程式設計的方式,通過宣告方式定義這些功能用於何處,通過預編譯方式和執行期動
態代理實現這些模組化橫切關注點程式功能進行統一維護,從而將橫切關注點與它們所影響的物件之
間分離出來,就是實現解耦。
橫切關注點可以被模組化為特殊的類,這些類被稱為切面(aspect)。這樣做有兩個優點:
1)每個關注點都集中於一個地方,而不是分散到多處程式碼中;
2)服務模組更簡潔,因為它們只包含主要的關注點的程式碼(核心業務邏輯),
而次要關注點的程式碼(日誌,事務,安全等)都被轉移到切面中。
二 AOP術語1、通知(Advice)切面類有自己要完成的工作,切面類的工作就稱為通知。通知定義了切面是做什麼以及何時使用。
"做什麼",即切面類中定義的方法是幹什麼的;
"何時使用",即5種通知型別,是在目標方法執行前,還是目標方法執行後等等;
"何處做",即通知定義了做什麼,何時使用,但是不知道用在何處,而切點定義的就是告訴通知應該用在
哪個類的哪個目標方法上,從而完美的完成橫切點功能。
Spring切面定義了5種類型通知:
1)前置通知(Before):在目標方法被呼叫之前呼叫通知功能。
2)後置通知(After):在目標方法完成之後呼叫通知,不會關心方法的輸出是什麼。
3)返回通知(After-returning): 在目標方法成功執行之後呼叫通知。
4)異常通知(After-throwing):在目標方法丟擲異常後呼叫通知。
5)環繞通知(Around):通知包裹了被通知的方法,在被通知的方法呼叫之前和之後執行自定義的行為。
2、連線點(Join point)在我們的應用程式中有可能有數以萬計的時機可以應用通知,而這些時機就被稱為連線點。
連線點是在應用執行過程中能夠插入切面的一個點。這個點可以是呼叫方法時、丟擲異常時、
甚至修改一個欄位時。切面程式碼可以利用這些點插入到應用的正常流程之中,並新增新的行為。
連線點是一個虛概念,可以把連線點看成是切點的集合。
下面我們看看切點是神馬鬼?
3、切點(Poincut)連線點談的是一個飄渺的大範圍,而切點是一個具體的位置,用於縮小切面所通知的連線點的範圍。
前面說過,通知定義的是切面的"要做什麼"和"在何時做",是不是沒有去哪裡做,而切點就定義了"去何處做"。
切點的定義會匹配通知所要織入的一個或多個連線點。我們通常使用明確的類和方法名稱,或者是使用
正則表示式定義所匹配的類和方法名稱來指定切點。說白了,切點就是讓通知找到"發洩的地方"。
4、切面(Aspect)切面是通知和切點的結合,通知和切點共同定義了切面的全部內容。因為通知定義的是切面的
"要做什麼"和"在何時做",而切點定義的是切面的"在何地做"。將兩者結合在一起,就可以完美的
展現切面在何時,何地,做什麼(功能)。
5、引入(Introduction)引入這個概念就比較高大尚,引入允許我們向現有的類新增新方法或屬性。
主要目的是想在無需修改A的情況下,引入B的行為和狀態。
6、織入(Weaving)織入是把切面應用到目標物件並建立新的代理物件的過程。切面在指定的連線點被織入到目標物件中。
在目標物件的生命週期裡有多個點可以進行織入:
編譯期: 
    切面在目標類編譯時被織入。需要特殊的編譯器,是AspectJ的方式,不是spring的菜。
類載入期: 
    切面在目標類載入到JVM時被織入。這種方式需要特殊的類載入器,它可以在目標類被引入應用之前
    增強該目標類的位元組碼。AspectJ5支援這種方式。
執行期:  
     切面在應用執行的某個時刻被織入。一般情況下,在織入切面時,AOP容器會為目標物件動態的建立
     一個代理物件。而這正是Spring AOP的織入切面的方式。
三 AOP實戰    舉個開會的例子,在家開家庭會議,上學開班會,上班了開工作會議,人死了開追悼會。
人的一生都在開會中出生,然後在開會中死去。每個人都離不開開會。而上學時候開會,
開會前老師都說: 靜一下,現在通訊發達了,網路時代,老師開會估計都說: 把手機收起來。
    而工作了,咱們最常聽見的是: 把手機調成靜音,或者關機。其實在開會之前的這些都不是開會要將的核心,
而開會後搞不好還要寫個總結報告,所有開會前開會後做的這些事情,都與核心業務邏輯開會都是獨立開來的,
我們用開會前,開會,開會後來舉例分析一下AOP的使用。AOP的實現可以通過註解方式或XML方式,
這裡主要分析註解方式,下一篇有機會再討論下XML方式。
1、建立切面類切面類包含通知和切入點,在建立切面類之前,我們需要了解下AspectJ的切點表示式,因為我們需要通過
切點表示式定義切點,用於準確的定位應該在什麼地方應用切面的通知。
直接上一個圖,解釋下切點表示式的元素:


對切點表示式有所瞭解後,我們通過@Aspect註解標註建立切面類。
package com.lanhuigu.spring; import org.aspectj.lang.annotation.After;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component; /** *  註解@Aspect標識該類為切面類 */// @

[email protected] class Person {     /**     * 開會之前--找個位置坐下     */    @Before("execution(* com.lanhuigu.spring.ConferenceServiceImpl.conference(..))")    public void takeSeats() {        System.out.println("找位置坐");    }     /**     * 開會之前--手機調成靜音     */    @Before("execution(* com.lanhuigu.spring.ConferenceServiceImpl.conference(..))")    public void silenceCellPhones() {        System.out.println("手機調成靜音");    }     /**     * 開會之後--寫會議總結報告     */    @After("execution(* com.lanhuigu.spring.ConferenceServiceImpl.conference(..))")    public void summary() {        System.out.println("寫會議總結報告");    } }在定義完這個切面類之後,有沒有發現3個方法通知型別之後的execution表示式內容完全一致,
如果你有程式碼強迫症,一定想把他們提成一個公用的,別的地方只需要引用一下就行,
這個地方使用@Pointcut滿足你的強迫症。
優化後的切面類:
package com.lanhuigu.spring; import org.aspectj.lang.annotation.After;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component; /** *  註解@Aspect標識該類為切面類 */// @
[email protected]
class Person { //    /**//     * 開會之前--找個位置坐下//     *///    @Before("execution(* com.lanhuigu.spring.ConferenceServiceImpl.conference(..))")//    public void takeSeats() {//        System.out.println("找位置坐");//    }////    /**//     * 開會之前--手機調成靜音//     *///    @Before("execution(* com.lanhuigu.spring.ConferenceServiceImpl.conference(..))")//    public void silenceCellPhones() {//        System.out.println("手機調成靜音");//    }////    /**//     * 開會之後--寫會議總結報告//     *///    @After("execution(* com.lanhuigu.spring.ConferenceServiceImpl.conference(..))")//    public void summary() {//        System.out.println("寫會議總結報告");//    }    /**     * =========================================================================     * 從上面的執行程式碼可以看出切點execution表示式內容都是一樣,     * 我們可以通過@Pointcut進行優化。     * =========================================================================     */     /**     * 通過註解@Pointcut定義切點,conference()只是一個標識,無所謂是什麼,     * 方法中內容本身也是空的,使用該切點的地方直接通過標識conference()引用切點表示式。     */    @Pointcut("execution(* com.lanhuigu.spring.ConferenceServiceImpl.conference(..))")    public void conference() {}     /**     * 開會之前--找個位置坐下     */    @Before("conference()")    public void takeSeats() {        System.out.println("找位置坐");    }     /**     * 開會之前--手機調成靜音     */    @Before("conference()")    public void silenceCellPhones() {        System.out.println("手機調成靜音");    }     /**     * 開會之後--寫會議總結報告     */    @After("conference()")    public void summary() {        System.out.println("寫會議總結報告");    } }咱們解釋一下切點表示式的含義:


通過execution指示器,選擇ConferenceServiceImpl類中的conference()方法。
方法表示式以“*”號開始,表明了我們不關心方法的返回值是神馬鬼。
對於方法引數列表通過兩個點表示,表示我們不在乎conference的引數。
在執行表示式的時候,我們可以通過邏輯運算子&&(and) , ||(or) , !(not)對錶達式進行搭配。比如:
execution(* com.lanhuigu.spring.ConferenceServiceImpl.conference(..)          && within(com.lanhuigu.spring.*))
增加了一個限制就是我們只管com.lanhuigu.spring下的包,這裡的&&可以使用and來替代,
同理|| , !都是一樣的用法,靈活多變,只能根據實際情況看著辦。
2、建立目標類,定義目標方法目標類就是我們的核心,開會,開會前和開會後,中間休息等等地方都是插入切面通知的地方,
這些地方的集合就是連線點,而某一個具體的位置就是切點,連線點就是切點的集合。
我們建立一個目標類,作為切面插入的目標。
建立一個開會的介面:
package com.lanhuigu.spring; public interface ConferenceService {    void conference();}開會介面的實現:
package com.lanhuigu.spring; import org.springframework.stereotype.Component; @Componentpublic class ConferenceServiceImpl implements ConferenceService {     @Override    public void conference() {        System.out.println("開會......");    } }
3、 編寫配置類,啟動AOP代理功能切面類,目標方法都建立完了,但是我們的切面類在啟動時也不會被轉化為代理的。
現在處於完事具備,只欠東風的狀態。需要一陣風,打一場赤壁之戰。
古有諸葛亮借東風,今有Spring通過@EnableAspectJAutoProxy註解啟動AspectJ自動代理。
JavaConfg配置:
package com.lanhuigu.spring; import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.EnableAspectJAutoProxy; /** * * Jdk代理:基於介面的代理,一定是基於介面,會生成目標物件的介面的子物件。 * Cglib代理:基於類的代理,不需要基於介面,會生成目標物件的子物件。 * * 1. 註解@EnableAspectJAutoProxy開啟代理; * * 2. 如果屬性proxyTargetClass預設為false, 表示使用jdk動態代理織入增強; * * 3. 如果屬性proxyTargetClass設定為true,表示使用cglib動態代理技術織入增強; * * 4. 如果屬性proxyTargetClass設定為false,但是目標類沒有宣告介面, *    spring aop還是會使用cglib動態代理,也就是說非介面的類要生成代理都用Cglib。 */@
[email protected]
(proxyTargetClass = true)@ComponentScanpublic class ConcertConfig {     /**     * 通過@Bean定義,注入到spring容器,也可以通過在Person類上新增@Component,實現同樣的效果。     */    @Bean    public Person person() {        return new Person();    } }4、 測試類package com.lanhuigu.spring; import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.test.context.ContextConfiguration;import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(classes = ConcertConfig.class)public class TestAopAnnotation {     @Autowired    private ConferenceServiceImpl conferenceService;     @Test    public void testAop() {        conferenceService.conference();    } }
5、程式執行結果手機調成靜音找位置坐開會......寫會議總結報告
 
在上面我們通過@EnableAspectJAutoProxy開啟的代理,如果使用的不是JavaConfig,
而是xml,我們可以通過xml中的元素<aop:aspectj-autoproxy>開啟代理。
spring的xml配置如下:
<?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="com.lanhuigu.*"/>    <!-- 啟用AspectJ自動代理 -->    <aop:aspectj-autoproxy proxy-target-class="true"/>    <!--       切面類宣告, 如果不想在這裡顯示宣告,可以在該類加上@Component註解,spring在掃描建立bean容器時會自動建立。    -->    <bean class="com.lanhuigu.spring.Person"/></beans>以上需要引入aop名稱空間。我們寫一個測試類試試, 需要註釋掉ConcertConfig和TestAopAnnotation,
否則會出現雙份橫切內容:
package com.lanhuigu.spring; import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;import org.springframework.test.context.ContextConfiguration;import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; /** * 注意: *   測試該類時,註釋掉ConcertConfig類和TestAopAnnotation類。 */public class TestAopXml {     @Test    public void testAopXml() {        // 根據配置檔案建立spring容器        ApplicationContext context =                new ClassPathXmlApplicationContext("applicationContext.xml");        /*         * 從容器中獲取Bean:         * 可以看到在xml中沒有配置ConferenceService或ConferenceServiceImpl的內容,         * 為什麼讀取applicationContext.xml配置檔案建立spring容器後根據Bean ID "conferenceServiceImpl"         * 從容器中獲取ConferenceServiceImpl物件?         * 因為在xml配置中<context:component-scan base-package="com.lanhuigu.*"/>         * 會掃描所有帶有spring註解的類,並納入到spring容器,而@Component註解註冊的Bean ID預設         * 為類首字母小寫作為Bean ID,所以ConferenceServiceImpl類在spring容器中的ID為conferenceServiceImpl。         * 從而可以根據Bean ID從容器獲取bean物件使用。         */        ConferenceServiceImpl conferenceService = (ConferenceServiceImpl)context.getBean("conferenceServiceImpl");        // 呼叫Bean方法        conferenceService.conference();    } }程式輸出的結果一樣,到此,我們基本上了解了基於註解的AOP實戰。
四  環繞通知關於通知型別,需要單獨分析的是環繞通知,他跟其他通知型別不一樣,環繞通知也是最為強大的一種通知方式,
所謂的環繞通知,顧名思義,它能夠讓你所編寫的邏輯將被通知的目標方法全部包裝起來。
實際上就像我們前面寫的開會前,開會後乾的哪些事情,對於環繞通知來說,一個方法就搞定了,因為他包圍了目標方法,
等同於在一個通知方法中同時編寫了前置通知和後置通知,環繞通知都會為執行開會前,開會後等等邏輯。
把前面的Person類重構為如下內容:
package com.lanhuigu.spring; import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.*;import org.springframework.stereotype.Component; /** *  註解@Aspect標識該類為切面類 */// @[email protected] class Person {     /**     * 通過註解@Pointcut定義切點,conference()只是一個標識,無所謂是什麼,     * 方法中內容本身也是空的,使用該切點的地方直接通過標識conference()引用切點表示式。     */    @Pointcut("execution(* com.lanhuigu.spring.ConferenceServiceImpl.conference(..))")    public void conference() {}     @Around("conference()")    public void testAround(ProceedingJoinPoint jp) {        try {            System.out.println("1111111111111");            System.out.println("2222222222222");            jp.proceed();            System.out.println("3333333333333");        } catch (Throwable e) {            System.out.println("開會不爽,打起來了");        }    } } 
重構完後再執行下TestAopAnnotation測試類,輸出如下結果:
11111111111112222222222222開會......3333333333333--------------------- 作者:街燈下的小草 來源:CSDN 原文:https://blog.csdn.net/yhl_jxy/article/details/78815636 版權宣告:本文為博主原創文章,轉載請附上博文連結!