Spring AOP的底層實現技術

AOP概述java

軟件的編程語言最終的目的就是用更天然更靈活的方式模擬世界,從原始機器語言到過程語言再到面向對象的語言,咱們看到編程語言在一步步用更天然、更強大的方式描述軟件。AOP是軟件開發思想的一個飛躍,AOP的引入將有效彌補OOP的不足,OOP和AOP分別從縱向和橫向對軟件進行抽象,有效地消除重複性的代碼,使代碼以更優雅的更有效的方式進行邏輯表達。編程

AOP有三種植入切面的方法:其一是編譯期織入,這要求使用特殊的Java編譯器,AspectJ是其中的表明者;其二是類裝載期織入,而這要求使用特殊的類裝載器,AspectJ和AspectWerkz是其中的表明者;其三爲動態代理織入,在運行期爲目標類添加加強生成子類的方式,Spring AOP採用動態代理織入切面。安全

Spring AOP使用了兩種代理機制,一種是基於JDK的動態代理,另外一種是基於CGLib的動態代理,之因此須要兩種代理機制,很大程度上是由於JDK自己只提供基於接口的代理,不支持類的代理。編程語言

基於JDK的代理和基於CGLib的代理是Spring AOP的核心實現技術,認識這兩代理技術,有助於探究Spring AOP的實現機理。只要你願意,你甚至能夠拋開Spring,提供本身的AOP實現。函數

帶有橫切邏輯的實例
   
首先,咱們來看一個沒法經過OOP進行抽象的重複代碼邏輯,它們就是AOP改造的主要對象。下面,咱們經過一個業務方法性能監視的實例瞭解橫切邏輯。業務方法性能監視,在每個業務方法調用以前開始監視,業務方法結束後結束監視並給出性能報告:性能

代碼清單 2 ForumService:包含性能監視橫切代碼測試

 

package com.baobaotao.proxy;
public class ForumServiceImpl implements ForumService ...{this

 public void removeTopic(int topicId) ...{
          //開始性能監視
  PerformanceMonitor.begin("com.baobaotao.proxy.ForumServiceImpl.removeTopic");
  System.out.println("模擬刪除Topic記錄:"+topicId);
  try ...{
   Thread.currentThread().sleep(20);
  } catch (Exception e) ...{
   throw new RuntimeException(e);
  }
  //結束監視、並給出性能報告信息
  PerformanceMonitor.end();
 }spa

 public void removeForum(int forumId) ...{
  //開始性能監視
PerformanceMonitor.begin("com.baobaotao.proxy.ForumServiceImpl.removeForum");
  System.out.println("模擬刪除Forum記錄:"+forumId);
  try ...{
   Thread.currentThread().sleep(40);
  } catch (Exception e) ...{
   throw new RuntimeException(e);
  }
         //結束監視、並給出性能報告信息
  PerformanceMonitor.end();
 }
}.net

代碼清單 2中粗體表示的代碼就是具備橫切特徵的代碼,須要進行性能監視的每一個業務方法的先後都須要添加相似的性能監視語句。
   
咱們保證明例的完整性,咱們提供了一個很是簡單的性能監視實現類,如所示代碼清單 3所示:

代碼清單 3 PerformanceMonitor

 

package com.baobaotao.proxy;

public class PerformanceMonitor {
  //經過一個ThreadLocal保存線程相關的性能監視信息
 private static ThreadLocal<MethodPerformace> performaceRecord =
new ThreadLocal<MethodPerformace>();
 public static void begin(String method) {
  System.out.println("begin monitor...");
  MethodPerformace mp = new MethodPerformace(method);
  performaceRecord.set(mp);
 }
 public static void end() {
  System.out.println("end monitor...");
  MethodPerformace mp = performaceRecord.get();
  mp.printPerformace(); //打印出業務方法性能監視的信息
 }
}

PerformanceMonitor提供了兩個方法,begin(String method)方法開始對某個業務類方法的監視,method爲業務方法的簽名,而end()方法結束對業務方法的監視,並給出性能監視的信息。因爲每個業務方法都必須單獨記錄性能監視數據,因此咱們使用了ThreadLocal,ThreadLocal是削除非線程安全狀態的不二法寶。ThreadLocal中的元素爲方法性能記錄對象MethodPerformace,它的代碼以下所示:

代碼清單 4 MethodPerformace

 

package com.baobaotao.proxy;
public class MethodPerformace {
 private long begin;
 private long end;
 private String serviceMethod;
    public MethodPerformace(String serviceMethod){
       this.serviceMethod = serviceMethod;
       this.begin = System.currentTimeMillis();//記錄方法調用開始時的系統時間
    }
    public void printPerformace(){
        //如下兩行程序獲得方法調用後的系統時間,並計算出方法執行花費時間
        end = System.currentTimeMillis();
        long elapse = end - begin;
        //報告業務方法執行時間
        System.out.println(serviceMethod+"花費"+elapse+"毫秒。");
    }
}

#p#

經過下面代碼測試這個擁有方法性能監視能力的業務方法:

 

package com.baobaotao.proxy;
public class TestForumService {
 public static void main(String[] args) {
        ForumService forumService = new ForumServiceImpl();
        forumService .removeForum(10);
    forumService .removeTopic(1012);
 }
}

咱們獲得如下的輸出信息:

 

begin monitor...
模擬刪除Forum記錄:10
end monitor...
com.baobaotao.proxy.ForumServiceImpl.removeForum花費47毫秒。

begin monitor...
模擬刪除Topic記錄:1012
end monitor...
com.baobaotao.proxy.ForumServiceImpl.removeTopic花費16毫秒。

如實例所示,要對業務類進行性能監視,就必須在每一個業務類方法的先後兩處添加上重複性的開啓性能監視和結束性能監視的代碼。這些非業務邏輯的性能監視代碼破壞了做爲業務類ForumServiceImpl的純粹性。下面,咱們分別JDK動態代理和CGLib動態代理技術,將業務方法中開啓和結束性能監視的這些橫切代碼從業務類中完成移除。

JDK動態代理
   
在JDK 1.3之後提供了動態代理的技術,容許開發者在運行期建立接口的代理實例。在Sun剛推出動態代理時,還很難想象它有多大的實際用途,如今咱們終於發現動態代理是實現AOP的絕好底層技術。
   
JDK的動態代理主要涉及到java.lang.reflect包中的兩個類:Proxy和InvocationHandler。其中InvocationHandler是一個接口,能夠經過實現該接口定義橫切邏輯,在並經過反射機制調用目標類的代碼,動態將橫切邏輯和業務邏輯編織在一塊兒。
  
而Proxy爲InvocationHandler實現類動態建立一個符合某一接口的代理實例。這樣講必定很抽象,咱們立刻着手動用Proxy和InvocationHandler這兩個魔法戒對上一節中的性能監視代碼進行AOP式的改造。
   
首先,咱們從業務類ForumServiceImpl 中刪除性能監視的橫切代碼,使ForumServiceImpl只負責具體的業務邏輯,如所示:

代碼清單 5 ForumServiceImpl:移除性能監視橫切代碼

 

package com.baobaotao.proxy;
public class ForumServiceImpl implements ForumService {
 public void removeTopic(int topicId) {
         ①
  System.out.println("模擬刪除Topic記錄:"+topicId);
  try {
   Thread.currentThread().sleep(20);
  } catch (Exception e) {
   throw new RuntimeException(e);
  }
   ②
 }
 public void removeForum(int forumId) {
         ①
  System.out.println("模擬刪除Forum記錄:"+forumId);
  try {
   Thread.currentThread().sleep(40);
  } catch (Exception e) {
   throw new RuntimeException(e);
  }
         ②
 }
}

在代碼清單 5中的①和②處,原來的性能監視代碼被移除了,咱們只保留了真正的業務邏輯。
   
從業務類中移除的橫切代碼固然還得找到一個寄居之所,InvocationHandler就是橫切代碼的家園樂土,咱們將性能監視的代碼安置在PerformaceHandler中,如代碼清單 6所示:

代碼清單 6 PerformaceHandler

 

package com.baobaotao.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class PerformaceHandler implements InvocationHandler {
    private Object target;
 public PerformaceHandler(Object target){//①target爲目標的業務類
  this.target = target;
 }
 public Object invoke(Object proxy, Method method, Object[] args)
   throws Throwable {
  PerformanceMonitor.begin(target.getClass().getName()+"."+ method.getName());
  Object obj = method.invoke(target, args);//②經過反射方法調用目標業務類的業務方法
  PerformanceMonitor.end();
  return obj;
 }
}

粗體部分的代碼爲性能監視的橫切代碼,咱們發現,橫切代碼只出現一次,而不是原來那樣星灑各處。你們注意②處的method.invoke(),該語句經過反射的機制調用目標對象的方法,這樣InvocationHandler的invoke(Object proxy, Method method, Object[] args)方法就將橫切代碼和目標業務類代碼編織到一塊兒了,因此咱們能夠將InvocationHandler當作是業務邏輯和橫切邏輯的編織器。下面,咱們對這段代碼作進一步的說明。

#p#

首先,咱們實現InvocationHandler接口,該接口定義了一個 invoke(Object proxy, Method method, Object[] args)的方法,proxy是代理實例,通常不會用到;method是代理實例上的方法,經過它能夠發起對目標類的反射調用;args是經過代理類傳入的方法參數,在反射調用時使用。
   
此外,咱們在構造函數裏經過target傳入真實的目標對象,如①處所示,在接口方法invoke(Object proxy, Method method, Object[] args)裏,將目標類實例傳給method.invoke()方法,經過反射調用目標類方法,如②所示。
   
下面,咱們經過Proxy結合PerformaceHandler建立ForumService接口的代理實例,如代碼清單 7所示:

代碼清單 7 TestForumService:建立代理實例

 

package com.baobaotao.proxy;
import java.lang.reflect.Proxy;
public class TestForumService {
 public static void main(String[] args) {
  ForumService target = new ForumServiceImpl();//①目標業務類
//② 將目標業務類和橫切代碼編織到一塊兒
  PerformaceHandler handler = new PerformaceHandler(target);
         //③爲編織了目標業務類邏輯和性能監視橫切邏輯的handler建立代理類
  ForumService proxy = (ForumService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
    target.getClass().getInterfaces(),
 handler);
         //④ 操做代理實例
  proxy.removeForum(10);
  proxy.removeTopic(1012);
 }
}

上面的代碼完成了業務類代碼和橫切代碼編織和接口代理實例生成的工做,其中在②處,咱們將ForumService實例編織爲一個包含性能監視邏輯的PerformaceHandler實例,而後在③處,經過Proxy的靜態方法newProxyInstance()爲融合了業務類邏輯和性能監視邏輯的handler建立一個ForumService接口的代理實例,該方法的第一個入參爲類加載器,第二個入參爲建立的代理實例所要實現的一組接口,第三個參數是整合了業務邏輯和橫切邏輯的編織器對象。

按照③處的設置方式,這個代理實例就實現了目標業務類的全部接口,也即ForumServiceImpl的ForumService接口。這樣,咱們就能夠按照調用ForumService接口的實例相同的方式調用代理實例,如④所示。運行以上的代碼,輸出如下的信息:

 

begin monitor...
模擬刪除Forum記錄:10
end monitor...
com.baobaotao.proxy.ForumServiceImpl.removeForum花費47毫秒。

begin monitor...
模擬刪除Topic記錄:1012
end monitor...
com.baobaotao.proxy.ForumServiceImpl.removeTopic花費26毫秒。

咱們發現,程序的運行效果和直接在業務類中編寫性能監視邏輯的效果一致,可是在這裏,原來分散的橫切邏輯代碼已經被咱們抽取到PerformaceHandler中。當其它業務類(如UserService、SystemService等)的業務方法也須要使用性能監視時,咱們只要按照以上的方式,分別爲它們建立代理對象就能夠了。下面,咱們用時序圖描述調用關係,進一步代理實例的本質,如圖1所示:


 
圖1:代理實例的時序圖
   
咱們在上圖中特別使用虛線陰影的方式對經過代理器建立的ForumService實例進行凸顯,該實例內部利用PerformaceHandler整合橫切邏輯和業務邏輯。調用者調用代理對象的的removeForum()和removeTopic()方法時,上圖的內部調用時序清晰地告訴了咱們實際上所發生的一切。

CGLib動態代理
  
使用JDK建立代理有一個限制,即它只能爲接口建立代理,這一點咱們從Proxy的接口方法newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)就看得很清楚,第三個入參interfaces就是爲代理實例指定的實現接口。雖然,面向接口的編程被不少頗有影響力人(包括Rod Johnson)的推崇,但在實際開發中,開發者也遇到了不少困惑:難道對一個簡單業務表的操做真的須要建立5個類(領域對象類、Dao接口,Dao實現類,Service接口和Service實現類)嗎?對於這一問題,咱們仍是留待你們進一步討論。如今的問題是:對於沒有經過接口定義業務方法的類,如何動態建立代理實例呢?JDK的代理技術顯然已經黔驢技窮,CGLib做爲一個替代者,填補了這個空缺。你能夠從http://cglib.sourceforge.net/獲取CGLib的類包,也能夠直接從Spring的關聯類庫lib/cglib中獲取類包。

#p#
  
CGLib採用很是底層的字節碼技術,能夠爲一個類建立子類,並在子類中採用方法攔截的技術攔截全部父類方法的調用,並在攔截方法相應地織入橫切邏輯。下面,咱們採用CGLib技術,編寫一個能夠爲任何類建立織入性能監視橫切邏輯的代理對象的代理器,如代碼清單 8所示:

代碼清單 8 CglibProxy

 

package com.baobaotao.proxy;
import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

public class CglibProxy implements MethodInterceptor {
 private Enhancer enhancer = new Enhancer();
 public Object getProxy(Class clazz) {
  enhancer.setSuperclass(clazz); ① 設置須要建立子類的類
  enhancer.setCallback(this);
  return enhancer.create(); ②經過字節碼技術動態建立子類實例
 }
 public Object intercept(Object obj, Method method, Object[] args,
   MethodProxy proxy) throws Throwable {
  PerformanceMonitor.begin(obj.getClass().getName()+"."+method.getName());
  Object result=proxy.invokeSuper(obj, args); ③ 經過代理類調用父類中的方法
  PerformanceMonitor.end();
  return result;
 }
}

在上面代碼中,你能夠經過getProxy(Class clazz)爲一個類建立動態代理對象,該代理對象是指定類clazz的子類。在這個代理對象中,咱們織入性能監視的橫切邏輯(粗體部分)。intercept(Object obj, Method method, Object[] args,MethodProxy proxy)是CGLib定義的Inerceptor接口的方法,obj表示父類的實例,method爲父類方法的反射對象,args爲方法的動態入參,而proxy爲代理類實例。
   
下面,咱們經過CglibProxy爲ForumServiceImpl類建立代理對象,並測試代理對象的方法,如代碼清單 9所示:

代碼清單 9 TestForumService:測試Cglib建立的代理類

 

package com.baobaotao.proxy;
import java.lang.reflect.Proxy;
public class TestForumService {
 public static void main(String[] args) {
   CglibProxy proxy = new CglibProxy();
   ForumServiceImpl forumService = //① 經過動態生成子類的方式建立代理對象
(ForumServiceImpl )proxy.getProxy(ForumServiceImpl.class);
   forumService.removeForum(10);
   forumService.removeTopic(1023);
 }
}

在①中,咱們經過CglibProxy爲ForumServiceImpl動態建立了一個織入性能監視邏輯的代理對象,並調用了代理對象的業務方法。運行上面的代碼,輸入如下的信息:

 

begin monitor...
模擬刪除Forum記錄:10
end monitor...
com.baobaotao.proxy.ForumServiceImpl$$EnhancerByCGLIB$$2a9199c0.removeForum花費47毫秒。
begin monitor...
模擬刪除Topic記錄:1023
end monitor...
com.baobaotao.proxy.ForumServiceImpl$$EnhancerByCGLIB$$2a9199c0.removeTopic花費16毫秒。

觀察以上的輸出,除了發現兩個業務方法中都織入了性能監控的邏輯外,咱們還發現代理類的名字是com.baobaotao.proxy.ForumServiceImpl$$EnhancerByCGLIB$$2a9199c0,這個特殊的類就是CGLib爲ForumServiceImpl所動態建立的子類。

小結
     
Spring AOP在底層就是利用JDK動態代理或CGLib動態代理技術爲目標Bean織入橫切邏輯。在這裏,咱們對以上兩節動態建立代理對象作一個小結。

在PerformaceHandler和CglibProxy中,有三點值得注意的地方是:第一,目標類的全部方法都被添加了性能監視橫切的代碼,而有時,這並非咱們所指望的,咱們可能只但願對業務類中的某些方法織入橫切代碼;第二,咱們手工指定了織入橫切代碼的織入點,即在目標類業務方法的開始和結束前調用;第三,咱們手工編寫橫切代碼。以上三個問題,在AOP中佔用重要的地位,由於Spring AOP的主要工做就是圍繞以上三點展開:Spring AOP經過Pointcut(切點)指定在哪些類的哪些方法上施加橫切邏輯,經過Advice(加強)描述橫切邏輯和方法的具體織入點(方法前、方法後、方法的兩端等),此外,Spring還經過Advisor(切面)組合Pointcut和Advice。有了Advisor的信息,Spring就能夠利用JDK或CGLib的動態代理技術爲目標Bean建立織入切面的代理對象了。

JDK動態代理所建立的代理對象,在JDK 1.3下,性能強差人意。雖然在高版本的JDK中,動態代理對象的性能獲得了很大的提升,可是有研究代表,CGLib所建立的動態代理對象的性能依舊比JDK的所建立的代理對象的性能高很多(大概10倍)。而CGLib在建立代理對象時性能卻比JDK動態代理慢不少(大概8倍),因此對於singleton的代理對象或者具備實例池的代理,由於不須要頻繁建立代理對象,因此比較適合用CGLib動態代理技術,反之適合用JDK動態代理技術。此外,因爲CGLib採用生成子類的技術建立代理對象,因此不能對目標類中的final方法進行代理。

相關文章
相關標籤/搜索