Spring AOP高級——源碼實現(1)動態代理技術

来源:http://www.cnblogs.com/yulinfeng/archive/2017/11/09/7811965.html
-Advertisement-
Play Games

在正式進入Spring AOP的源碼實現前,我們需要準備一定的基礎也就是面向切麵編程的核心——動態代理。 動態代理實際上也是一種結構型的設計模式,JDK中已經為我們準備好了這種設計模式,不過這種JDK為我們提供的動態代理有2個缺點: 鑒於以上2個缺點,於是就出現了第二種動態代理技術——CGLIB(C ...


  在正式進入Spring AOP的源碼實現前,我們需要準備一定的基礎也就是面向切麵編程的核心——動態代理。 動態代理實際上也是一種結構型的設計模式,JDK中已經為我們準備好了這種設計模式,不過這種JDK為我們提供的動態代理有2個缺點:

  1. 只能代理實現了介面的目標對象;
  2. 基於反射,效率低

  鑒於以上2個缺點,於是就出現了第二種動態代理技術——CGLIB(Code Generation Library)。這種代理技術一是不需要目標對象實現介面(這大大擴展了使用範圍),二是它是基於位元組碼實現(這比反射效率高)。當然它並不是完全沒有缺點,因為它不能代理final方法(因為它的動態代理實際是生成目標對象的子類)。

  Spring AOP中生成代理對象時既可以使用JDK的動態代理技術,也可以使用CGLIB的動態代理技術,本章首先對這兩者動態代理技術做簡要瞭解,便於後續源碼的理解。

JDK動態代理技術

  JDK動態代理技術首先要求我們目標對象需要實現一個介面:

1 package proxy;
2 
3 /**
4  * Created by Kevin on 2017/11/8.
5  */
6 public interface Subject {
7     void sayHello();
8 }

  接下來就是我們需要代理的真實對象,即目標對象:

 1 package proxy;
 2 
 3 /**
 4  * 目標對象,即需要被代理的對象
 5  * Created by Kevin on 2017/11/8.
 6  */
 7 public class RealSubject implements Subject{
 8     public void sayHello() {
 9         System.out.println("hello world");
10     }
11 }

  這是一個真實的對象,我們希望在不更改原有代碼邏輯的基礎上增強該類的sayHello方法,利用JDK動態代理技術需要我們實現InvocationHandler介面中的invoke方法:

 1 package proxy;
 2 
 3 import java.lang.reflect.InvocationHandler;
 4 import java.lang.reflect.Method;
 5 
 6 public class ProxySubject implements InvocationHandler {
 7     private Object target;
 8 
 9     public ProxySubject(Object target) {
10         this.target = target;
11     }
12 
13     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
14         System.out.println("調用前");
15         Object object = method.invoke(target, args);
16         System.out.println("調用後");
17         return object;
18     }
19 }

  第15行,在invoke方法中可以看到,在調用目標對象的方法前後我們對方法進行了增加,這其實就是AOP中Before和After通知的奧義所在。

  加入測試代碼:

 1 package proxy;
 2 
 3 import java.lang.reflect.Proxy;
 4 
 5 /**
 6  * Created by Kevin on 2017/11/8.
 7  */
 8 public class Test {
 9     public static void main(String[] args) {
10         Subject subject = (Subject) Proxy.newProxyInstance(RealSubject.class.getClassLoader(), RealSubject.class.getInterfaces(), new ProxySubject(new RealSubject()));
11         subject.sayHello();
12 
13         //查看subject對象的類型
14         System.out.println(subject.getClass().getName());
15     }
16 }

  執行結果:

  可以看到和AOP幾乎一樣,前面提到過,動態代理就是AOP的核心。同時我們可以看到被代理的類的類型是:com.sun.proxy.$Proxy0。等會深入JDK源碼時我們將會看到為什麼。

  回到上面的例子,我們通過Proxy. newProxyInstance生成了一個代理類,顯然這個類是在Run-Time(運行時)生成的,也就是說,JDK動態代理中代理類的生成來自於Java反射機制的支撐。

  上面例子中我們將實現InvocationHandler的類取名為“ProxySubject”,這其實是不准確的,我們看到了最後代理類的類型並不是ProxySubject,這個類實際上是處理需要增強的方法,也就是在invoke中的實現邏輯,最後並不是生成這個類型的代理類,這也不是生成的代理類,所以取名這個是不准確的。

  首先從Proxy.newProxyInstance開始,來研究JDK是如何生成代理類的。

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)

  該方法有3個參數,瞭解JVM類載入的可能知道確定為同一個類需要有2個條件:

  • 類的全限定名稱相同

  • 載入類的類載入器相同

  要想生成目標對象的代理首先就要確保其類載入器相同,所以需要將目標對象的類載入器作為參數傳遞;其次JDK動態代理技術需要代理類和目標對象都繼承自同一介面,所以需要將目標對象的介面作為參數傳遞;最後,傳遞InvocationHandler,這是主角,因為我們對目標對象的增強邏輯在這個實現類中,傳遞該對象使得代理類能夠對其進行調用。

  在Proxy.newProxyInstance方法中創建代理類的過程主要有3步:

1.檢查

 1 public static Object newProxyInstance(ClassLoader loader,
 2                                       Class<?>[] interfaces,
 3                                       InvocationHandler h)
 4     throws IllegalArgumentException
 5 {
 6     Objects.requireNonNull(h);    //1.1檢查參數是否為空
 7 
 8     final Class<?>[] intfs = interfaces.clone();
 9     final SecurityManager sm = System.getSecurityManager();    //獲取安全管理器,安全管理器用於對外部資源的訪問控制
10     if (sm != null) {
11         checkProxyAccess(Reflection.getCallerClass(), loader, intfs);    //1.2檢查是否有訪問許可權
12     }

  在上面源碼中有一個獲取安全管理器以及檢查是否具有訪問許可權的過程。安全管理器可能在實際中不太常用,它是為了程式在某些敏感資源的訪問上做的許可權控制,也就是起到保護程式的作用。在這裡暫時不用仔細去探究,只需要大概瞭解即可。這裡做的許可權檢查實際上是對ClassLoader的檢查,例如:有的程式不允許你對類進行代理,此時加入安全管理器即可防止你對該類的代理。

2.獲取代理類型

Class<?> cl = getProxyClass0(loader, intfs);    //獲取代理類類型

  這句話通過目標對象的類載入器,以及它所繼承的介面,即可獲取代理類的類型。

 1 /**
 2  * Generate a proxy class.  Must call the checkProxyAccess method
 3  * to perform permission checks before calling this.
 4 *從註釋中可以看到,這個方法用於生成代理類,在調用此方法前必須要確保
 5 *已經做過許可權檢查。
 6  */
 7 private static Class<?> getProxyClass0(ClassLoader loader,
 8                                        Class<?>... interfaces) {
 9     if (interfaces.length > 65535) {        //一個類最多實現65535個介面
10         throw new IllegalArgumentException("interface limit exceeded");
11     }
12     return proxyClassCache.get(loader, interfaces);    //先從緩存中獲取代理類,如果不存在則通過ProxyClassFactory創建,這其中會涉及到比較複雜的代理緩存機制,本篇主要講動態代理過程的源碼實現,對於動態代理的緩存機制在以後再研究。
13 }

  上面的方法返回的是com.sun.proxy.$Proxy0代理類型,下麵就會通過這個代理類型生成代理類。

 3.生成代理類

 1 try {
 2     if (sm != null) {
 3         checkNewProxyPermission(Reflection.getCallerClass(), cl);    //這裡還需要做一次檢查,檢查的是生成的代理類型做許可權檢查,當然前提還是通過System.setSecurityManager設置安全管理類
 4     }
 5 
 6     final Constructor<?> cons = cl.getConstructor(constructorParams);    //通過反射獲取構造器,cl是代理類型其構造器的參數類型為InvocationHandler,所以參數傳入InvocationHandler
 7     final InvocationHandler ih = h;
 8     if (!Modifier.isPublic(cl.getModifiers())) {   //判斷目標對象的構造器修飾符是我否為public,如果不是則不能生成代理類,返回null
 9         AccessController.doPrivileged(new PrivilegedAction<Void>() {
10             public Void run() {
11                 cons.setAccessible(true);
12                 return null;
13             }
14         });
15     }
16     return cons.newInstance(new Object[]{h});    //最後生成代理類
17 } catch (IllegalAccessException|InstantiationException e) {
18     throw new InternalError(e.toString(), e);
19 } catch (InvocationTargetException e) {
20     Throwable t = e.getCause();
21     if (t instanceof RuntimeException) {
22         throw (RuntimeException) t;
23     } else {
24         throw new InternalError(t.toString(), t);
25     }
26 } catch (NoSuchMethodException e) {
27     throw new InternalError(e.toString(), e);
28 }

  以上就是通過JDK動態代理生成代理類的過程,其中會涉及到動態代理的緩存機制,以及代理類位元組碼的生成過程,由於比較複雜,在本文暫不做介紹。由此可以清楚的看到,JDK的動態代理底層是通過Java反射機制實現的,並且需要目標對象繼承自一個介面才能生成它的代理類。

  接下來探討另一種動態代理技術——CGLib。

CGLib動態代理技術

  通過CGLib來創建一個代理需要引入jar包,其pom.xml依賴如下所示:

<dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib</artifactId>
  <version>3.2.4</version>
</dependency>
View Code

  前面提到了CGLib動態代理技術不需要目標對象實現自一個介面:

 1 package cglibproxy;
 2 
 3 /**
 4  * 目標對象(需要被代理的類)
 5  * Created by Kevin on 2017/11/6.
 6  */
 7 public class RealSubject {
 8     public void sayHello() {
 9         System.out.println("hello");
10     }
11 }

  下麵我們就使用CGLib代理這個類:

 1 package cglibproxy;
 2 
 3 import net.sf.cglib.proxy.Enhancer;
 4 import net.sf.cglib.proxy.MethodInterceptor;
 5 import net.sf.cglib.proxy.MethodProxy;
 6 
 7 import java.lang.reflect.Method;
 8 
 9 /**
10  * 代理類
11  * Created by Kevin on 2017/11/6.
12  */
13 public class ProxySubject implements MethodInterceptor {
14     private Enhancer enhancer = new Enhancer();
15 
16     public Object getProxy(Class clazz) {
17         enhancer.setSuperclass(clazz);
18         enhancer.setCallback(this);
19         return enhancer.create();    //用於創建無參的目標對象代理類,對於有參構造器則調用Enhancer.create(Class[] argumentTypes, Object[] arguments),第一個參數表示參數類型,第二個參數表示參數的值。
20     }
21 
22     @Override
23     public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
24         System.out.println("調用前");
25         Object result = methodProxy.invokeSuper(object, args);
26         System.out.println("調用後");
27         return result;
28     }
29 }

  可以看到同樣是需要實現一個介面——MethodIntercept,並且實現一個和invoke類似的方法——intercept。

  加入測試代碼:

 1 package cglibproxy;
 2 
 3 /**
 4  * Created by Kevin on 2017/11/6.
 5  */
 6 public class Main {
 7     public static void main(String[] args) {
 8         RealSubject subject = (RealSubject) new ProxySubject().getProxy(RealSubject.class);
 9         subject.sayHello();
10         System.out.println(subject.getClass().getName());
11     }
12 }

  執行結果:

  可以看到的執行結果和JDK動態代理的結果一樣,不同的是代理類的類型是cglibproxy.RealSubject$$EnhancerByCGLIB$$cb568e93。接著我們來看CGLib是如何生成代理類的。

  生成代理類的是ProxySubject類中的getProxy方法,而其中又是傳入兩個參數:

enhancer.setSuperclass(clazz);    //設置需要代理的類
enhancer.setCallback(this);    //設置回調方法

  參數設置好後就調用enhancer.create()方法創建代理類。

1 public Object create() {
2     classOnly = false;    //這個欄位設置為false表示返回的是具體的Object代理類,在createClass()方法中設置的是classOnly=true表示的返回class類型的代理類。
3     argumentTypes = null;        //創建的是無參目標對象的代理類,故沒有參數,所以參數類型設置為null
4     return createHelper();
5 }

  看來還在調用一個叫createHelper的方法。

1 private Object createHelper() {
2     preValidate();        //提前作一些校驗
3    //
4     ……
5 }
 1 private void preValidate() {
 2     if (callbackTypes == null) {
 3         callbackTypes = CallbackInfo.determineTypes(callbacks, false);
 4         validateCallbackTypes = true;
 5     }    //檢查回調方法是否為空
 6     if (filter == null) {    //檢查是否設置過濾器,如果設置了多個回調方法就需要設置過濾器
 7         if (callbackTypes.length > 1) {
 8             throw new IllegalStateException("Multiple callback types possible but no filter specified");
 9         }
10         filter = ALL_ZERO;
11     }
12 }

  接著查看createHelper的剩餘代碼:

 1 private Object createHelper() {
 2     preValidate();
 3     Object key = KEY_FACTORY.newInstance((superclass != null) ? superclass.getName() : null,
 4             ReflectUtils.getNames(interfaces),
 5             filter == ALL_ZERO ? null : new WeakCacheKey<CallbackFilter>(filter),
 6             callbackTypes,
 7             useFactory,
 8             interceptDuringConstruction,
 9             serialVersionUID);
10     this.currentKey = key;        //在CGLib中也使用到了緩存機制,這段代碼也比較複雜,有關緩存的策略暫時也不做分析吧 
11     Object result = super.create(key);    //利用位元組實現並創建代理類對象
12     return result;
13 }

  馬馬虎虎地只能說是介紹了JDK與CGLib兩種動態代理技術,並沒有很深入地研究,特別是在兩者在緩存機制上的實現,略感遺憾。

  另外,在開頭提到了CGLib的性能比JDK高,這實際上並不准確。或許這在特別條件下的確如此,因為在我實測發現JDK8的動態代理效率非常高,甚至略高於CGLib,但是在JDK6的環境下的效率就顯得比較低了。所以,通常所說的CGLib性能比JDK動態代理要高,是傳統的掛念,實際上Java一直都在不斷優化動態代理性能,在比較高版本的JDK條件下可以放行大膽的使用JDK原生的動態代理。

 

 

這是一個能給程式員加buff的公眾號 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ...
  • 今天,打開了以前裝過的vmware虛擬機,正常啟動之後,一直想不起登錄密碼,怎麼都是登錄不進去。然後在網上查找資料,最後重置了密碼。下麵,分享下具體操作過程。 1、重新啟動虛擬機,在出現啟動進度條時按下e鍵(啟動編輯器),進入以下界面後,再按下e鍵; 2、進入以下界面後,通過上下鍵選中第二個選項,再 ...
  • 源起 最近看到國內兩篇文章[1][2]先後翻譯了就職於Netflix的性能分析大牛Brendan Gregg於2017年7月31日寫的《Golang bcc/BPF Function Tracing》[3],這迅速引起了我的興趣,2016年時我曾在做MQTT伺服器端開發時便意識到軟體調試及動態追蹤技 ...
  • 寄存器變數 這個可以不理睬 register 關鍵字定義的變數直接放在寄存器當中 寄存器是放在CPU內部的存儲單元,它的速度比記憶體快的多,所以當程式中有10000多次調用同一個變數的時候聲明成寄存器變數會提高程式的執行速度。 科技發展不用這樣寫 register int i,f=1; 外部變數 使用 ...
  • 文件操作基本流程 電腦系統分為:電腦硬體,操作系統,應用程式三部分。 我們用python或其他語言編寫的應用程式若想要把數據永久保存下來,必須要保存於硬碟中,這就涉及到應用程式要操作硬體,眾所周知,應用程式是無法直接操作硬體的,這就用到了操作系統。操作系統把複雜的硬體操作封裝成簡單的介面給用戶/ ...
  • 有時會遇到this作為返回值的情況,那麼此時返回的到底是什麼呢? 返回的是調用this所處方法的那個對象的引用,讀起來有點繞口哈,有沒有想起小學語文分析句子成份的試題,哈哈。 一點點分析的話,主幹是“返回的是引用”; 什麼引用呢?“那個對象的引用”; 哪個對象呢?“調用方法的那個對象”; 調用的哪個 ...
  • 字元串是 Python 中最常用的數據類型。我們可以使用引號( ' 或 " )來創建字元串。 創建字元串很簡單,只要為變數分配一個值即可。 python中單引號和雙引號使用完全相同。 使用三引號( ''' 或 """ )可以指定一個多行字元串。 轉義符 '\' 自然字元串, 通過在字元串前加r或R。 ...
  • 文件的讀操作 示例: 運行結果: 文件的寫操作 知識點: 1. 寫操作前,文件先格式化清空文件 2.清空操作,在執行open的w方法後,清空 運行結果: 打開文件後顯示如下 文件的append方法 語法格式: 文件這種方法為追加模式:1, 空白文件中,直接從頭開始寫入內容; 2 有內容的文件,會在末 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...