天天看點

設計模式之代理,手動實作動态代理,揭秘原理實作

  周末,帶着老婆兒子一起逛公園。兒子一個人跑在前面,吧唧一下不小心摔了一跤,腦袋瓜子摔了個包,稀裡嘩啦的哭道:“爸爸,我會不會摔成傻子!”

  我指了指我頭上的傷痕安慰道:“不會的,你看,這是爸爸小時候摔的。”

  話還沒有說話,小家夥哭的更厲害了:“那就是說我長大後就會和你一樣傻了,我不要,我不要!”

  老婆忍不住發飙:“别哭了,你怎麼會變傻呢?你看你爸,你爸傻嗎?”

  我趕緊回應道:“是啊,你看我多聰明!”

  兒子:“真的,不騙我?”

  老婆:“當然!”

  兒子:“可是如果老爸不是傻子,當年怎麼會娶你這個母老虎呢?”

  我、老婆:……

設計模式之代理,手動實作動态代理,揭秘原理實作

  所謂代理,就是一個人或者一個機構代表另一個人或者另一個機構采取行動。在一些情況下,一個客戶不想或者不能直接引用一個對象,而代理對象可以在用戶端和目标對象之間起到中介的左右。

  代理模式:給某一個對象提供一個代理或占位符,并由代理對象來控制對原對象的通路,通過代理對象通路目标對象,這樣可以在不修改原目标對象的前提下,提供額外的功能操作,擴充目标對象的功能。說簡單點,代理模式就是設定一個中間代理來控制通路原目标對象,以達到增強原對象的功能和簡化通路方式。一般而言會分三種:靜态代理、動态代理和CGLIB代理

  代理模式結構如下:

設計模式之代理,手動實作動态代理,揭秘原理實作

  靜态代理需要代理對象和被代理對象實作一樣的接口,我們來看個例子就清楚了

  示例代理:static-proxy

  代理類:UserDaoProxy.java

設計模式之代理,手動實作動态代理,揭秘原理實作
設計模式之代理,手動實作動态代理,揭秘原理實作

View Code

  UserDaoProxy代理IUserDao類型,此時也隻能代理IUserDao類型的被代理對象。測試結果就不展示了,相信大家看了代碼也知道了

  優點:可以在不修改目标對象的前提下擴充目标對象的功能

  缺點:如果需要代理多個類,每個類都會有一個代理類,會導緻代理類無限制擴充;如果類中有多個方法,同樣的代理邏輯需要反複實作、應用到每個方法上,一旦接口增加方法,目标對象與代理對象都要進行修改

  一個靜态代理隻能代理一個類,那麼有沒有什麼方式可以實作同一個代理類來代理任意對象呢?肯定有的,也就是下面講到的:動态代理

  代理類在程式運作時建立的代理方式被成為動态代理。 也就是說,這種情況下,代理類并不是在Java代碼中定義的,而是在運作時根據我們在Java代碼中的“訓示”動态生成的。下面我們一步一步手動來實作動态代理。下面的示例都是直接針對接口的,就不是針對接口的具體實作類了,靜态代理示例中,UserDaoProxy代理的是IUserDao的實作類:UserDaoImpl,那麼動态代理示例就直接針對接口了,下面示例針對的都是UserMapper接口,模拟的mybatis,但不局限于UserMapper接口

    1、先利用反射動态生成代理類,并持久化代理類到磁盤(也就是生成代理類的java源檔案),generateJavaFile方法如下

設計模式之代理,手動實作動态代理,揭秘原理實作
設計模式之代理,手動實作動态代理,揭秘原理實作

      生成的代理類:$Proxy0.java 如下

設計模式之代理,手動實作動态代理,揭秘原理實作
設計模式之代理,手動實作動态代理,揭秘原理實作

      這個代理類的生成過程是我們自己實作的,實作不難,但排版太繁瑣,我們可以用javapoet來生成代理類源代碼,generateJavaFileByJavaPoet方法如下

設計模式之代理,手動實作動态代理,揭秘原理實作
設計模式之代理,手動實作動态代理,揭秘原理實作

      生成的代理類:JavaPoet$Proxy0.java 如下

設計模式之代理,手動實作動态代理,揭秘原理實作
設計模式之代理,手動實作動态代理,揭秘原理實作

    利用javapoet生成的代理類更接*我們*時手動實作的類,排版更符合我們的編碼習慣,看上去更自然一些;兩者的實作過程是一樣的,隻是javapoet排版更好

    2、既然代理類的源代碼已經有了,那麼需要對其編譯了,compileJavaFile方法如下

設計模式之代理,手動實作動态代理,揭秘原理實作
設計模式之代理,手動實作動态代理,揭秘原理實作

      會在指定目錄下看到:$Proxy0.class

    3、加載$Proxy0.class,并建立其執行個體對象(代理執行個體對象)

設計模式之代理,手動實作動态代理,揭秘原理實作
設計模式之代理,手動實作動态代理,揭秘原理實作

      有了代理執行個體對象,我們就可以利用它進行操作了,示範結果如下

設計模式之代理,手動實作動态代理,揭秘原理實作

    完整工程位址:proxy-java-file,完整流程圖如下

設計模式之代理,手動實作動态代理,揭秘原理實作

    此時的Proxy類能建立任何接口的執行個體,解決了靜态代理存在的代理類泛濫、多個方法中代理邏輯反複實作的問題;但有個問題不知道大家注意到:$Proxy0.java有必要持久化到磁盤嗎,我們能不能直接編譯記憶體中的代理類的字元串源代碼,得到$Proxy0.class呢?

    $Proxy0.java和$Proxy0.class是沒必要生成到磁盤的,我們直接編譯記憶體中的代理類的字元串源代碼,同時直接在記憶體中加載$Proxy0.class,不用寫、讀磁盤,可以提升不少性能

    完整工程位址:proxy-none-java-file,此時的流程圖如下

設計模式之代理,手動實作動态代理,揭秘原理實作

    Proxy.java源代碼如下

設計模式之代理,手動實作動态代理,揭秘原理實作
設計模式之代理,手動實作動态代理,揭秘原理實作

    相比有代理類源代碼持久化,核心的動态代理生成過程不變,隻是減少了.java和.class檔案的持久化;其中用到了第三方工具:com.itranswarp.compile(我們也可以拓展jdk,實作記憶體中操作),完成了字元串在記憶體中的編譯、class在記憶體中的加載,直接用jdk的編譯工具,會在磁盤生成$Proxy0.class

    測試結果如下

設計模式之代理,手動實作動态代理,揭秘原理實作

      可以看到,沒有.java和.class的持久化

    此時就完美了嗎?如果現在有另外一個接口ISendMessage,代理邏輯不是

    我們該怎麼辦? 針對ISendMessage又重新寫一個Proxy?顯然還不夠靈活,說的簡單點:此種代理可以代理任何接口,但是代理邏輯确是固定死的,不能自定義,這樣會造成一種代理邏輯會有一個代理工廠(Proxy),會造成代理工廠的泛濫

    既然無代理類源代碼持久化中的代理邏輯不能自定義,那麼我們就将它抽出來,提供代理邏輯接口

    完整工程位址:proxy-none-java-file-plus,流程圖與無代理類源代碼持久化中一樣,此時代理類的生成過程複雜了不少,涉及到代理邏輯接口:InvacationHandler的處理

    generateJavaFile(...)方法

設計模式之代理,手動實作動态代理,揭秘原理實作
設計模式之代理,手動實作動态代理,揭秘原理實作
設計模式之代理,手動實作動态代理,揭秘原理實作

    此時各元件之間關系、調用情況如下

設計模式之代理,手動實作動态代理,揭秘原理實作

    此時Proxy就可以完全通用了,可以生成任何接口的代理對象了,也可以實作任意的代理邏輯;至此,我們完成了一個簡易的仿JDK實作的動态代理

    我們來看看JDK下動态代理的實作,示例工程:proxy-jdk,測試結果就不展示了,我們來看看JDK下Proxy.newInstance方法,有三個參數

      1、Classloader:類加載器,我們可以使用自定義的類加載器;上述手動實作示例中,直接在Proxy寫死了;

      2、Class<?>[]:接口類數組,這個其實很容易了解,我們應該允許我們自己實作的代理類同時實作多個接口。我們上述手動實作中隻傳入一個接口,是為了簡化實作;

      3、InvocationHandler:這個沒什麼好說的,與我們的實作一緻,用于自定義代理邏輯

    我們來追下源碼,看看JDK的動态代理是否與我們的手動實作是否一緻

設計模式之代理,手動實作動态代理,揭秘原理實作

    與我們的自定義實作差不多,利用反射,逐個接口、逐個方法進行處理;ProxyClassFactory負責生成代理類的Class對象,主要由apply方法負責,調用了

    來生成代理類的Class;ProxyGenerator中有個是有靜态常量:saveGeneratedFiles,辨別是否持久化代理類的class檔案,預設值是false,也就是不持久化,我們可以通過設定jdk系統參數,實作JDK的動态代理持久化代理類的class檔案

  對cglib不做深入研究了,隻舉個使用案例:proxy-cglib,使用方式與JDK的動态代理類似,實作的效果也基本一緻,但是實作原理上還是有差别的

  JDK的動态代理有一個限制,就是使用動态代理的對象必須實作一個或多個接口,而CGLIB沒有這個限制,具體差別不是本文範疇了,大家自行去查閱資料

  長篇大論講了那麼多,我們卻一直沒有講動态代理的作用,使用動态代理我們可以在不改變源碼的情況下,對目标對象的目标方法進行前置或後置增強處理。這有點不太符合我們的一條線走到底的程式設計邏輯,這種程式設計模型有一個專業名稱叫AOP,面向切面程式設計,具體案例有如下:

  1、spring的事務,事務的開啟可以作為前置增強,事務的送出或復原作為後置增強,資料庫的操作處在兩者之間(目标對象需要完成的事);

  2、日志記錄,我們可以在不改變原有實作的基礎上,對目标對象進行日志的輸出,可以前置處理,記錄參數情況,也可以後置處理,記錄傳回的結果;

  3、web程式設計,傳入參數的校驗;

  4、web程式設計,權限的控制也可以用aop來實作;

  隻要明白了AOP,那麼哪些場景能使用動态代理也就比較明了了

  1、示例代碼中的Proxy是代理工廠,負責生産代理對象的,不是代理對象類

  2、手動實作動态代理,我們分了三版

    第一版:代理類源代碼持久化,為了便于了解,我們将代理類的java檔案和class檔案持久化到了磁盤,此時解決了靜态代理中代理類泛濫的問題,我們的代理類工廠(Proxy)能代理任何接口;

    第二版:代理類源代碼不持久化,代理類的java檔案和和class檔案本來就隻是臨時檔案,将其去掉,不用讀寫磁盤,可以提高效率;但此時有個問題,我們的代理邏輯卻寫死了,也就說一個代理類工廠隻能生産一種代理邏輯的代理類對象,如果我們有多種代理邏輯,那麼就需要有多個代理類工廠,顯然靈活性不夠高,還有優化空間;

    第三版:代理邏輯接口化,供使用者自定義,此時代理類工廠就可以代理任何接口、任何代理邏輯了,反正代理邏輯是使用者自定義傳入,使用者想怎麼定義就怎麼定義;

  3、示例參考的是mybatis中mapper的生成過程,雖然隻是簡單的模拟,但流程卻是一緻的,有興趣的可以看看我前兩篇部落格,結合起來看更好了解

  《java與模式》

  10分鐘看懂動态代理設計模式

繼續閱讀