Java動態(tài)代理四種實現(xiàn)方式詳解
代理模式也是一種非常常見的設(shè)計模式。了解Spring框架的都知道,Spring AOP 使用的就是動態(tài)代理模式。今天就來系統(tǒng)的重溫一遍代理模式。
在現(xiàn)實生活中代理是隨處可見的,當(dāng)事人因某些隱私不方便出面,或者當(dāng)事人不具備某些相關(guān)的專業(yè)技能,而需要一個職業(yè)人員來完成一些專業(yè)的操作, 也可能由于當(dāng)事人沒有時間處理事務(wù),而聘用代理人出面。而在軟件設(shè)計中,使用代理模式的地方也很多,由于安全原因,屏蔽客戶端直接訪問真實對象, 或者為了提升系統(tǒng)性能,使用代理模式實現(xiàn)延遲加載,還有就是AOP,對委托類的功能進行增強等。
一、代理模式的結(jié)構(gòu)
代理模式的主要參與者有4個,如下圖所示:

| 角色 | 作用 |
|---|---|
| Subject | 主題接口,定義了代理類和委托類的公共對外方法,也是代理類代理委托類的方法 |
| RealSubject | 委托類,真實主題,真正實現(xiàn)業(yè)務(wù)邏輯的類 |
| Proxy | 代理類,代理和封裝委托類 |
| Client | 客戶端,使用代理類和主題接口完成業(yè)務(wù)邏輯 |
loading="lazy" alt="" />角色作用Subject主題接口,定義了代理類和委托類的公共對外方法,也是代理類代理委托類的方法RealSubject委托類,真實主題,真正實現(xiàn)業(yè)務(wù)邏輯的類Proxy代理類,代理和封裝委托類Client客戶端,使用代理類和主題接口完成業(yè)務(wù)邏輯
二、代理模式的實現(xiàn)
代理模式一般分為靜態(tài)代理和動態(tài)代理兩種:
- 靜態(tài)代理,顧名思義,就是提前創(chuàng)建好代理類文件并在程序運行前已經(jīng)編譯成字節(jié)碼。
- 動態(tài)代理,是指在運行時動態(tài)生成代理類,即代理類的字節(jié)碼將在運行時生成并載入到ClassLoader中。
了解了兩種代理模式大概區(qū)別后,接下來就以一個短信發(fā)送功能增強的示例來詳細闡述兩種代理的實現(xiàn)方式。
1、靜態(tài)代理實現(xiàn)
第一步,定義主題接口,該接口只有一個send方法:
public interface ISender {
public boolean send();
}
第二步,定義主題真正實現(xiàn)類:
public class SmsSender implements ISender {
public boolean send() {
System.out.println("sending msg");
return true;
}
}
第三步,創(chuàng)建代理類,封裝實現(xiàn)類:
public class ProxySender implements ISender {
private ISender sender;
public ProxySender(ISender sender){
this.sender = sender;
}
public boolean send() {
System.out.println("處理前");
boolean result = sender.send();
System.out.println("處理后");
return result;
}
}
第四步,客戶端調(diào)用:
@Test
public void testStaticProxy(){
ISender sender = new ProxySender(new SmsSender());
boolean result = sender.send();
System.out.println("輸出結(jié)果:" + result);
}
以上就實現(xiàn)了一個簡單的靜態(tài)代理,很明顯,靜態(tài)代理需要為每個真實主題定義一個形式上完全一樣的封裝類,
如果真實主題方法有所修改,那代理類也需要跟著修改,不利于系統(tǒng)的維護。
2、動態(tài)代理實現(xiàn)
與靜態(tài)代理相比,動態(tài)代理有更多優(yōu)勢,動態(tài)代理不僅不需要定義代理類,甚至可以在運行時指定代理類的執(zhí)行邏輯,從而大大提升系統(tǒng)的靈活性。
目前動態(tài)代理類的生成方法有很多,有JDK自帶的動態(tài)代理、CGLIB、Javassist和ASM庫等。
- JDK動態(tài)代理:內(nèi)置在JDK中,不需要引入第三方j(luò)ar,使用簡單,但功能比較弱。
- CGLIB/Javassist:這兩個都是高級的字節(jié)碼生成庫,總體性能比JDK動態(tài)代理好,且功能強大。
- ASM:低級字節(jié)碼生成工具,近乎使用bytecode編碼,對開發(fā)人員要求最高。當(dāng)然性能也是最好(相比前幾種也不是很大的提升,這里不做具體介紹)。
以下實例依然以SmsSender和ISender作為被代理對象和接口進行試驗。
1) JDK動態(tài)代理
JDK的動態(tài)代理需要實現(xiàn)一個處理方法調(diào)用的Handler,用于實現(xiàn)代理方法的內(nèi)部邏輯,實現(xiàn)InvocationHandler接口。
public class JdkProxyHandler implements InvocationHandler {
private Object target;
public JdkProxyHandler(Object target){
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("處理前");
Object result = method.invoke(target,args);
System.out.println("處理后");
return result;
}
}
客戶端調(diào)用:
@Test
public void testJdkProxy(){
ISender sender = (ISender) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),
new Class[]{ISender.class},
new JdkProxyHandler(new SmsSender()));
boolean result = sender.send();
System.out.println("代理對象:" + sender.getClass().getName());
System.out.println("輸出結(jié)果:" + result);
}
輸出結(jié)果:
處理前
sending msg
處理后
代理對象:com.sun.proxy.$Proxy4
輸出結(jié)果:true
這樣實現(xiàn)一個簡單的AOP就完成了,我們看到代理類的類型是com.sun.proxy.$Proxy4。那JDK是如何創(chuàng)建代理類?
首先從Proxy.newProxyInstance入手,來研究JDK是如何生成代理類:
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
該方法有3個參數(shù):
- loader:用哪個類加載器去加載代理對象,生成目標(biāo)對象的代理需要確保其類加載器相同,所以需要將目標(biāo)對象的類加載器作為參數(shù)傳遞。
- interfaces:代理類需實現(xiàn)的接口列表,JDK動態(tài)代理技術(shù)需要代理類和目標(biāo)對象都繼承自同一接口,所以需要將目標(biāo)對象的接口作為參數(shù)傳遞。
- h:調(diào)用處理器,調(diào)用實現(xiàn)了InvocationHandler類的一個回調(diào)方法,對目標(biāo)對象的增強邏輯在這個實現(xiàn)類中。
具體代碼如下:
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h) throws IllegalArgumentException {
//1.檢查
Objects.requireNonNull(h);
final Class<?>[] intfs = interfaces.clone();
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}
/*
* Look up or generate the designated proxy class.
*/
//獲取代理類類型
Class<?> cl = getProxyClass0(loader, intfs);
/*
* Invoke its constructor with the designated invocation handler.
*/
try {
if (sm != null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}
//通過反射創(chuàng)建代理對象
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
return cons.newInstance(new Object[]{h});
} catch (IllegalAccessException|InstantiationException e) {
throw new InternalError(e.toString(), e);
} catch (InvocationTargetException e) {
Throwable t = e.getCause();
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
throw new InternalError(t.toString(), t);
}
} catch (NoSuchMethodException e) {
throw new InternalError(e.toString(), e);
}
}
總結(jié):具體代碼細節(jié)就不在這里深究,但可以明顯的看出,JDK的動態(tài)代理底層是通過Java反射機制實現(xiàn)的,并且需要目標(biāo)對象繼承自一個接口才能生成它的代理類。
2) CGLIB(Code Generation Library)動態(tài)代理
使用CGLIB動態(tài)代理前需要引入依賴:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
和JDK代理不同,CGLib動態(tài)代理技術(shù)不需要目標(biāo)對象實現(xiàn)自一個接口,只需要實現(xiàn)一個處理代理邏輯的切入類,并實現(xiàn)MethodInterceptor接口。
定義真實主題實現(xiàn)類:
public class BdSender {
public boolean send() {
System.out.println("sending msg");
return true;
}
}
代理類邏輯處理類:
public class CglibProxyInterceptor implements MethodInterceptor {
private Enhancer enhancer = new Enhancer();
/**
* 獲取代理類
* @param clazz
* @return
*/
public Object getProxy(Class clazz) {
enhancer.setSuperclass(clazz);
enhancer.setCallback(this);
return enhancer.create();
}
public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("處理前");
Object result = methodProxy.invokeSuper(object,args);
System.out.println("處理后");
return result;
}
}
客戶端調(diào)用:
@Test
public void testCglibProxy(){
BdSender sender = (BdSender) new CglibProxyInterceptor().getProxy(BdSender.class);
boolean result = sender.send();
System.out.println("代理對象:" + sender.getClass().getName());
System.out.println("輸出結(jié)果:" + result);
}
輸出結(jié)果:
處理前
sending msg
處理后
代理對象:org.yd.proxy.BdSender$$EnhancerByCGLIB$$d65f9e34
輸出結(jié)果:true
總結(jié)CgLib的特點:
- 使用CGLib實現(xiàn)動態(tài)代理,完全不受代理類必須實現(xiàn)接口的限制
- CGLib底層采用ASM字節(jié)碼生成框架,使用字節(jié)碼技術(shù)生成代理類,比使用Java反射效率要高
- CGLib不能對聲明為final的方法進行代理,因為CGLib原理是動態(tài)生成被代理類的子類
3)Javassist動態(tài)代理
Javassist是一個開源的分析、編輯和創(chuàng)建Java字節(jié)碼的類庫,可以直接編輯和生成Java生成的字節(jié)碼。
相對于bcel, asm等這些工具,開發(fā)者不需要了解虛擬機指令,就能動態(tài)改變類的結(jié)構(gòu),或者動態(tài)生成類。
使用avassist動態(tài)代理前需要引入依賴:
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.27.0-GA</version>
</dependency>
使用Javassist生成動態(tài)代理可以有以下兩種方式:
- 代理工廠創(chuàng)建:需要實現(xiàn)
MethodHandler用于代理邏輯處理,實現(xiàn)與CGLib非常類似 - 動態(tài)代碼創(chuàng)建:可通過Java代碼生成字節(jié)碼,這種方式創(chuàng)建的動態(tài)代理非常靈活,甚至可以在運行時生成業(yè)務(wù)邏輯
代理工廠創(chuàng)建 — 代理邏輯處理類
public class JavassistProxyHandler implements MethodHandler {
private ProxyFactory proxyFactory = new ProxyFactory();
/**
* 獲取代理對象
* @param clazz 被代理類
* @return
* @throws Exception
*/
public Object getProxy(Class clazz) throws Exception {
proxyFactory.setSuperclass(clazz);
Class<?> factoryClass = proxyFactory.createClass();
Object proxy = factoryClass.newInstance();
((ProxyObject)proxy).setHandler(this);
return proxy;
}
public Object invoke(Object object, Method method, Method method1, Object[] args) throws Throwable {
System.out.println("處理前");
Object result = method1.invoke(object,args);
System.out.println("處理后");
return result;
}
}
客戶端調(diào)用:
@Test
public void testJavassistProxy() throws Exception {
BdSender sender = (BdSender) new JavassistProxyHandler().getProxy(BdSender.class);
boolean result = sender.send();
System.out.println("代理對象:" + sender.getClass().getName());
System.out.println("輸出結(jié)果:" + result);
}
輸出結(jié)果
處理前
sending msg
處理后
代理對象:org.yd.proxy.BdSender_$$_jvstbce_0
輸出結(jié)果:true
動態(tài)代碼創(chuàng)建 — 代理邏輯處理類:
public static Object getProxy(Class clazz) throws Exception {
ClassPool mPool = ClassPool.getDefault();
CtClass c0 = mPool.get(clazz.getName());
//定義代理類名稱
CtClass mCtc = mPool.makeClass(clazz.getName() + "$$BytecodeProxy");
//添加父類繼承
mCtc.setSuperclass(c0);
//添加類的字段信息
CtField field = new CtField(c0, "real", mCtc);
field.setModifiers(AccessFlag.PRIVATE);
mCtc.addField(field);
//添加構(gòu)造函數(shù)
CtConstructor constructor = new CtConstructor(new CtClass[]{c0},mCtc);
constructor.setBody("{$0.real = $1;}"); // $0代表this, $1代表構(gòu)造函數(shù)的第1個參數(shù)
mCtc.addConstructor(constructor);
//添加方法
CtMethod ctMethod = mCtc.getSuperclass().getDeclaredMethod("send");
CtMethod newMethod = new CtMethod(ctMethod.getReturnType(), ctMethod.getName(),ctMethod.getParameterTypes(), mCtc);
newMethod.setBody("{" +
"System.out.println(\"處理前\");" +
"boolean result = $0.real.send();" +
"System.out.println(\"處理后\");" +
"return result;}");
mCtc.addMethod(newMethod);
//生成動態(tài)類
return mCtc.toClass().getConstructor(clazz).newInstance(clazz.newInstance());
}
客戶端調(diào)用:
@Test
public void testJavassisBytecodetProxy() throws Exception {
BdSender sender = (BdSender) JavassistDynamicCodeProxy.getProxy(BdSender.class);
boolean result = sender.send();
System.out.println("代理對象:" + sender.getClass().getName());
System.out.println("輸出結(jié)果:" + result);
}
輸出結(jié)果:
處理前
sending msg
處理后
代理對象:org.yd.proxy.BdSender$$BytecodeProxy
輸出結(jié)果:true
Javassist被用于struts2和hibernate中,都用來做動態(tài)字節(jié)碼修改使用。一般開發(fā)中不會用到,但在封裝框架時比較有用。
以上介紹了靜態(tài)代理和動態(tài)代理創(chuàng)建的幾種方法與優(yōu)缺點介紹,希望可以幫到大家。
到此這篇關(guān)于Java四種動態(tài)代理實現(xiàn)方式的文章就介紹到這了,更多相關(guān)Java動態(tài)代理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解spring boot 以jar的方式啟動常用shell腳本
本篇文章主要介紹了詳解spring boot 以jar的方式啟動常用shell腳本,具有一定的參考價值,有興趣的可以了解一下2017-09-09
IDEA引MAVEN項目jar包依賴導(dǎo)入問題解決方法
這篇文章主要介紹了IDEA引MAVEN項目jar包依賴導(dǎo)入問題解決,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-11-11
JAVA通過Filter實現(xiàn)允許服務(wù)跨域請求的方法
這里的域指的是這樣的一個概念:我們認(rèn)為若協(xié)議 + 域名 + 端口號均相同,那么就是同域即我們常說的瀏覽器請求的同源策略。這篇文章主要介紹了JAVA通過Filter實現(xiàn)允許服務(wù)跨域請求,需要的朋友可以參考下2018-11-11
Spring Boot中使用Spring-data-jpa實現(xiàn)數(shù)據(jù)庫增刪查改
本篇文章主要介紹了Spring Boot中使用Spring-data-jpa實現(xiàn)增刪查改,非常具有實用價值,需要的朋友可以參考下。2017-03-03

