淺談Java動(dòng)態(tài)代理的實(shí)現(xiàn)
一、代理設(shè)計(jì)模式
1.1 什么是代理
- 考慮真實(shí)的編程場(chǎng)景,項(xiàng)目中存在一個(gè)訪問(wèn)其他數(shù)據(jù)源的接口,包含一個(gè)
query()方法 - 我們已經(jīng)針對(duì)這個(gè)接口,實(shí)現(xiàn)了MySQL、Hive、HBase、MongoDB等作為數(shù)據(jù)源的實(shí)現(xiàn)類
- 但是,在測(cè)試過(guò)程中,我們發(fā)現(xiàn)這些數(shù)據(jù)源的查詢并不是很穩(wěn)定
- 最原始的想法: 在所有實(shí)現(xiàn)類
query()方法中,代碼首部獲取startTime,代碼尾部獲取endTime,通過(guò)打印日志的方式,知道每次查詢的耗時(shí)
long startTime = System.currentTimeMillis();
logger.info("query mysql start:" + new Date(startTime).toLocaleString());
// 具體的query代碼
...
long endTime = System.currentTimeMillis();
logger.info(String.format("query mysql end: %s, consumed time: %dms", new Date(endTime).toLocaleString(), (endTime-startTime)));
- 直接修改已經(jīng)實(shí)現(xiàn)的方法,存在很多缺點(diǎn):
(1)現(xiàn)在是打印日志,代碼非常簡(jiǎn)單,就算query()方法不是你實(shí)現(xiàn)的,你也能很好的完成。
(2)但是如果是其他功能呢?比如,如果查詢失敗,要求你查詢重試
- 因此,在不改變已經(jīng)實(shí)現(xiàn)好的
query()方法前提下,去實(shí)現(xiàn)日志打印的功能是最好的方法。 - 進(jìn)階想法: 我為每個(gè)實(shí)現(xiàn)類創(chuàng)建一個(gè)
包裝類。
(1)這個(gè)包裝類與實(shí)現(xiàn)類一樣,實(shí)現(xiàn)了相同的接口。
(2)在query()方法中,直接調(diào)用實(shí)現(xiàn)類的query()方法,并在調(diào)用前后進(jìn)行日志打印
(3)對(duì)實(shí)現(xiàn)類方法的調(diào)用,都改成對(duì)包裝類方法的調(diào)用
long startTime = System.currentTimeMillis();
logger.info("query mysql start:" + new Date(startTime).toLocaleString());
// 使用try-finally
JSONObject[] data = null;
try {
data = mysql.query();
return data;
} catch (Exception exception) {
throw exception;
} finally {
long endTime = System.currentTimeMillis();
logger.info(String.format("query mysql end: %s, consumed time: %dms", new Date(endTime).toLocaleString(), (endTime - startTime)));
}
- 這時(shí),代理模式的概念就變得非常清晰了:不直接調(diào)用實(shí)現(xiàn)類的某個(gè)方法,而是通過(guò)實(shí)現(xiàn)類的代理去調(diào)用。
- 這樣不僅可以實(shí)現(xiàn)調(diào)用者與被調(diào)用者之間的解耦合,還可以在不修改調(diào)用者的情況下,豐富功能邏輯。
1.2 代理模式入門
代理模式的UML圖如下
1.subject: 抽象主題角色,是一個(gè)接口,定義了一系列的公共對(duì)外方法
2.real subject: 真實(shí)主題角色,也就是我剛剛提到的實(shí)現(xiàn)類,又稱委托類。
委托類實(shí)現(xiàn)抽象主題,負(fù)責(zé)實(shí)現(xiàn)具體的業(yè)務(wù)邏輯
3.proxy: 代理主題角色,簡(jiǎn)稱代理類。它也實(shí)現(xiàn)了抽象主題,用于代理、封裝,甚至增強(qiáng)委托類。
一般通過(guò)內(nèi)含委托類,實(shí)現(xiàn)對(duì)委托類的封裝
4.client: 當(dāng)訪問(wèn)具體的業(yè)務(wù)邏輯時(shí),clinet表面是訪問(wèn)代理類,而實(shí)際是訪問(wèn)被代理類封裝的委托類
代理模式的應(yīng)用場(chǎng)景:目前,就我本人所接觸的使用場(chǎng)景,就是通過(guò)代理去打印日志、增強(qiáng)業(yè)務(wù)邏輯 😂
二、Java代理的三種實(shí)現(xiàn)
2.1 靜態(tài)代理
- 所謂的靜態(tài)和動(dòng)態(tài),是相對(duì)于字節(jié)碼的生成時(shí)機(jī)來(lái)說(shuō)的:
(1)靜態(tài)是指字節(jié)碼,即class文件,在編譯時(shí)就已經(jīng)生成。
(2)動(dòng)態(tài)是指字節(jié)碼在運(yùn)行時(shí)動(dòng)態(tài)生成,而不是編譯時(shí)提前生成
- 剛剛,我們針對(duì)數(shù)據(jù)查詢的進(jìn)階方法,實(shí)際就是靜態(tài)代理
- 通過(guò)為每個(gè)委托類創(chuàng)建對(duì)應(yīng)的代理類,然后編譯時(shí)就可以得到代理類的字節(jié)碼
下面是一個(gè)具體的靜態(tài)代理實(shí)例:
抽象主題:
public interface Animal {
void eat();
}
委托類:Dog和Cat,實(shí)現(xiàn)了抽象接口
public class Dog implements Animal {
@Override
public void eat() {
System.out.println("I like eating bone");
}
}
public class Cat implements Animal {
@Override
public void eat() {
System.out.println("I like eating fish");
}
}
代理類:代理類中含有對(duì)應(yīng)的委托類,通過(guò)調(diào)用委托類的具體實(shí)現(xiàn),來(lái)封裝委托類
public class DogProxy implements Animal {
private Dog dog;
public DogProxy(Dog dog) {
this.dog = dog;
}
@Override
public void eat() {
System.out.print("I'm a "+dog.getClass().getSimpleName() +". ");
dog.eat();
}
}
public class CatProxy implements Animal {
private Cat cat;
public CatProxy(Cat cat) {
this.cat = cat;
}
@Override
public void eat() {
System.out.print("I'm a " + cat.getClass().getSimpleName()+". ");
cat.eat();
}
}
靜態(tài)代理雖然實(shí)現(xiàn)簡(jiǎn)單、不更改原始的業(yè)務(wù)邏輯,但是仍然存在以下缺點(diǎn):
1.如果存在多個(gè)委托類,則需要?jiǎng)?chuàng)建多個(gè)代理類,這樣則會(huì)產(chǎn)生過(guò)多的代理類。
2.如果抽象主題增加、刪除、修改方法時(shí),委托類和代理類都需要同時(shí)修改,不易維護(hù)。
2.2 Java自帶的動(dòng)態(tài)代理
- 靜態(tài)代理存在以上缺點(diǎn),如果我們能在程序運(yùn)行時(shí),動(dòng)態(tài)生成對(duì)應(yīng)的代理類,則無(wú)需創(chuàng)建并維護(hù)過(guò)多的代理類
- 使用Java自帶的
java.lang.reflect.Proxy和java.lang.reflect.InvocationHandler類,可以實(shí)現(xiàn)動(dòng)態(tài)代理 - 從類的全路徑可以看出,Java的動(dòng)態(tài)代理實(shí)際利用的是反射機(jī)制實(shí)現(xiàn)的
- 其中,
Proxy用于創(chuàng)建對(duì)應(yīng)接口的代理類。具體代理的是哪個(gè)委托類,是由實(shí)現(xiàn)InvocationHandler接口的中介類決定的 - 代理類、委托類、中介類之間的關(guān)系如下

Java自帶的動(dòng)態(tài)代理具體實(shí)現(xiàn):
1.創(chuàng)建抽象主題 —— 與靜態(tài)代理類一致,不再展示
2.創(chuàng)建實(shí)現(xiàn)類 —— 與靜態(tài)代理類一致,不再展示
3.實(shí)現(xiàn)InvocationHandler接口,創(chuàng)建中介類
public class AnimalInvokeHandler implements InvocationHandler {
private Animal animal;
public AnimalInvokeHandler(Animal animal) {
this.animal = animal;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("proxy class: " + proxy.getClass().getName());
System.out.printf("proxy instanceof Animal: %b \n", proxy instanceof Animal);
System.out.printf("---- Call method: %s, class: %s ----\n", method.getName(), animal.getClass().getSimpleName());
// 通過(guò)反射,調(diào)用委托類的方法
Object result = method.invoke(animal, args);
return result;
}
}
4.通過(guò)Proxy創(chuàng)建動(dòng)態(tài)代理類,實(shí)現(xiàn)對(duì)抽象主題的代理
public static void main(String[] args) {
// 通過(guò)Proxy.newProxyInstance創(chuàng)建代理類
// 將代理類轉(zhuǎn)為抽象主題,可以動(dòng)態(tài)的創(chuàng)建實(shí)現(xiàn)了該主題的代理類
// 必須從實(shí)現(xiàn)類去獲取需要代理的接口
// 指定中介類,通過(guò)中介類實(shí)現(xiàn)代理
Animal proxy = (Animal) Proxy.newProxyInstance(Main.class.getClassLoader(), Dog.class.getInterfaces(), new AnimalInvokeHandler(new Dog()));
proxy.eat();
}
執(zhí)行結(jié)果:

Java原生的動(dòng)態(tài)代理,利用反射動(dòng)態(tài)生成代理類字節(jié)碼ProxyX.class,然后將其強(qiáng)制轉(zhuǎn)化為抽象主題類型,就能實(shí)現(xiàn)對(duì)該接口的代理
jdk動(dòng)態(tài)代理之所以只能代理接口是因?yàn)榇眍惐旧硪呀?jīng)extends了Proxy,而java是不允許多重繼承的,但是允許實(shí)現(xiàn)多個(gè)接口
Java原生動(dòng)態(tài)代理,又叫jdk動(dòng)態(tài)代理,具有以下優(yōu)缺點(diǎn)
1.優(yōu)點(diǎn): jdk動(dòng)態(tài)代理,避免了靜態(tài)代理需要?jiǎng)?chuàng)建并維護(hù)過(guò)多的代理類的
2.缺點(diǎn): jdk動(dòng)態(tài)代理只能代理接口,因?yàn)镴ava的單繼承原則:代理類本身已經(jīng)繼承了Proxy類,就不能再繼承其他類,只能實(shí)現(xiàn)委托類的抽象主題接口。
2.3 cglib實(shí)現(xiàn)動(dòng)態(tài)代理
- jdk動(dòng)態(tài)代理存在只能代理接口的問(wèn)題,是十分不方便的。
- 考慮以下場(chǎng)景:
一個(gè)類中有兩個(gè)方法methodA:轉(zhuǎn)賬到其他賬戶,methodB:查詢賬戶余額。
如果用戶訪問(wèn)methodA,希望先提示用戶檢查賬戶信息是否正確;
如果用戶訪問(wèn)methodB,希望在用戶查詢完余額后,提示用戶關(guān)注銀行的微信公眾號(hào)。
- 上述類已經(jīng)成功用于業(yè)務(wù)場(chǎng)景了,我們想要實(shí)現(xiàn)這些增強(qiáng)功能,最好不要更改其原始代碼,而是通過(guò)代理實(shí)現(xiàn)功能增強(qiáng)
- 這時(shí),便可以使用
cglib實(shí)現(xiàn)對(duì)類的動(dòng)態(tài)代理 —— 小白看了實(shí)現(xiàn)后,可能會(huì)傾向于說(shuō)這就是類似Java web的攔截器 😂
三、cglib動(dòng)態(tài)代理的實(shí)現(xiàn)
添加maven依賴
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.11</version>
</dependency>
簡(jiǎn)化版的銀行系統(tǒng)類
public class BankSystem {
// 轉(zhuǎn)賬
public boolean transferAccount(double amount, String address) {
System.out.printf("Send %f dollars to account %s", amount, address);
// 轉(zhuǎn)賬成功
return true;
}
// 查詢賬戶余額
public String queryBalance(){
System.out.printf("Query account balance success");
return "Account balance: 2400 dollars";
}
}
為轉(zhuǎn)賬方法創(chuàng)建攔截器
public class TransferInterceptor implements MethodInterceptor {
/**
*
* @param o,表示要增強(qiáng)的對(duì)象,其實(shí)就是代理中的委托類
* @param method,被攔截的方法,其實(shí)就是委托類中被代理的方法
* @param objects,被攔截方法的入?yún)ⅲ绻腔緮?shù)據(jù)類型需要傳入包裝類型
* @param methodProxy,對(duì)method的代理,通過(guò)invokeSuper(),實(shí)現(xiàn)對(duì)method的調(diào)用
* @return java.lang.Object
* @author sunrise
* @date 2021/5/23 10:43 上午
**/
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
before(objects);
Object result = methodProxy.invokeSuper(o, objects);
return result;
}
private void before(Object[] args){
System.out.printf("Please check: you will send %.2f dollar to account %s.\n", args[0], args[1]);
}
}
為余額查詢方法創(chuàng)建攔截器
public class QueryBalanceInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
// 調(diào)用預(yù)發(fā)插敘方法
Object result = proxy.invokeSuper(obj, args);
after();
return result;
}
private void after() {
System.out.printf("Please pay attention to WeChat official account.\n");
}
}
創(chuàng)建filter,實(shí)現(xiàn)方法與攔截器的映射
public class BankFilter implements CallbackFilter {
@Override
public int accept(Method method) {
if ("transferAccount".equals(method.getName())) {
// 使用攔截器列表中的第1個(gè)攔截器
return 0;
}
// 使用攔截器列表中的第2個(gè)攔截器
return 1;
}
}
使用cglib實(shí)現(xiàn)動(dòng)態(tài)代理 —— 我認(rèn)為就是方法攔截 😂
public static void main(String[] args) {
// 新建攔截器
TransferInterceptor transferInterceptor = new TransferInterceptor();
QueryBalanceInterceptor queryBalanceInterceptor = new QueryBalanceInterceptor();
// 創(chuàng)建工具類
Enhancer enhancer = new Enhancer();
// 設(shè)置委托類,在cglib中,這是cglib需要繼承的超類
enhancer.setSuperclass(BankSystem.class);
// 設(shè)置多個(gè)攔截器
enhancer.setCallbacks(new Callback[]{transferInterceptor, queryBalanceInterceptor});
// 實(shí)現(xiàn)攔截器和方法的映射,即為不同的方法配置不同的攔截器
enhancer.setCallbackFilter(new BankFilter());
// 創(chuàng)建代理類
BankSystem proxy = (BankSystem) enhancer.create();
// 執(zhí)行轉(zhuǎn)賬,調(diào)用TransferInterceptor
boolean ok = proxy.transferAccount(1024.28, "lucy");
System.out.println("transfer money success: " + ok);
// 查詢余額,調(diào)用QueryBalanceInterceptor
proxy.queryBalance();
}
cglib總結(jié):
1.優(yōu)點(diǎn)1: cglib基于類實(shí)現(xiàn)動(dòng)態(tài)代理,通過(guò)ASM字節(jié)碼框架動(dòng)態(tài)生成委托類的子類,并使用方法攔截器實(shí)現(xiàn)對(duì)委托類方法的攔截。
2.優(yōu)點(diǎn)2: 基于ASM字節(jié)碼框架動(dòng)態(tài)生成代理類,比jdk動(dòng)態(tài)生成代理類更加高效
3.缺點(diǎn): 通過(guò)繼承委托類創(chuàng)建動(dòng)態(tài)代理類,因此不能代理final委托類或委托類中的final方法。
四、面試常見(jiàn)問(wèn)題
java中代理的實(shí)現(xiàn)
共有三種方法:靜態(tài)代理、JDK動(dòng)態(tài)代理、cglib動(dòng)態(tài)代理
- 靜態(tài)代理: 為每個(gè)實(shí)現(xiàn)類都創(chuàng)建一個(gè)對(duì)應(yīng)的代理類,需要?jiǎng)?chuàng)建并維護(hù)大量的代理類
- jdk動(dòng)態(tài)代理: 通過(guò)
Proxy.newProxyInstance()為抽象主題創(chuàng)建代理類,被代理的委托類包含在InvocationHandler類中,由InvocationHandler類的invoke方法通過(guò)反射實(shí)現(xiàn)對(duì)委托類方法的調(diào)用 - cglib動(dòng)態(tài)代理:通過(guò)ASM字節(jié)碼框架,繼承委托類以創(chuàng)建代理類。代理類通過(guò)方法攔截器,實(shí)現(xiàn)對(duì)委托類方法的攔截
三種代理方式的比較
1.靜態(tài)代理,需要?jiǎng)?chuàng)建并維護(hù)大量的委托類
2.jdk動(dòng)態(tài)代理,避免了靜態(tài)類的上述缺點(diǎn),但只能代理接口(Java單繼承原則,代理類已經(jīng)繼承了Proxy類)
3.cglib動(dòng)態(tài)代理,可以實(shí)現(xiàn)對(duì)類的代理,并通過(guò)方法攔截器實(shí)現(xiàn)對(duì)委托類(父類)方法的攔截;使用強(qiáng)大的ASM字節(jié)碼框架,更加高效;通過(guò)繼承實(shí)現(xiàn)對(duì)類的代理,使其無(wú)法代理final類或類中的final方法
為何調(diào)用代理類的方法,會(huì)自動(dòng)進(jìn)入InvocationHandler的invoke()方法?
- 通過(guò)
newProxyInstance()創(chuàng)建代理類時(shí),會(huì)為代理類設(shè)置InvocationHandler:h - 動(dòng)態(tài)生成的代理類字節(jié)碼,通過(guò)反編譯可以發(fā)現(xiàn),它實(shí)現(xiàn)了抽象主題中的每個(gè)方法
- 方法的實(shí)現(xiàn),是調(diào)用內(nèi)部成員
h.invoke()方法
this.h.invoke(this, method, args);
因此,調(diào)用代理類的方法時(shí),實(shí)際上會(huì)調(diào)用InvocationHandler的invoke()方法
到此這篇關(guān)于淺談Java動(dòng)態(tài)代理的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)Java動(dòng)態(tài)代理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于ThreadPoolTaskExecutor的使用說(shuō)明
這篇文章主要介紹了基于ThreadPoolTaskExecutor的使用說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11
從java反編譯及字節(jié)碼角度探索分析String拼接字符串效率
這篇文章主要介紹了從java反編譯及字節(jié)碼角度探索分析String拼接字符串效率,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12
Java實(shí)戰(zhàn)之仿天貓商城系統(tǒng)的實(shí)現(xiàn)
這篇文章主要介紹了如何利用Java制作一個(gè)基于SSM框架的迷你天貓商城系統(tǒng),文中采用的技術(shù)有JSP、Springboot、SpringMVC、Spring等,需要的可以參考一下2022-03-03
spring-data-redis自定義實(shí)現(xiàn)看門狗機(jī)制
redission看門狗機(jī)制是解決分布式鎖的續(xù)約問(wèn)題,本文主要介紹了spring-data-redis自定義實(shí)現(xiàn)看門狗機(jī)制,具有一定的參考價(jià)值,感興趣的可以了解一下2024-03-03
使用Java動(dòng)態(tài)創(chuàng)建Flowable會(huì)簽?zāi)P偷氖纠a
動(dòng)態(tài)創(chuàng)建流程模型,尤其是會(huì)簽(Parallel Gateway)模型,是提升系統(tǒng)靈活性和響應(yīng)速度的關(guān)鍵技術(shù)之一,本文將通過(guò)Java編程語(yǔ)言,深入探討如何在運(yùn)行時(shí)動(dòng)態(tài)地創(chuàng)建包含會(huì)簽環(huán)節(jié)的Flowable流程模型,需要的朋友可以參考下2024-05-05
SpringMVC中@RequestMapping注解的實(shí)現(xiàn)
RequestMapping是一個(gè)用來(lái)處理請(qǐng)求地址映射的注解,本文主要介紹了SpringMVC中@RequestMapping注解的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2024-01-01
springboot+dynamicDataSource動(dòng)態(tài)添加切換數(shù)據(jù)源方式
這篇文章主要介紹了springboot+dynamicDataSource動(dòng)態(tài)添加切換數(shù)據(jù)源方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01
JSch教程使用sftp協(xié)議實(shí)現(xiàn)服務(wù)器文件載操作
這篇文章主要為大家介紹了JSch如何使用sftp協(xié)議實(shí)現(xiàn)服務(wù)器文件上傳下載操作,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2022-03-03

