Spring框架:AOP詳解

AOP的中文名稱叫做面向切面編程。這個名字非常形象,因爲你真的可以把一個系統像麪包一樣切開,並直接在麪包上增加修飾。切面可大可小,大到整個系統,小到某一個方法。


AOP有什麼用呢?舉個例子,每個組件中都可能含有安全、事務、數據庫等方面的邏輯,AOP就是把每個組件中的安全作爲一個方面進行集中處理,事務作爲一個方面,數據庫作爲一個方面等等。這樣才能做到高內聚、低耦合。AOP中有三個重要的術語:通知、切點、連接點。他們之間的關係如下圖。



AOP實現的原理是修改目標類中的代碼。至於怎麼修改,有多種方式:編譯時、類加載時、運行時。編譯時修改需要特殊的編譯器,類加載時修改需要特殊的類加載器。運行時,就是應用在運行的時候AOP框架會爲目標對象生成一個動態代理類。Spring AOP採用的就是運行時代理。Spring容器通過ObjectFactory創建所有的Bean實例,並且實例之外增加一層動態代理。SpringAOP具體實現主要涉及到反射機制中的Proxy.newProxyInstance和InvocationHandler,在後續的JVM文章中還會詳細介紹。


除了Spring AOP目前流行的AOP框架還有AspectJ、JBoss AOP。


下面是AOP的Hello World程序。目標是,在某個類的createApple方法調用之前做一些事情,但是又不能直接改變這個方法的代碼。下面這段代碼就是在createApple方法執行之前,額外執行beforeCreateApple,有點類似於Hook。代碼如下:

<bean id="appleListener" class="xxx"/>

<aop:aspect ref="appleListener">
  <aop:pointcut id="apple" expression="execution(* *.createApple(..))" />
  
  <aop:before pointcut-ref="apple" method="beforeCreateApple" />
</aop:aspect>

上面這段代碼的意思是,當程序中任何一個類的createApple方法被調用之前,都先調用appleListener中的beforeCreateApple方法。


切點表達式語言。上面例子中的execution(* *.createApple(..))就是表達式語言,第一個星號表示返回值的類型,第二個星號表示被調用的類名。支持如下語法:

  • args() 將參數傳遞給切面
  • @args() 匹配註解才傳遞參數
  • execution() 匹配具體的方法
  • this() 匹配當前bean
  • target() 匹配目標對象
  • @target() 匹配目標對象的註解
  • within() 匹配實例的類型
  • @within() 匹配實例的註解
  • @annotation() 匹配註解
  • bean() 匹配bean id

下面舉例說明切點表達式語言。

// 切點爲執行com.example.Apple.eat方法,返回值任意,參數任意。
execution(* com.example.Apple.eat(..))

// within表示只匹配com.example.*下的任意方法。用了and連接符號。
execution(* com.example.Apple.eat(..) and within(com.example.*))

// bean表示匹配相應的bean
execution(* com.example.Apple.eat(..) and bean(apple))


下面的例子演示了切點的各種修飾方式。

<aop:config>
  <!--定義切面,test是事先定義好的一個bean-->
  <aop:aspect ref="test">
    <!--定義切點-->
    <aop:pointcup id="apple-eat" expression="execution(* com.example.Apple.eat(..))"/>
    
    <!--在切點之前調用test.beforeEat-->
    <aop:before pointcut-ref="apple-eat" method="beforeEat"/>
    
    <!--在切點執行成功之後調用-->
    <aop:after-return pointcut-ref="apple-eat" method="eatSuccess"/>
    
    <!--在切點執行失敗之後調用-->
    <aop:after-throwing pointcut-ref="apple-eat" method="eatFailed"/>
    
    <!--在切點之後調用,不管成功失敗-->
    <aop:after pointcut-ref="apple-eat" method="afterEat"/>
    
    <!--環繞通知,下面有詳細說明-->
    <aop:around pointcut-ref="apple-eat" method="eatApple"/>
    
    <!--動態增加接口,下面有詳細說明-->
    <aop:declare-parents types-matching="com.example.Apple+" implement-interface="com.example.Fruit" default-impl="com.example.FruitImpl"/>
  </aop:aspect>
</aop:config>


現代化的Spring支持註解方式的切面。下面請看例子。

// 定義切面
@Aspect
public class Test {
    // 定義切點。方法中不需要寫任何代碼。
    @Pointcut("execution(* com.example.Apple.eat(..))")
    public void appleEat() { }
    
    // 切面之前
    @Before("appleEat()")
    public void beforeEat() { }
    
    // 切面執行成功之後
    @AfterReturning("appleEat()")
    public void eatSuccess() { }
    
    // 切面執行失敗之後
    @AfterThrowing("appleEat()")
    public void eatFailed() { }
    
    // 切面之後,不管成功失敗
    @After("appleEat()")
    public void afterEat() { }
    
    // 環繞切面,下面有詳細說明
    @Around("appleEat")
    public void eatApple(ProceedingJoinPoint joinpoint) { }
    
    // 定義傳遞參數的切點
    @Pointcut("execution(* com.example.Apple.eat(..)) and args(size)")
    public void appleEat2() { }
    
    // 接收切點的參數
    @Before("appleEat2")
    public void beforeEat2(int size) {
        // 能夠得到切點的size參數
    }
}


環繞通知。它的目的是爲了解決切點前後無法通信的問題。本質是四種切點的結合體。比如我想記錄一個切點的執行時間,就需要用到環繞通知。下面是環繞通知的代碼。

public void eatApple(ProceedingJoinPoint joinPoint) {
    // 在切點之前
    System.out.println("before pointcut");
    
    // 手動執行切點
    joinPoint.proceed();
    
    // 在切點之後
    System.out.println("after pointcut");
}


Introduction引入,也就是動態增加新接口。它的作用就是在程序運行的過程中動態地爲一個實例增加接口。請看下面的例子。

// Introduction引入。
@DeclareParents(value="com.example.Phone+", defaultImpl="com.example.AppleWatchImpl")
public static AppleWatch appleWatch;

上面的例子中給appleWatch字段增加了一個註解,意思是讓appleWatch字段可以轉換成Phone類型,原本appleWatch是不能轉換成Phone的。

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