
前言
《設計模式自習室》系列,顧名思義,本系列文章帶你溫習常見的設計模式。主要內容有:
- 該模式的介紹,包括: 引子、意圖(大白話解釋) 類圖、時序圖(理論規範)
- 該模式的代碼示例:熟悉該模式的代碼長什麼樣子
- 該模式的優缺點:模式不是萬金油,不可以濫用模式
- 該模式的應用案例:瞭解它在哪些重要的源碼中被使用
該系列會逐步更新於我的博客和公眾號(博客見文章底部),也希望各位觀眾老爺能夠關注我的個人公眾號:後端技術漫談,不會錯過精彩好看的文章。
系列文章回顧
- 【設計模式自習室】開篇:為什麼我們要用設計模式?
- 【設計模式自習室】建造者模式
- 【設計模式自習室】原型模式
- 【設計模式自習室】透徹理解單例模式
- 【設計模式自習室】理解工廠模式的三種形式
- 【設計模式自習室】適配器模式
- 【設計模式自習室】裝飾模式
- 【設計模式自習室】橋接模式 Bridge Pattern:處理多維度變化
- 【設計模式自習室】門面模式 Facade Pattern
- 【設計模式自習室】享元模式 Flyweight Pattern:減少對象數量
結構型——代理模式 Proxy Pattern
引子
通俗的來講,代理模式就是我們生活中常見的中介。在某些情況下,一個客戶不想或者不能直接引用一個對象,此時可以通過一個稱之為“代理”的第三者來實現間接引用。
為什麼要用代理模式
- 中介隔離作用:在某些情況下,一個客戶類不想或者不能直接引用一個委託對象,而代理類對象可以在客戶類和委託對象之間起到中介的作用,其特徵是代理類和委託類實現相同的接口。
- 開閉原則,增加功能:真正的業務功能還是由委託類來實現,但是可以在業務功能執行的前後加入一些公共的服務。例如我們想給項目加入緩存、日誌這些功能,我們就可以使用代理類來完成,而沒必要打開已經封裝好的委託類。
定義
代理模式給某一個對象提供一個代理對象,並由代理對象控制對原對象的引用。
常見的代理區分為靜態代理和動態代理:
1. 靜態代理
在程序運行前就已經存在代理類的字節碼文件,代理類和真實主題角色的關係在運行前就確定了。
是由程序員創建或特定工具自動生成源代碼,在對其編譯。在程序員運行之前,代理類.class文件就已經被創建了。
2. 動態代理
為什麼類可以動態的生成?
這就涉及到Java虛擬機的類加載機制了
Java虛擬機類加載過程主要分為五個階段:加載、驗證、準備、解析、初始化。其中加載階段需要完成以下3件事情:
通過一個類的全限定名來獲取定義此類的二進制字節流 將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構 在內存中生成一個代表這個類的 java.lang.Class 對象,作為方法區這個類的各種數據訪問入口
由於虛擬機規範對這3點要求並不具體,所以實際的實現是非常靈活的,關於第1點,獲取類的二進制字節流(class字節碼)就有很多途徑:
從ZIP包獲取,這是JAR、EAR、WAR等格式的基礎 從網絡中獲取,典型的應用是 Applet 運行時計算生成,這種場景使用最多的是動態代理技術,在 java.lang.reflect.Proxy 類中,就是用了
ProxyGenerator.generateProxyClass 來為特定接口生成形式為 *$Proxy 的代理類的二進制字節流 由其它文件生成,典型應用是JSP,即由JSP文件生成對應的Class類 從數據庫中獲取等等
所以,動態代理就是想辦法,根據接口或目標對象,計算出代理類的字節碼,然後再加載到JVM中使用。
更多Java類加載機制可以查看:
Java虛擬機知識點快速複習手冊
動態代理又有兩種典型的實現方式:JDK動態代理和CGLib動態代理
- 通過實現接口的方式 -> JDK動態代理
- 通過繼承類的方式 -> CGLIB動態代理
2.1 JDK反射機制(接口代理)
- 是在程序運行時通過反射機制動態創建的。
- 為需要攔截的接口生成代理對象以實現接口方法攔截功能。
2.2 CGLIB代理
- 其原理是通過字節碼技術為一個類創建子類,並在子類中採用方法攔截的技術攔截所有父類方法的調用,順勢織入橫切邏輯。
- 但因為採用的是繼承,所以不能對final修飾的類進行代理。
- JDK動態代理與CGLib動態代理均是實現Spring AOP的基礎。
類圖
如果看不懂UML類圖,可以先粗略瀏覽下該圖,想深入瞭解的話,可以繼續谷歌,深入學習:

代理模式包含如下角色:
- Subject(抽象主題角色):定義代理類和真實主題的公共對外方法,也是代理類代理真實主題的方法;
- RealSubject(真實主題角色):真正實現業務邏輯的類;
- Proxy(代理主題角色):用來代理和封裝真實主題;
時序圖
代碼實現和使用場景
代理模式得到了非常廣泛的應用,最常用的便是我們在Spring中使用的CGlib代理,所以我們將代碼示例和使用場景再次融合在一起來講解。
主要分為四個代碼小Demo,分別是:
- 靜態代理代碼示例
- JDK動態代理代碼示例
- CGLIB底層使用的ASM字節碼插入技術代碼示例
- CGLIB動態代理代碼示例
1. 靜態代理示例代碼
編寫一個接口 UserService ,以及該接口的一個實現類 UserServiceImpl
<code>public
interface
UserService
{public
void
select
();public
void
update
();}public
class
UserServiceImpl
implements
UserService
{public
void
select
() { System.out
.println("查詢 selectById"
); }public
void
update
() { System.out
.println("更新 update"
); }}/<code>
我們將通過靜態代理對 UserServiceImpl 進行功能增強,在調用 select 和 update 之前記錄一些日誌(記錄開始和結束的時間點)。寫一個代理類 UserServiceProxy,代理類需要實現 UserService
<code>public
class
UserServiceProxy
implements
UserService
{private
UserService target; /<code>
客戶端測試
<code>public
class
Client1
{public
static
void
main
(String[] args
) { UserService userServiceImpl =new
UserServiceImpl(); UserService proxy =new
UserServiceProxy(userServiceImpl); proxy.select
(); proxy.update(); }}/<code>
通過靜態代理,我們達到了功能增強的目的,而且沒有侵入原代碼,這是靜態代理的一個優點。
2. JDK動態代理
JDK動態代理主要涉及兩個類:java.lang.reflect.Proxy 和
java.lang.reflect.InvocationHandler
編寫一個調用邏輯處理器 LogHandler 類,提供日誌增強功能,並實現 InvocationHandler 接口;在 LogHandler 中維護一個目標對象,這個對象是被代理的對象(真實主題角色);在 invoke 方法中編寫方法調用的邏輯處理
<code>import
java.lang.reflect.InvocationHandler;import
java.lang.reflect.Method;import
java.util.Date;public
class
LogHandler
implements
InvocationHandler
{ Object target; /<code>
編寫客戶端,獲取動態生成的代理類的對象須藉助 Proxy 類的 newProxyInstance 方法,具體步驟可見代碼和註釋
<code>import
proxy.UserService;import
proxy.UserServiceImpl;import
java.lang.reflect.InvocationHandler;import
java.lang.reflect.Proxy;public
class
Client2
{public
static
void
main
(String[] args)
throws
IllegalAccessException, InstantiationException { /<code>
結果:
<code>logstart
time
[ThuDec
20
16
:55
:19
CST2018
] 查詢 selectByIdlogend
time
[ThuDec
20
16
:55
:19
CST2018
]log
start
time
[ThuDec
20
16
:55
:19
CST2018
] 更新 updatelogend
time
[ThuDec
20
16
:55
:19
CST2018
] /<code>
上方1和2中的示例代碼來自,文中有詳細的細節分析:
http://laijianfeng.org/2018/12/Java-%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86%E8%AF%A6%E8%A7%A3/
3. CGLib動態代理
CGLIB代理則是通過繼承的方式來生成代理類。
字節碼修改示例代碼
首先,我們先通過代碼來了解一下字節碼修改技術是如何實現的
我們使用ASM字節碼操作類庫(cglib底層使用的是ASM)來給出一段示例代碼,小夥伴們也可以自己在本地運行試試。
ASM 可以直接產生二進制 class 文件,它能被用來動態生成類或者增強既有類的功能。
ASM從類文件中讀入信息後,能夠改變類行為,分析類信息,甚至能夠根據用戶要求生成新類。
ASM相對於其他類似工具如BCEL、SERP、Javassist、CGLIB,它的最大的優勢就在於其性能更高,其jar包僅30K。Hibernate和Spring都使用了cglib代理,而cglib底層使用的是ASM,可見ASM在各種開源框架都有廣泛的應用。
Base類:被修改的類,該類實現了每3秒輸出一個process,用來模擬正在處理的請求。
<code>packageasm
;import
java.lang.management.ManagementFactory;public
class
Base
{public
static
void
main
(String[] args)
{ String name = ManagementFactory.getRuntimeMXBean().getName(); String s = name.split("@"
)[0
]; /<code>
執行字節碼修改和轉換的類:該類中,我們實現在被修改類前後都輸出start和end語句的方法
<code>public
class
TestTransformer
implements
ClassFileTransformer
{public
byte
[] transform(ClassLoader loader, String className, Class> classBeingRedefined, ProtectionDomain protectionDomain,byte
[] classfileBuffer)throws
IllegalClassFormatException { System.out.println("Transforming "
+ className);try
{ ClassPool cp = ClassPool.getDefault(); CtClass cc = cp.get("asm.Base"
); CtMethod m = cc.getDeclaredMethod("process"
); m.insertBefore("{ System.out.println("start"); }"
); m.insertAfter("{ System.out.println("end"); }"
);return
cc.toBytecode(); }catch
(Exception e) { e.printStackTrace(); }return
null
; }}/<code>
接著我們生成字節碼修改的Jar包
<code>public
class
TestAgent
{public
static
void
agentmain
(String args, Instrumentation inst)
{ inst.addTransformer(new
TestTransformer(),true
);try
{ inst.retransformClasses(TransformTarget.
class
); System.out.println("Agent Load Done."
); }catch
(Exception e) { System.out.println("agent load failed!"
); } }}/<code>
我們將生成的agent.jar通過JVM Tool寫入正在執行的Base進程中。可以看出,本來只是3秒輸出procss的類變成了從前後輸出start和end的類,類被成功修改了。
上面字節碼修改的Demo代碼我放在了自己的Github倉庫中:
https://github.com/qqxx6661/Java_Practise/tree/master/ASMDemo
CGLib動態代理代碼示例
maven引入CGLIB包,然後編寫一個UserDao類,它沒有接口,只有兩個方法,select() 和 update()
<code>public
class
UserDao
{public
void
select
() { System.out
.println("UserDao 查詢 selectById"
); }public
void
update
() { System.out
.println("UserDao 更新 update"
); }}/<code>
編寫一個 LogInterceptor ,繼承了 MethodInterceptor,用於方法的攔截回調
<code>import
java.lang.reflect.Method;import
java.util.Date;public
class
LogInterceptor
implements
MethodInterceptor
{public
Objectintercept
(Object object, Method method, Object[] objects, MethodProxy methodProxy)
throws
Throwable { before(); Object result = methodProxy.invokeSuper(object, objects); /<code>
測試類
<code>import
net.sf.cglib.proxy.Enhancer;public
class
CglibTest
{public
static
void
main
(String[] args)
{ DaoProxy daoProxy =new
DaoProxy(); Enhancer enhancer =new
Enhancer(); enhancer.setSuperclass(Dao.
class
); /<code>
運行結果和上面相同。
優缺點
靜態代理優缺點
- 優點:可以做到在符合開閉原則的情況下對目標對象進行功能擴展。
- 缺點:當需要代理多個類的時候,由於代理對象要實現與目標對象一致的接口,有兩種方式: 只維護一個代理類,由這個代理類實現多個接口,但是這樣就導致代理類過於龐大 新建多個代理類,每個目標對象對應一個代理類,但是這樣會產生過多的代理類
JDK動態代理優缺點
- 優勢:雖然相對於靜態代理,動態代理大大減少了我們的開發任務,同時減少了對業務接口的依賴,降低了耦合度。
- 劣勢:只能對接口進行代理
CGLIB動態代理優缺點
CGLIB創建的動態代理對象比JDK創建的動態代理對象的性能更高,但是CGLIB創建代理對象時所花費的時間卻比JDK多得多。
- 所以對於單例的對象,因為無需頻繁創建對象,用CGLIB合適,反之使用JDK方式要更為合適一些。
- 同時由於CGLib由於是採用動態創建子類的方法,對於final修飾的方法無法進行代理。
參考
- https://www.cnblogs.com/daniels/p/8242592.html
- http://laijianfeng.org/2018/12/Java-%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86%E8%AF%A6%E8%A7%A3/
- https://www.cnblogs.com/jie-y/p/10732347.html
補充
裝飾模式與代理模式的區別
裝飾器模式關注於在一個對象上動態的添加方法,然而代理模式關注於控制對對象的訪問。
- 使用代理模式的時候, 我們常常在一個代理類中創建一個對象的實例。
- 當我們使用裝飾器模式的時候,我們通常的做法是將原始對象作為一個參數傳給裝飾者的構造器。
關注我
我是一名後端開發工程師。
主要關注後端開發,數據安全,爬蟲,物聯網,邊緣計算等方向,歡迎交流。
各大平臺都可以找到我
- 微信公眾號:後端技術漫談
- Github:@qqxx6661
- CSDN:@後端技術漫談
- 知乎:@後端技術漫談
- 簡書:@後端技術漫談
- 掘金:@後端技術漫談
原創博客主要內容
- Java面試知識點複習全手冊
- 設計模式/數據結構 自習室
- Leetcode/劍指offer 算法題解析
- SpringBoot/SpringCloud菜鳥入門實戰系列
- 爬蟲相關技術文章
- 後端開發相關技術文章
- 逸聞趣事/好書分享/個人興趣
個人公眾號:後端技術漫談
公眾號:後端技術漫談.jpg
如果文章對你有幫助,不妨收藏,投幣,轉發,在看起來~