前言
《設(shè)計(jì)模式自習(xí)室》系列,顧名思義,本系列文章帶你溫習(xí)常見的設(shè)計(jì)模式薛夜。主要內(nèi)容有:
- 該模式的介紹,包括:
- 引子版述、意圖(大白話解釋)
- 類圖梯澜、時(shí)序圖(理論規(guī)范)
- 該模式的代碼示例:熟悉該模式的代碼長(zhǎng)什么樣子
- 該模式的優(yōu)缺點(diǎn):模式不是萬(wàn)金油,不可以濫用模式
- 該模式的應(yīng)用案例:了解它在哪些重要的源碼中被使用
系列文章回顧
- 【設(shè)計(jì)模式自習(xí)室】開篇:為什么我們要用設(shè)計(jì)模式渴析?
- 【設(shè)計(jì)模式自習(xí)室】建造者模式
- 【設(shè)計(jì)模式自習(xí)室】原型模式
- 【設(shè)計(jì)模式自習(xí)室】透徹理解單例模式
- 【設(shè)計(jì)模式自習(xí)室】理解工廠模式的三種形式
- 【設(shè)計(jì)模式自習(xí)室】適配器模式
- 【設(shè)計(jì)模式自習(xí)室】裝飾模式
- 【設(shè)計(jì)模式自習(xí)室】橋接模式 Bridge Pattern:處理多維度變化
- 【設(shè)計(jì)模式自習(xí)室】門面模式 Facade Pattern
- 【設(shè)計(jì)模式自習(xí)室】享元模式 Flyweight Pattern:減少對(duì)象數(shù)量
結(jié)構(gòu)型——代理模式 Proxy Pattern
引子
通俗的來(lái)講腊徙,代理模式就是我們生活中常見的中介简十。在某些情況下,一個(gè)客戶不想或者不能直接引用一個(gè)對(duì)象撬腾,此時(shí)可以通過一個(gè)稱之為“代理”的第三者來(lái)實(shí)現(xiàn)間接引用。
為什么要用代理模式
- 中介隔離作用:在某些情況下恢恼,一個(gè)客戶類不想或者不能直接引用一個(gè)委托對(duì)象民傻,而代理類對(duì)象可以在客戶類和委托對(duì)象之間起到中介的作用,其特征是代理類和委托類實(shí)現(xiàn)相同的接口场斑。
- 開閉原則漓踢,增加功能:真正的業(yè)務(wù)功能還是由委托類來(lái)實(shí)現(xiàn),但是可以在業(yè)務(wù)功能執(zhí)行的前后加入一些公共的服務(wù)漏隐。例如我們想給項(xiàng)目加入緩存喧半、日志這些功能,我們就可以使用代理類來(lái)完成青责,而沒必要打開已經(jīng)封裝好的委托類挺据。
定義
代理模式給某一個(gè)對(duì)象提供一個(gè)代理對(duì)象,并由代理對(duì)象控制對(duì)原對(duì)象的引用脖隶。
常見的代理區(qū)分為靜態(tài)代理和動(dòng)態(tài)代理:
1. 靜態(tài)代理
在程序運(yùn)行前就已經(jīng)存在代理類的字節(jié)碼文件扁耐,代理類和真實(shí)主題角色的關(guān)系在運(yùn)行前就確定了。
是由程序員創(chuàng)建或特定工具自動(dòng)生成源代碼产阱,在對(duì)其編譯婉称。在程序員運(yùn)行之前,代理類.class文件就已經(jīng)被創(chuàng)建了构蹬。
2. 動(dòng)態(tài)代理
為什么類可以動(dòng)態(tài)的生成王暗?
這就涉及到Java虛擬機(jī)的類加載機(jī)制了
Java虛擬機(jī)類加載過程主要分為五個(gè)階段:加載、驗(yàn)證庄敛、準(zhǔn)備俗壹、解析、初始化铐姚。其中加載階段需要完成以下3件事情:
- 通過一個(gè)類的全限定名來(lái)獲取定義此類的二進(jìn)制字節(jié)流
- 將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)
- 在內(nèi)存中生成一個(gè)代表這個(gè)類的 java.lang.Class 對(duì)象策肝,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)訪問入口
由于虛擬機(jī)規(guī)范對(duì)這3點(diǎn)要求并不具體,所以實(shí)際的實(shí)現(xiàn)是非常靈活的隐绵,關(guān)于第1點(diǎn)之众,獲取類的二進(jìn)制字節(jié)流(class字節(jié)碼)就有很多途徑:
- 從ZIP包獲取,這是JAR依许、EAR棺禾、WAR等格式的基礎(chǔ)
- 從網(wǎng)絡(luò)中獲取,典型的應(yīng)用是 Applet
- 運(yùn)行時(shí)計(jì)算生成峭跳,這種場(chǎng)景使用最多的是動(dòng)態(tài)代理技術(shù)膘婶,在 java.lang.reflect.Proxy 類中缺前,就是用了 ProxyGenerator.generateProxyClass 來(lái)為特定接口生成形式為 *$Proxy 的代理類的二進(jìn)制字節(jié)流
- 由其它文件生成,典型應(yīng)用是JSP悬襟,即由JSP文件生成對(duì)應(yīng)的Class類
- 從數(shù)據(jù)庫(kù)中獲取等等
所以衅码,動(dòng)態(tài)代理就是想辦法,根據(jù)接口或目標(biāo)對(duì)象脊岳,計(jì)算出代理類的字節(jié)碼逝段,然后再加載到JVM中使用。
更多Java類加載機(jī)制可以查看:
Java虛擬機(jī)知識(shí)點(diǎn)快速?gòu)?fù)習(xí)手冊(cè)
動(dòng)態(tài)代理又有兩種典型的實(shí)現(xiàn)方式:JDK動(dòng)態(tài)代理和CGLib動(dòng)態(tài)代理
- 通過實(shí)現(xiàn)接口的方式 -> JDK動(dòng)態(tài)代理
- 通過繼承類的方式 -> CGLIB動(dòng)態(tài)代理
2.1 JDK反射機(jī)制(接口代理)
- 是在程序運(yùn)行時(shí)通過反射機(jī)制動(dòng)態(tài)創(chuàng)建的割捅。
- 為需要攔截的接口生成代理對(duì)象以實(shí)現(xiàn)接口方法攔截功能奶躯。
2.2 CGLIB代理
- 其原理是通過字節(jié)碼技術(shù)為一個(gè)類創(chuàng)建子類,并在子類中采用方法攔截的技術(shù)攔截所有父類方法的調(diào)用亿驾,順勢(shì)織入橫切邏輯嘹黔。
- 但因?yàn)椴捎玫氖抢^承,所以不能對(duì)final修飾的類進(jìn)行代理莫瞬。
- JDK動(dòng)態(tài)代理與CGLib動(dòng)態(tài)代理均是實(shí)現(xiàn)Spring AOP的基礎(chǔ)儡蔓。
類圖
如果看不懂UML類圖,可以先粗略瀏覽下該圖乏悄,想深入了解的話浙值,可以繼續(xù)谷歌,深入學(xué)習(xí):
代理模式包含如下角色:
- Subject(抽象主題角色):定義代理類和真實(shí)主題的公共對(duì)外方法檩小,也是代理類代理真實(shí)主題的方法开呐;
- RealSubject(真實(shí)主題角色):真正實(shí)現(xiàn)業(yè)務(wù)邏輯的類;
- Proxy(代理主題角色):用來(lái)代理和封裝真實(shí)主題规求;
時(shí)序圖
代碼實(shí)現(xiàn)和使用場(chǎng)景
代理模式得到了非常廣泛的應(yīng)用筐付,最常用的便是我們?cè)赟pring中使用的CGlib代理,所以我們將代碼示例和使用場(chǎng)景再次融合在一起來(lái)講解阻肿。
主要分為四個(gè)代碼小Demo瓦戚,分別是:
- 靜態(tài)代理代碼示例
- JDK動(dòng)態(tài)代理代碼示例
- CGLIB底層使用的ASM字節(jié)碼插入技術(shù)代碼示例
- CGLIB動(dòng)態(tài)代理代碼示例
1. 靜態(tài)代理示例代碼
編寫一個(gè)接口 UserService ,以及該接口的一個(gè)實(shí)現(xiàn)類 UserServiceImpl
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");
}
}
我們將通過靜態(tài)代理對(duì) UserServiceImpl 進(jìn)行功能增強(qiáng)丛塌,在調(diào)用 select 和 update 之前記錄一些日志(記錄開始和結(jié)束的時(shí)間點(diǎn))较解。寫一個(gè)代理類 UserServiceProxy,代理類需要實(shí)現(xiàn) UserService
public class UserServiceProxy implements UserService {
private UserService target; // 被代理的對(duì)象
public UserServiceProxy(UserService target) {
this.target = target;
}
public void select() {
before();
target.select(); // 這里才實(shí)際調(diào)用真實(shí)主題角色的方法
after();
}
public void update() {
before();
target.update(); // 這里才實(shí)際調(diào)用真實(shí)主題角色的方法
after();
}
private void before() { // 在執(zhí)行方法之前執(zhí)行
System.out.println(String.format("log start time [%s] ", new Date()));
}
private void after() { // 在執(zhí)行方法之后執(zhí)行
System.out.println(String.format("log end time [%s] ", new Date()));
}
}
客戶端測(cè)試
public class Client1 {
public static void main(String[] args) {
UserService userServiceImpl = new UserServiceImpl();
UserService proxy = new UserServiceProxy(userServiceImpl);
proxy.select();
proxy.update();
}
}
通過靜態(tài)代理赴邻,我們達(dá)到了功能增強(qiáng)的目的印衔,而且沒有侵入原代碼,這是靜態(tài)代理的一個(gè)優(yōu)點(diǎn)姥敛。
2. JDK動(dòng)態(tài)代理
JDK動(dòng)態(tài)代理主要涉及兩個(gè)類:java.lang.reflect.Proxy 和 java.lang.reflect.InvocationHandler
編寫一個(gè)調(diào)用邏輯處理器 LogHandler 類奸焙,提供日志增強(qiáng)功能,并實(shí)現(xiàn) InvocationHandler 接口;在 LogHandler 中維護(hù)一個(gè)目標(biāo)對(duì)象与帆,這個(gè)對(duì)象是被代理的對(duì)象(真實(shí)主題角色)了赌;在 invoke 方法中編寫方法調(diào)用的邏輯處理
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Date;
public class LogHandler implements InvocationHandler {
Object target; // 被代理的對(duì)象,實(shí)際的方法執(zhí)行者
public LogHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
Object result = method.invoke(target, args); // 調(diào)用 target 的 method 方法
after();
return result; // 返回方法的執(zhí)行結(jié)果
}
// 調(diào)用invoke方法之前執(zhí)行
private void before() {
System.out.println(String.format("log start time [%s] ", new Date()));
}
// 調(diào)用invoke方法之后執(zhí)行
private void after() {
System.out.println(String.format("log end time [%s] ", new Date()));
}
}
編寫客戶端玄糟,獲取動(dòng)態(tài)生成的代理類的對(duì)象須借助 Proxy 類的 newProxyInstance 方法勿她,具體步驟可見代碼和注釋
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 {
// 設(shè)置變量可以保存動(dòng)態(tài)代理類,默認(rèn)名稱以 $Proxy0 格式命名
// System.getProperties().setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
// 1. 創(chuàng)建被代理的對(duì)象茶凳,UserService接口的實(shí)現(xiàn)類
UserServiceImpl userServiceImpl = new UserServiceImpl();
// 2. 獲取對(duì)應(yīng)的 ClassLoader
ClassLoader classLoader = userServiceImpl.getClass().getClassLoader();
// 3. 獲取所有接口的Class嫂拴,這里的UserServiceImpl只實(shí)現(xiàn)了一個(gè)接口UserService,
Class[] interfaces = userServiceImpl.getClass().getInterfaces();
// 4. 創(chuàng)建一個(gè)將傳給代理類的調(diào)用請(qǐng)求處理器贮喧,處理所有的代理對(duì)象上的方法調(diào)用
// 這里創(chuàng)建的是一個(gè)自定義的日志處理器,須傳入實(shí)際的執(zhí)行對(duì)象 userServiceImpl
InvocationHandler logHandler = new LogHandler(userServiceImpl);
/*
5.根據(jù)上面提供的信息猪狈,創(chuàng)建代理對(duì)象 在這個(gè)過程中箱沦,
a.JDK會(huì)通過根據(jù)傳入的參數(shù)信息動(dòng)態(tài)地在內(nèi)存中創(chuàng)建和.class 文件等同的字節(jié)碼
b.然后根據(jù)相應(yīng)的字節(jié)碼轉(zhuǎn)換成對(duì)應(yīng)的class,
c.然后調(diào)用newInstance()創(chuàng)建代理實(shí)例
*/
UserService proxy = (UserService) Proxy.newProxyInstance(classLoader, interfaces, logHandler);
// 調(diào)用代理的方法
proxy.select();
proxy.update();
// 保存JDK動(dòng)態(tài)代理生成的代理類雇庙,類名保存為 UserServiceProxy
// ProxyUtils.generateClassFile(userServiceImpl.getClass(), "UserServiceProxy");
}
}
結(jié)果:
log start time [Thu Dec 20 16:55:19 CST 2018]
查詢 selectById
log end time [Thu Dec 20 16:55:19 CST 2018]
log start time [Thu Dec 20 16:55:19 CST 2018]
更新 update
log end time [Thu Dec 20 16:55:19 CST 2018]
上方1和2中的示例代碼來(lái)自谓形,文中有詳細(xì)的細(xì)節(jié)分析:
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動(dòng)態(tài)代理
CGLIB代理則是通過繼承的方式來(lái)生成代理類。
字節(jié)碼修改示例代碼
首先疆前,我們先通過代碼來(lái)了解一下字節(jié)碼修改技術(shù)是如何實(shí)現(xiàn)的
我們使用ASM字節(jié)碼操作類庫(kù)(cglib底層使用的是ASM)來(lái)給出一段示例代碼寒跳,小伙伴們也可以自己在本地運(yùn)行試試。
ASM 可以直接產(chǎn)生二進(jìn)制 class 文件竹椒,它能被用來(lái)動(dòng)態(tài)生成類或者增強(qiáng)既有類的功能童太。
ASM從類文件中讀入信息后,能夠改變類行為胸完,分析類信息书释,甚至能夠根據(jù)用戶要求生成新類。
ASM相對(duì)于其他類似工具如BCEL赊窥、SERP爆惧、Javassist、CGLIB锨能,它的最大的優(yōu)勢(shì)就在于其性能更高扯再,其jar包僅30K。Hibernate和Spring都使用了cglib代理址遇,而cglib底層使用的是ASM熄阻,可見ASM在各種開源框架都有廣泛的應(yīng)用。
Base類:被修改的類傲隶,該類實(shí)現(xiàn)了每3秒輸出一個(gè)process饺律,用來(lái)模擬正在處理的請(qǐng)求。
package asm;
import java.lang.management.ManagementFactory;
public class Base {
public static void main(String[] args) {
String name = ManagementFactory.getRuntimeMXBean().getName();
String s = name.split("@")[0];
//打印當(dāng)前Pid
System.out.println("pid:"+s);
while (true) {
try {
Thread.sleep(3000L);
} catch (Exception e) {
break;
}
process();
}
}
public static void process() {
System.out.println("process");
}
}
執(zhí)行字節(jié)碼修改和轉(zhuǎn)換的類:該類中,我們實(shí)現(xiàn)在被修改類前后都輸出start和end語(yǔ)句的方法
public class TestTransformer implements ClassFileTransformer {
@Override
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;
}
}
接著我們生成字節(jié)碼修改的Jar包
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!");
}
}
}
我們將生成的agent.jar通過JVM Tool寫入正在執(zhí)行的Base進(jìn)程中复濒〔甭簦可以看出,本來(lái)只是3秒輸出procss的類變成了從前后輸出start和end的類巧颈,類被成功修改了畦木。
上面字節(jié)碼修改的Demo代碼我放在了自己的Github倉(cāng)庫(kù)中:
https://github.com/qqxx6661/Java_Practise/tree/master/ASMDemo
CGLib動(dòng)態(tài)代理代碼示例
maven引入CGLIB包,然后編寫一個(gè)UserDao類砸泛,它沒有接口十籍,只有兩個(gè)方法,select() 和 update()
public class UserDao {
public void select() {
System.out.println("UserDao 查詢 selectById");
}
public void update() {
System.out.println("UserDao 更新 update");
}
}
編寫一個(gè) LogInterceptor 唇礁,繼承了 MethodInterceptor勾栗,用于方法的攔截回調(diào)
import java.lang.reflect.Method;
import java.util.Date;
public class LogInterceptor implements MethodInterceptor {
/**
* @param object 表示要進(jìn)行增強(qiáng)的對(duì)象
* @param method 表示攔截的方法
* @param objects 數(shù)組表示參數(shù)列表,基本數(shù)據(jù)類型需要傳入其包裝類型盏筐,如int-->Integer围俘、long-Long、double-->Double
* @param methodProxy 表示對(duì)方法的代理琢融,invokeSuper方法表示對(duì)被代理對(duì)象方法的調(diào)用
* @return 執(zhí)行結(jié)果
* @throws Throwable
*/
@Override
public Object intercept(Object object, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
before();
Object result = methodProxy.invokeSuper(object, objects); // 注意這里是調(diào)用 invokeSuper 而不是 invoke界牡,否則死循環(huán),methodProxy.invokesuper執(zhí)行的是原始類的方法漾抬,method.invoke執(zhí)行的是子類的方法
after();
return result;
}
private void before() {
System.out.println(String.format("log start time [%s] ", new Date()));
}
private void after() {
System.out.println(String.format("log end time [%s] ", new Date()));
}
}
測(cè)試類
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); // 設(shè)置超類宿亡,cglib是通過繼承來(lái)實(shí)現(xiàn)的
enhancer.setCallback(daoProxy);
Dao dao = (Dao)enhancer.create(); // 創(chuàng)建代理類
dao.update();
dao.select();
}
}
運(yùn)行結(jié)果和上面相同。
優(yōu)缺點(diǎn)
靜態(tài)代理優(yōu)缺點(diǎn)
- 優(yōu)點(diǎn):可以做到在符合開閉原則的情況下對(duì)目標(biāo)對(duì)象進(jìn)行功能擴(kuò)展纳令。
- 缺點(diǎn):當(dāng)需要代理多個(gè)類的時(shí)候挽荠,由于代理對(duì)象要實(shí)現(xiàn)與目標(biāo)對(duì)象一致的接口,有兩種方式:
- 只維護(hù)一個(gè)代理類泊碑,由這個(gè)代理類實(shí)現(xiàn)多個(gè)接口坤按,但是這樣就導(dǎo)致代理類過于龐大
- 新建多個(gè)代理類,每個(gè)目標(biāo)對(duì)象對(duì)應(yīng)一個(gè)代理類馒过,但是這樣會(huì)產(chǎn)生過多的代理類
JDK動(dòng)態(tài)代理優(yōu)缺點(diǎn)
- 優(yōu)勢(shì):雖然相對(duì)于靜態(tài)代理臭脓,動(dòng)態(tài)代理大大減少了我們的開發(fā)任務(wù),同時(shí)減少了對(duì)業(yè)務(wù)接口的依賴腹忽,降低了耦合度来累。
- 劣勢(shì):只能對(duì)接口進(jìn)行代理
CGLIB動(dòng)態(tài)代理優(yōu)缺點(diǎn)
CGLIB創(chuàng)建的動(dòng)態(tài)代理對(duì)象比JDK創(chuàng)建的動(dòng)態(tài)代理對(duì)象的性能更高,但是CGLIB創(chuàng)建代理對(duì)象時(shí)所花費(fèi)的時(shí)間卻比JDK多得多窘奏。
- 所以對(duì)于單例的對(duì)象嘹锁,因?yàn)闊o(wú)需頻繁創(chuàng)建對(duì)象,用CGLIB合適着裹,反之使用JDK方式要更為合適一些领猾。
- 同時(shí)由于CGLib由于是采用動(dòng)態(tài)創(chuàng)建子類的方法,對(duì)于final修飾的方法無(wú)法進(jìn)行代理。
參考
- 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
補(bǔ)充
裝飾模式與代理模式的區(qū)別
裝飾器模式關(guān)注于在一個(gè)對(duì)象上動(dòng)態(tài)的添加方法摔竿,然而代理模式關(guān)注于控制對(duì)對(duì)象的訪問面粮。
- 使用代理模式的時(shí)候,我們常常在一個(gè)代理類中創(chuàng)建一個(gè)對(duì)象的實(shí)例继低。
- 當(dāng)我們使用裝飾器模式的時(shí)候熬苍,我們通常的做法是將原始對(duì)象作為一個(gè)參數(shù)傳給裝飾者的構(gòu)造器。