JDK動態代理的深入理解
引入代理模式
代理模式是框架中經常使用的一種模式,動態代理是AOP(面向切面程式設計)思想的一種重要的實現方式,在我們常用的框架中也經常遇見代理模式的身影,例如在Spring中事務管理就運用了動態代理,它將Service層原先應該進行的事務管理交給了Spring框架,大大簡化了開發流程。在Hibernate中物件的懶載入模式,也運用了JDK的動態代理以及cglib代理。
靜態代理
在說動態代理之前,我們需要先了解一下靜態代理
靜態代理通常用於對原有業務邏輯的擴充。比如持有第三方jar包中的某個類,並呼叫了其中的某些方法。然後出於某種原因,比如記錄日誌、列印方法執行時間,但是又不好將這些邏輯寫入第三方jar包的方法裡。所以可以建立一個代理類實現和這個類方法相同的方法,通過讓代理類持有真實物件,然後在原始碼中呼叫代理類方法,來達到新增我們需要業務邏輯的目的。
這其實也就是代理模式的一種實現,通過對真實物件的封裝,來實現擴充套件性。
代理的三要素
1. 被代理物件實現的介面。
2. 被代理的物件,也稱為目標物件。
3. 代理物件。
這裡我們提出一個需求:我們在編寫的Service類的每個方法中沒有進行事務處理,但根據需要我們必須進行實物處理,那此時我們進行靜態代理。
ServiceInterface介面:
public interface ServiceInterface {
void a();
void b();
}
介面的實現類:
public class ServiceImpl implements ServiceInterface{ @Override public void a() { System.out.println("執行a方法"); } @Override public void b() { System.out.println("執行b方法"); } }
代理類:
public class ServiceImplProxy implements ServiceInterface{ private ServiceInterface service; public ServiceImplProxy(ServiceInterface service) { super(); this.service = service; } @Override public void a() { System.out.println("開啟事務"); service.a(); System.out.println("提交事務"); } @Override public void b() { System.out.println("開啟事務"); service.b(); System.out.println("提交事務"); } }
測試:
public class TestProxy {
public static void main(String[] args) {
//建立目標物件
ServiceInterface service=new ServiceImpl();
//建立代理物件,將目標物件傳入代理物件
ServiceInterface serviceProxy =new ServiceImplProxy(service);
serviceProxy.a();
serviceProxy.b();
}
}
靜態代理的優點及缺點
上面演示了靜態代理的完整過程,現在我們來看看靜態代理的優缺點
優點:擴充套件原功能,不侵入原始碼。
缺點:
1. 如果我們只需要呼叫代理物件的某一個方法,換句話說我們只需要代理物件代理委託類的某一個方法時,我們仍然需要實現介面中所有的方法,這樣顯得很浪費。當然在這裡我們可以通過繼承來實現靜態代理,使用繼承時,我們只需要覆寫需要代理的方法即可。
2. 如果在介面中定義了很多方法,而這些方法都需要被代理,並且代理的邏輯都是相同的,比如說在WEB開發中Service層所有方法執行之前都需要開啟事務,結束後關閉事務。那麼此時我們使用靜態代理,會導致大量程式碼重複(想想如果有50個方法,你去一個一個手敲吧)。
既然靜態代理這麼多缺點,那JDK一定會幫我們解決這個問題,現在我們就來看看JDK自帶的動態代理。
動態代理
動態代理的原理:
動態代理由程式在記憶體中動態生成一個物件,不需要我們手寫代理物件,我們只需要指定代理方法的模板即可。
JDK自帶動態代理的核心類:
java.lang.reflect.Proxy類:
該類中的newInstance的靜態方法,用於建立動態代理物件,該類也是所有生成的動態代理物件的父類。
方法:
Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
引數:
ClassLoader:傳入被代理類的類載入器,實際上只需要傳入類載入器即可,不一定必須傳入“被代理類”的類載入器,也 可以傳入別的例項的類載入器,因為java中類載入器是一個物件。這個類載入器用於將生成的位元組碼檔案載入進方法區, 並生成位元組碼物件。
Class<?>[] interfaces:這個引數是指定代理物件實現的介面,可以實現多個介面,這些介面中的所有方法都會按照invoke模板 中的程式碼進行加強。
InvocationHandler h:傳入InvocationHandler介面的實現類,該類中的invoke方法是代理物件中所有方法的邏輯處理模板。
java.lang.reflect.InvocationHandler介面:
該介面有一個invoke方法,我們需要實現invoke方法,這個方法就是代理方法的模板。
介面中需要實現的方法:
invoke(Object proxy, Method method, Object[] args)
引數:
Object proxy:代表代理物件本身,可以它呼叫代理物件的其他方法
Method method:代表“被代理物件”對應方法的位元組碼物件
Object[] args:傳入“代理物件”對應方法的引數陣列
動態代理的示例程式碼:
編寫InvocationHandler實現類
public class ServiceInvocationHandler implements InvocationHandler {
private Object instance;//這是目標物件(被代理物件)
public Object getInstance() {
return instance;
}
public void setInstance(Object instance) {
this.instance = instance;
}
/***
* proxy:代表代理物件本身,可以通過它呼叫代理物件的其他方法
* method:代表目標物件對應方法的位元組碼物件
* args:代表目標物件相應的引數
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("開啟事務");
method.invoke(instance,args);
System.out.println("提交事務");
return null;
}
}
通過Proxy類建立動態代理
public class TestMyProxy {
public static void main(String[] args) throws Exception{
//建立目標物件
ServiceInterface sale = new ServiceImpl();
//建立模板物件
ServiceInvocationHandler invocationHandler = new ServiceInvocationHandler();
//將目標物件注入模板物件中
invocationHandler.setInstance(sale);
Class serviceClazz = sale.getClass();
ServiceInterface proxy = (ServiceInterface) Proxy.newProxyInstance(serviceClazz.getClassLoader(), serviceClazz.getInterfaces(), invocationHandler);
proxy.a();
proxy.b();
}
}
通過測試,動態代理物件與前面的靜態代理結果相同
生成代理物件的位元組碼檔案
看到這裡肯定很多人都會另一頭霧水,動態代理底層到底是怎麼實現的,我們想辦法通過程式將代理物件位元組碼檔案輸出到磁碟上,然後通過jdgui反編譯工具,檢視動態代理物件的原始碼結構。
public class TestMyProxy {
public static void main(String[] args) throws Exception{
byte[] buffer = ProxyGenerator.generateProxyClass("$Proxy0", new Class[]{ServiceInterface.class});
try {
FileOutputStream output = new FileOutputStream("C:/Users/Lenovo/Desktop/$Proxy0.class");
output.write(buffer);
} catch (Exception e) {
e.printStackTrace();
}
}
}
注:使用ProxyGenerator類需要將JRE中lib目錄下的一個jar包匯入到專案中
通過反編譯工具可以看到代理物件原始碼結構如下(下列程式碼對原始碼進行了部分刪減,如果有需要可以自行生成原始碼檢視):
public final class $Proxy0
extends Proxy
implements ServiceInterface
{
private static Method m1;
private static Method m4;
private static Method m3;
private static Method m2;
private static Method m0;
public $Proxy0(InvocationHandler paramInvocationHandler)
{
super(paramInvocationHandler);
}
public final void b()
{
try
{
this.h.invoke(this, m4, null);
return;
}
catch (Error|RuntimeException localError)
{
throw localError;
}
catch (Throwable localThrowable)
{
throw new UndeclaredThrowableException(localThrowable);
}
}
public final void a()
{
try
{
this.h.invoke(this, m3, null);
return;
}
catch (Error|RuntimeException localError)
{
throw localError;
}
catch (Throwable localThrowable)
{
throw new UndeclaredThrowableException(localThrowable);
}
}
static
{
try
{
m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
m4 = Class.forName("com.proxy.ServiceInterface").getMethod("b", new Class[0]);
m3 = Class.forName("com.proxy.ServiceInterface").getMethod("a", new Class[0]);
m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
return;
}
catch (NoSuchMethodException localNoSuchMethodException)
{
throw new NoSuchMethodError(localNoSuchMethodException.getMessage());
}
catch (ClassNotFoundException localClassNotFoundException)
{
throw new NoClassDefFoundError(localClassNotFoundException.getMessage());
}
}
}
我們分析原始碼可知,代理物件中的代理方法,都執行了this.h.invoke()方法,this.h實際上是代理類的父類中(Proxy類)中的屬性,它儲存了在呼叫Proxy.newInstance()方法時傳入的InvocationHandler例項,而InvocationHandler中的invoke方法就是,代理類的代理邏輯。
也就是說在生成的一個動態代理物件中,對目標物件中所有方法都進行了相同的前處理和後處理過程,因為都執行相同的invoke“模板”方法,也就是說如果我們想對目標物件中每個方法進行不同的前處理和後處理,我們需要編寫不同的InvocationHandler實現類,然後建立不同的動態代理物件才能實現這一需求。
動態代理思想的總結:
動態代理實際上就是在記憶體中直接生成位元組碼檔案(通常程式設計師寫的java檔案編譯後生成的位元組碼檔案都在硬碟中),然後將位元組碼載入進方法區生成位元組碼物件,生成位元組碼物件後通過反射就可以建立代理物件的例項了。生成位元組碼檔案就必定需要生成這個代理類的方法,那程式是怎麼知道這個代理物件需要實現哪些方法呢,這就是為什麼在Proxy.newProxyInstance()需要傳入介面的陣列,傳入幾個介面,這個代理就會實現這些介面,程式自然就知道它需要實現哪些方法了。此時就會開始迴圈生成代理類中的方法,那這個方法的具體實現程式碼又是什麼呢?由於生成的代理類繼承了Proxy類,在Proxy類中有一個h欄位,儲存的是一個InvocationHandler實現類的物件,而我們通過Proxy.newInstance()方法時,傳入了一個InvocationHandler例項,所以在代理物件中存放著一個InvocationHandler物件,代理物件的每個方法都會呼叫父類中存放的InvocationHandler例項中的invoke方法。