Spring實戰——面向切面的Spring

面向切面是Spring又一大核心,本章我們就來詳細瞭解下:面向切面編程的基本原理以及創建使用切面的常用方法,如何爲POJO創建切面,使用@AspectJ註釋,爲AspectJ切面注入依賴。

爲什麼要AOP
日誌、安全和事物管理是軟件中很重要的組成部分,但是如果每個應用對象都需要自己處理這些問題,不僅開發者會十分厭煩,而且也不利於維護、複用以及擴展。這些分佈於應用多處的功能被稱爲橫切關注點

依賴注入(DI)是爲了應用對象之間的解耦,面向切面(AOP)則是爲了橫切關注點和應用對象之間的解耦。

1. 什麼是面向切面(繼承、委託和切面)
AOP也是一種複用的手段,那些橫切關注點都是應該與核心功能分離的職責。就複用來說還有繼承和委託。
如果使用繼承,整個應用系統很多地方都得使用相同的基類,比如在Android開發中Activity和Service它們都有可能用到緩存處理(舉個例子,緩存可以和網絡請求處理一起放在數據處理層和控制層分離),如果把緩存放到基類中,那麼activity和service都要分別創建一個包含該功能的基類,因爲很難修改activity和service的基類,它在framework中。因此基於繼承的複用在系統級別是很難實現的,應爲應用範圍越大,系統中可能包含多個角度的劃分,你要修改基類會造成很大影響。另外使用繼承本身還會造成類數量”爆炸“,使得系統龐大臃腫。

而是用委託可以很大程度上解決,但是委託一般使用組合的方式對接受委託對象進行調用,被委託的服務對委託者來說仍舊是可見的,有時可能需要對委託對象進行復雜的調用,使得類與類之間的聯繫變得不是很清晰。

切面提供了另一種選擇,仍然是在集中的地方定義通用功能,橫切關注點可以被模塊化爲特殊的類,首先每個關注點關注一個問題集中到一處不是分散到多處代碼。其次,服務模塊更加簡潔,只需要關心核心功能,因爲如果調用切面服務模塊不用關心,交給AOP容器來完成(運行期是這樣,編譯期和類加載期稍後討論),很類似依賴倒轉,但它解決是行爲之間的耦合。

1.1 AOP術語

通知(advice):通知決定切面何時使用,是方法前還是方法後,spring提供了5種選擇,Before,After,After-returning(成功後調用),After-throwing(異常後調用),Around(包含方法,前後執行自定義行爲);
連接點(join point):在應用中會有很多可以應用通知的時機,調用方法時,拋出異常時,修改字段時,這些都是連接點,切面代碼可以利用這些連接點插入添加新的行爲;
切點(pointcut):通知定義了“什麼”和“何時”,切點就定義了“何處”,通常使用明確的類和方法名稱來指定這些切點,或是利用正則表達式定義匹配的類和方法名來指定切點,定義切點就是選擇那些連接點可以織入;
切面(Aspect):切面就是完成的橫切關注點的功能,它是切點和通知的結合,最後通知和切點共同決定了是什麼、在何時和何處完成其功能;
引入(introduction):向現有類添加新的方法和屬性,無需修直接改現有類;
織入(Weaving):將切面應用到目標對象來創建新的代理對象的過程,切面在指定的連接點被織入到目標對象中:
     編譯期:AspectJ提供的織入編譯器織入切面;
     類加載器:AspectJ提供LTW類加載器織入切面;
     運行期:AOP容器會爲目標對象動態地創建一個代理對象織入切面(動態代理,可以參看《Java編程思想》);

1.2 Spring對AOP的支持
Spring提供4種AOP支持(前三種都是基於代理的AOP變體):
(1)基於代理的經典AOP;
(2)@AspectJ註解驅動的切面;
(3)純POJO切面;
(4)注入式AspectJ切面;

Spring是運行期AOP通知對象的,在調用目標bean方法時,纔會創建代理對象。
Spring支持方法連接點,因爲Spring基於動態代理所以只支持方法連接點,而AspectJ和Jboss則不同。

2. 使用切點選擇連接點
介紹完了aop和spring中的aop後,首先看一看如何編寫切點:
Spring藉助AspectJ的切點表達式來定義切面,只能使用AspectJ中基於代理的部分,否則會拋出異常。

2.1 編寫切點
定義pointcut:execution(* com.springection.springdol.Instrument.play(..));
*表示任意返回類型,play(..)表示任意參數;

execution是主要使用的AspectJ表達式,其他的可以用來限定pointcut的範圍。

2.2 bean()指示器
execution(* com.springinaction.springdol.Instrument.play()) and !bean(eddie)
可以指定切點位於ID不爲eddie的Instrument類型的Bean上。

3. 在XML中聲明切面
通過如下的標籤可以定義基於POJO的不同位置的切面聲明:
我們可以定義一個簡單的Java類
public class Audience {
     public void takeSeats() {...}
     public void turnOffCellPhones(){}
     public void applaud() {...}
     public void demandRefund() {...}
}

接着我們可以定義xml來設置切面的配置
<aop:config>
     <aop:aspect ref="audience">
          <aop:pointcut id="performance" expresstion="【aspectJ表達式】"
          <aop:before pointcut-ref='performance' method="takeSeats" />
          <aop:before pointcut-ref="performance" method="turnOffCellPhones" />
          ...
     </aop:aspect>
</aop:config>
通知的邏輯是如下組織的:
聲明環繞通知
你可能想如果前置通知和後置通知如何來共享一些必要信息呢?Audience類是單例的,因此成員變量的方式不能保證線程安全。因此環繞的方式就很適合解決這類問題。
public void watchPerformance(ProceedingJ joinpoint) {
     try {
          //前置功能
          joinpoint.proceed();
          //後置功能
     } catch(Throwable t) {
          //失敗後
     }
}
再定義一個<aop:around />就可以實現了,可以看到,切點是通過參數的方式傳入,同樣可以實現前置、後置和異常時的切面功能,也不用擔心信息共享的問題。


爲通知傳遞參數
之前幾種方式你可能會發現沒沒傳入額外的參數,當然spring提供傳參的功能:
<aop:config>
     <aop:aspect ref="nagician">
          <aop:pointcut id="thinking" expression="... and args(thougths)" />

          <aop:before pointcut-ref="thinking" method="interceptThoughts" arg-names="thoughts" />
     </aop:aspect>
</aop:config>

通過切面引入新功能
雖然java不是一個開放式的語言,但我們知道sprin是通過代理的方式實現aop,因此可以通過爲代理定義新的方法的方式來增加新的功能。
你可能會問什麼要這麼做,有什麼意義,設想需求更改有時你可能需要需要爲每一類及其子類添加新的功能,但是不同的子類可能有不同的實現,因此你不能直接基類,那麼你可能會想到增加一些抽象類,這種方式增加了類的數量,另外使用接口,這就要你手動修改很多類,設置擴展jar包中的類的時候,難道不斷的繼承嗎,顯示spring作爲開發框架就解決這類問題的。
例:
爲Performer添加Contestant接口:
public interface Contestant {
     void receiveAward();
}

方式一:
<aop:aspect>
     <aop:declare parents
          types-matching="com.springinaction.springidol.performer+"
          implement-interface="xxx.Contestant"
          default-impl="xxx.ConcreteContestant" /> 具體的實現
</aop:aspect>
方式二:
<aop:aspect>
     <aop:declare parents
          types-matching="com.springinaction.springidol.performer+"
          implement-interface="xxx.Contestant"
          default-refs="contestantDelegate" /> 這裏是Bean的ID
</aop:aspect>

這樣就可以不修改原有類的代碼了,減少了入侵式編程,實現了“開發-封閉”;


4. 註解切面
使用註解可以代替XML定義切面,直接在POJO上添加註解可以實現切面的定義:
@Aspect
public class Audiance {
        @Pointcut("execution(* com.yjh.example.Performer.performance())" )
        public void performance() {
       }
       
        @Before( "performance()")
        public void takeSeats() {
              System. out.println("[before]take seats" );
       }
       
        @Before( "performance()")
        public void turnOffLights() {
              System. out.println("[before]turn off lights" );
       }
}
這樣可以通過註解方便的定義切面,不需要再XML中配置aop了。我們已經知道Spring的aop是基於動態代理的,因此需要在使用時爲Bean創建代理。
Spring通過創建一個AnnotationAwareAspecJProxyCreator類的Bean,因此要將它註冊爲Bean,通過它來創建自動代理類。爲了簡化註冊的過程,你只要在配置xml中添加,<aop:aspectj-autoproxy />Spring就知道註冊該Bean了。

使用AspectJ註解的優缺點
優點:可以減少XML的量,方便使用;
缺點:對於無法修改源碼的類不能應用切面,還得用<aop:aspect>

4.1 註解環繞通知
使用@around可以實現環繞通知:
@Around("performance()" )
public void watchPerformance(ProceedingJoinPoint proccedJoinPoint) {
               //定義環繞通知,比如統計處理過程的耗時
               try {
                     System. out .println("start proceeding..." );
                      long startTime = System.currentTimeMillis ();
                     
                      proccedJoinPoint .proceed();
                     
                      long endTime = System.currentTimeMillis ();
                     
                     System. out .println("proceed used: " + (endTime startTime ) + "ms in total." );
              } catch (Throwable e ) {
                     System. out .println("[ERROR]There is a error caughted in proceeding.");
              }
       }
這個例子展示了計算程序處理過程時間統計的功能,如果不用切面,這樣的功能寫到業務邏輯中,再反覆刪掉想想都是一件很麻煩的事。


4.2 註解傳遞參數給通知
@Aspect
public class MindReader {
        private String thoughts ;
       
        @Pointcut("execution(* com.yjh.example.Thinker.thinkAboutSomething(String)) && args(thoughts)")
        public void thinkAboutSomething(String toughts) {
        }
       
        @Before( "thinkAboutSomething(thoughts)" )
        public void interceptThought(String thoughts) {
               this.thoughts = thoughts ;
        }

        public String getThoughts() {
               return thoughts ;
       }
       
}

4.3 標註引入
之前已經提到過,引入是在不修改源碼的情況下給Bean添加新的行爲,Spring已經提供了<aop:declared-parents>來實現xml引入。
通過@DeclareParents註解我們也可以實現爲Bean添加新行爲的過程。
@Aspect
public class ContestantIntroducer {
        @DeclareParents(
                     value= "com.yjh.example.Performer+",
                     defaultImpl=ConcreteContestant. class
                     )
        public static Contestant contestant;
}

註冊ContestantIntroducer爲Bean,<bean class="xxx.ContestantIntroducer" />

注意:使用@DeclareParents並沒有和declare-ref屬性相對應的註解。因此如果要委託的對象是Spring Bean的話,還是要使用<aop:declare-parents>

5. 注入AspectJ切面
Spring只能提供基於方法的動態代理的AOP,如果你需要更強大的切面功能,比如在創建通知時應用通知,Spring就無能爲力了。而你可以使用AspectJ的其他切面功能來完成。

在使用AspectJ切面的時候,Spring可以做什麼呢?切面本身也是一個類,它可能也有一些依賴關係,因此使用Spring的依賴注入來解決切面的屬性注入問題。

public aspect JudgeAspect {
     public JudgeAspect() {}

     pointcut performance() : execution(* perform(..));

     after() returning() : performance() {...}
     
     //injected
         private CriticismEngine criticismEngine;
         public void setCriticismEngine(CriticismEngine criticismEngine) {     
               this.criticismEngine = criticismEngine;
          }
}
這就是一個AspectJ切面,接下來可以使用spring來注入依賴:
<bean class="com.springinaction.springdol.JudgeAspect"
     factory-method="aspectOf">
     <property name="critismEngine" ref="criticismEngine" />
</bean>

注意這裏使用了factory-method,AspectJ切面是AspectJ在運行期創建的,Spring無法創建,因此應該使用AspectJ切面提供的aspectOf()靜態方法作爲工廠方法。
發佈了40 篇原創文章 · 獲贊 10 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章