背景:
筆者在學校進行實驗室考核時,當時有一個issue是需要我們利用動態代理實現AOP面向切面程式設計記錄紀錄檔。當時造這個小輪子的時候,前前後後花了不少時間,遂總結此文。
本小節你將收穫:
瞭解代理模式、學會如何實現動態代理、深入探究動態代理的實現原理
@Author:Akai-yuan
我們先來舉幾個生活中的例子,幫助你快速理解代理的思想:你的遊戲打得很菜,請代練幫你上分;某男明星開演唱會黃牛賣票;你在公司刪庫跑路請律師幫你打官司。這些都是代理的思想。這些中間商肯定會做一些額外的動作,比如賺點差價。
代理模式就是設定一箇中間代理來控制存取原目標物件,以達到增強原物件的功能和簡化存取方式。
提供了對目標物件額外的存取方式,即通過代理物件存取目標物件,這樣可以在不修改原目標物件的前提下,提供額外的功能操作,擴充套件目標物件的功能。
即使是動態代理也是代理模式,那麼肯定要有一個方法來建立代理物件,下面這個方法就是用於建立代理物件,即Proxy類下的newProxyInstance方法:
public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,
InvocationHandler h) throws IllegalArgumentException
引數介紹:
loader: 一個ClassLoader物件,定義了由哪個ClassLoader物件來對生成的代理物件進行載入,獲取被代理物件的ClassLoader即可(使用class類下的getClassLoader方法)。
interfaces:一個Interface物件的陣列,被代理物件所實現的所有介面。
h: 一個InvocationHandler物件,就是實現InvocationHandler介面的類,表示的是當動態代理物件在呼叫方法的時候,會關聯到哪一個InvocationHandler物件上。
jdk動態代理有關的類主要是Proxy類和InvocationHandler介面,兩者都位於java.lang.reflect包,可見它們都是和反射有關的。關於InvocationHandler介面,他只有一個方法:invoke方法(注意不是Mehod類的invoke方法):
Object invoke(Object proxy, Method method, Object[] args) throws Throwable
引數介紹:
proxy:指代我們所代理的那個真實物件的物件,也就是代理物件
method: 指代的是我們所要呼叫真實物件的某個方法
args:method方法的引數,以陣列形式表示
2.3 編寫實現InvocationHandler介面的類
要動態的建立代理物件的話,我們首先需要編寫一個實現InvocationHandler介面的類,然後重寫其中的invoke方法,其中target是需要被代理的物件(真實物件)。
public class MyProxy implements InvocationHandler {
/**
* 被代理的物件,即真實物件,只需要通過某種方式從本類外部獲取即可
*/
private Object target;
public MyProxy() {
}
/**
*
* @param target 被代理的物件
* @return 返回代理物件
*
* 我們可以通過Proxy類通過的靜態方法newProxyInstance來建立被代理物件(target)的代理物件
* target.getClass().getClassLoader() 獲取被代理物件(target)的類載入器
* target.getClass().getInterfaces() 獲取被代理物件(target)實現的所有介面
* this 實現InvocationHandler介面的自定義類,即本類
*/
public Object getProxy(Object target) {
this.target = target;
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
}
/**
*
* @param proxy 代理物件(代理類的範例)
* @param method 被代理物件需要執行執行的方法
* @param args 方法的引數
* @return 返回被代理物件(target)執行method方法的結果
* @throws Throwable 丟擲異常
*/
@Override
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;
}
}
測試用例:
public interface UserMapper {
void add();
}
public class UserMapperImpl implements UserMapper{
@Override
public void add() {
System.out.println("在UserMapperImpl中,執行了UserMapper的add方法...");
}
}
public static void main(String[] args) {
MyProxy myProxy = new MyProxy();
//userMapper物件為真實物件
UserMapperImpl userMapper = new UserMapperImpl();
//建立代理物件
UserMapper proxy = (UserMapper) myProxy.getProxy(userMapper);
//列印真實物件的Class
System.out.println(userMapper.getClass());
//列印代理物件的Class
System.out.println(proxy.getClass());
proxy.add();
}
下圖可以解釋上述程式碼的呼叫過程:
那麼問題來了:proxy被強制轉化為了UserMapper後,add方法是否是其實現類UserMapperImpl中的add方法?
以下是控制檯輸出結果:
從結果中我們可以看到,建立出來的代理物件proxy的型別是"com.sun.proxy.$Proxy0",$Proxy0代理類在系統內部的編號,它並不是程式編譯之後存在虛擬機器器中的類,而是執行時執行時動態生成類位元組碼,並載入到JVM中,編譯完成後沒有實際對應的class檔案。
正是因為代理物件是執行時臨時生成的,這就區別於靜態代理的代理物件類需要先進行編譯之後才能建立代理物件,這一點是動態代理和靜態代理最大的區別,利用這點,動態代理模式建立代理物件的方式比靜態代理靈活許多。
動態建立代理物件的話需要通過反射代理方法,比較消耗系統效能,但動態代理模式明顯是利大於弊的。
jdk動態代理代理的是介面,其實只要是一個介面,即使它沒有實現類,動態代理還是可以建立出它的代理類的:
public <T> T getProxyInstance(Class<?> proxyInterface) {
Class<?>[] interfaces = null;
Class<?> clazz = null;
//如果是介面
if (proxyInterface.isInterface()) {
clazz = proxyInterface;
interfaces = new Class[]{proxyInterface};
} else {
//如果不是介面則建立一個範例物件
try {
target = proxyInterface.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
logger.severe("MyProxy類利用反射建立範例物件失敗!");
}
clazz = target.getClass();
interfaces = target.getClass().getInterfaces();
}
return (T) Proxy.newProxyInstance(clazz.getClassLoader(), interfaces, this);
}
我們只需要用於一個class陣列接收介面的class即可,這種直接通過介面建立代理物件的應用其中之一就是Mybatis框架,其實我們在service層注入的xxxMapper並不是一個實現類(xxxMapper並沒有實現類),注入的其實是一個xxxMapper的代理物件,如:
@SpringBootTest
class ApplicationTests {
@Resource
UserMapper userMapper;
@Test
void contextLoads() {
System.out.println(userMapper.getClass());
}
}
執行結果:
那麼問題又來了:
jdk動態代理可以只代理介面,沒有實現類也可以,那上面提到的proxy.add()方法執行的就不是UserMapperImpl中的add方法,難道是直接執行UserMapper的add()方法?但是UserMapper是一個介面,它的add方法並沒有執行體,那麼proxy.add()方法到底是哪裡的方法呢?
public class Test {
public static void main(String[] args) {
// 儲存生成的代理類的位元組碼檔案
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
MyProxy myProxy = new MyProxy();
UserMapperImpl userMapper = new UserMapperImpl();
UserMapper proxy = (UserMapper) myProxy.getProxy(userMapper);
System.out.println(userMapper.getClass());
System.out.println(proxy.getClass());
proxy.add();
}
}
我們可以使用"System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");"儲存執行時生成的$Proxy0類:
該類的全部程式碼:
package com.sun.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
import wjh.test.UserMapper;
public final class $Proxy0 extends Proxy implements UserMapper {
private static Method m1;
private static Method m2;
private static Method m3;
private static Method m0;
public $Proxy0(InvocationHandler var1) throws {
super(var1);
}
public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final void add() throws {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m2 = Class.forName("java.lang.Object").getMethod("toString");
m3 = Class.forName("wjh.test.UserMapper").getMethod("add");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
通過觀察發現該類繼承了Proxy類和實現了UserMapper介面,並且有一個方法m1,m2,m3,m0
繼續觀察發現這四個方法其實是通過反射獲取的,其中equals、toString、hashCode方法都是Object類的方法,而其中的m3則是獲取的UserMapper介面的add方法。
然後當執行$Proxy0的add方法時,執行了程式碼的"super.h.invoke(this, m3, (Object[])null);",這句話我們現在可能看不懂,但從字面上我們可以通過「invoke」和帶括號的三個引數型別來推測這個「invoke」一定是某個類的某個方法,只是我們不清楚這個類是哪個類,這個方法是哪個方法。
那麼接下來我們繼續思考:
$Proxy0的add方法執行的是"super.h.invoke(this, m3, (Object[])null);",super可以理解是它的父類別Proxy類,那super.h的h又是什麼呢?h.invoke又是什麼呢?我們看看這三個引數"this、m3、(Object[])null",它們對於的型別依次是Object、Method、Object[],這三個引數是否感覺到有那麼一點點的眼熟,是否感覺好像在哪裡見過?
下面就讓我們看看這個感覺見過,但又想不起來的東西:
可以看到其中的invoke方法的三個引數型別依次也是:Object、Method、Object[],和上面的「super.h.invoke(this, m3, (Object[])null);」引數型別一樣,而且方法名也都是invoke,這時巧合還是必然?如果是必然的,那麼我們就可以推測出:
呼叫$Proxy0的add方法會進入MyProxy的invoke方法後先執行前置附加操作;然後執行「method.invoke(target, args)」,其中的targe就是被代理的物件UserMapperImpl,method就是則是前面通過反射獲取的m3,即UserMapper介面的add方法(由於UserMapperImpl實現於UserMapper,所以執行的其實是target實現的add方法);最後再執行後置附加操作並返回結果。
那麼這個推測是否正確,暫時不清楚,我們需要接著往下看。
思考:
我們上面通過「System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");」這句程式碼已經瞭解到了$Proxy0類是什麼、有什麼,但是我們是不是忽略了這個類到底是怎麼來的呢?
大致過程:
直觀圖:
我們知道$Proxy0物件(即代理物件)是通過Proxy的newProxyInstance方法建立的,既然要知道這個物件怎麼來的,那麼就必須要閱讀這個方法的原始碼了,在閱讀原始碼之前,我們先回顧一下我們怎麼呼叫了這個方法:
/**
*
* @param target 被代理的物件
* @return 返回代理物件
*
* 我們可以通過Proxy類通過的靜態方法newProxyInstance來建立被代理物件(target)的代理物件
* target.getClass().getClassLoader() 獲取被代理物件(target)的類載入器
* target.getClass().getInterfaces() 獲取被代理物件(target)實現的所有介面
* this 實現InvocationHandler介面的自定義類,即本類
*/
public Object getProxy(Object target) {
this.target = target;
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
}
接著我們看一下原始碼,下面就是Proxy類的newProxyInstance方法方法的原始碼:
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
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);
}
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);
}
}
首先需要對傳進來的InvocationHandler類(即我們這裡的MyProxy類)進行判空,因為後續使用代理物件$Proxy0呼叫的方法都是MyProxy類類的invoke方法,所以這個類是不可以傳空值進來的。
其中checkProxyAccess的作用是檢查是否有生成代理類的許可權。
然後是下圖的方法,它的作用是查詢或生成代理類$Proxy0,需要的引數是真正物件的類載入器和實現的介面,可見這個方法是重中之重,我們再後續介紹
前面提到的一個「getProxyClass0(loader, intfs)」還沒有結束,只是知道了它的作用是建立一個臨時代理類(即$Proxy0)或者查詢已經存在虛擬機器器中的代理類。下面是該方法的原始碼:
結果我們發現它又呼叫了「proxyClassCache.get(loader, interfaces)」方法,經查閱資料:瞭解到「proxyClassCache」是快取,其目的是為了複用,同時防止多執行緒重複建立同一個代理類。大家可以點進這個get方法的原始碼檢視這個快取機制。
如果快取中沒有代理類,那麼就會生成一個新的代理類,新的代理類是在上面的ProxyClassFactory中生成的,這個類裡面有一個apply方法,它返回的就是代理類$Proxy0,但是這個方法其實也只是做了一些表面工作:為代理類起名、對傳入的介面陣列infs做一些校驗,對一些需要生成代理類的引數進行判空...... 而真正生成代理類的方法是這個方法裡面呼叫的「ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags)」方法,它的返回值是二進位制陣列,在介紹這個方法之前,我們有必要簡單瞭解位元組碼檔案的結構:
位元組碼檔案其實是一個Java官方嚴格規定位元組數、格式的二進位制檔案,每一個位元組都具有特殊的含義,比如前面4個位元組:CA FE BA BE表示魔值,魔值代表它是否為一個Class檔案、00 00代表Java的此版本號、00 34代表主版本號,這裡就不一一介紹了,暫時只需要指定它的每個位元組都有著特殊的含義即可。位元組碼檔案儲存的一個類的所有資訊:版本、屬性、方法......
而我們下面介紹的_generateProxyClass_方法就是按照位元組碼檔案的規範一點一點的使用二進位制來組成一個二進位制陣列並返回。
private class ProxyMethod {
public String methodName;
public Class<?>[] parameterTypes;
public Class<?> returnType;
public Class<?>[] exceptionTypes;
public Class<?> fromClass;
public String methodFieldName;
}
private Map<String, List<ProxyGenerator.ProxyMethod>> proxyMethods = new HashMap();
private byte[] generateClassFile() {
/*----------------第一步:將所有的方法組裝成ProxyMethod物件---------------*/
//首先為代理類生成toString, hashCode, equals等代理方法
this.addProxyMethod(hashCodeMethod, Object.class);
this.addProxyMethod(equalsMethod, Object.class);
this.addProxyMethod(toStringMethod, Object.class);
Class[] var1 = this.interfaces;
int var2 = var1.length;
int var3;
Class var4;
//遍歷每一個介面的每一個方法, 並且為其生成ProxyMethod物件
for(var3 = 0; var3 < var2; ++var3) {
var4 = var1[var3];
Method[] var5 = var4.getMethods();
int var6 = var5.length;
for(int var7 = 0; var7 < var6; ++var7) {
Method var8 = var5[var7];
this.addProxyMethod(var8, var4);
}
}
Iterator var11 = this.proxyMethods.values().iterator();
//對於具有相同簽名的代理方法, 檢驗方法的返回值是否相容
List var12;
while(var11.hasNext()) {
var12 = (List)var11.next();
checkReturnTypes(var12);
}
/*-------------第二步:組裝要生成的class檔案的所有的欄位資訊和方法資訊-------*/
Iterator var15;
try {
//為代理類新增構造器方法
this.methods.add(this.generateConstructor());
var11 = this.proxyMethods.values().iterator();
//遍歷快取中的代理方法
while(var11.hasNext()) {
var12 = (List)var11.next();
var15 = var12.iterator();
while(var15.hasNext()) {
ProxyGenerator.ProxyMethod var16 = (ProxyGenerator.ProxyMethod)var15.next();
//新增代理類的靜態欄位, 例如:private static Method m1;
this.fields.add(new ProxyGenerator.FieldInfo(var16.methodFieldName, "Ljava/lang/reflect/Method;", 10));
//新增代理類的代理方法
this.methods.add(var16.generateMethod());
}
}
//新增代理類的靜態欄位初始化方法
this.methods.add(this.generateStaticInitializer());
} catch (IOException var10) {
throw new InternalError("unexpected I/O Exception", var10);
}
/* -----------------------第三步:寫入最終的class檔案---------------------*/
//驗證方法和欄位集合不能大於65535
if (this.methods.size() > 65535) {
throw new IllegalArgumentException("method limit exceeded");
} else if (this.fields.size() > 65535) {
throw new IllegalArgumentException("field limit exceeded");
} else {
//驗證常數池中存在代理類的全限定名
this.cp.getClass(dotToSlash(this.className));
//驗證常數池中存在代理類父類別的全限定名, 父類別名為:"java/lang/reflect/Proxy"
this.cp.getClass("java/lang/reflect/Proxy");
var1 = this.interfaces;
var2 = var1.length;
//驗證常數池存在代理類介面的全限定名
for(var3 = 0; var3 < var2; ++var3) {
var4 = var1[var3];
this.cp.getClass(dotToSlash(var4.getName()));
}
//接下來要開始寫入檔案了,設定常數池唯讀
this.cp.setReadOnly();
//建立位元組陣列輸出流
ByteArrayOutputStream var13 = new ByteArrayOutputStream();
DataOutputStream var14 = new DataOutputStream(var13);
try {
//1.寫入魔數
var14.writeInt(-889275714);
//2.寫入次版本號
var14.writeShort(0);
//3.寫入主版本號
var14.writeShort(49);
//4.寫入常數池
this.cp.write(var14);
//5.寫入存取修飾符
var14.writeShort(this.accessFlags);
//6.寫入類索引
var14.writeShort(this.cp.getClass(dotToSlash(this.className)));
//7.寫入父類別索引, 生成的代理類都繼承自Proxy
var14.writeShort(this.cp.getClass("java/lang/reflect/Proxy"));
//8.寫入介面計數值
var14.writeShort(this.interfaces.length);
Class[] var17 = this.interfaces;
int var18 = var17.length;
//9.寫入介面集合
for(int var19 = 0; var19 < var18; ++var19) {
Class var22 = var17[var19];
var14.writeShort(this.cp.getClass(dotToSlash(var22.getName())));
}
//10.寫入欄位計數值
var14.writeShort(this.fields.size());
var15 = this.fields.iterator();
//11.寫入欄位集合
while(var15.hasNext()) {
ProxyGenerator.FieldInfo var20 = (ProxyGenerator.FieldInfo)var15.next();
var20.write(var14);
}
//12.寫入方法計數值
var14.writeShort(this.methods.size());
var15 = this.methods.iterator();
//13.寫入方法集合
while(var15.hasNext()) {
ProxyGenerator.MethodInfo var21 = (ProxyGenerator.MethodInfo)var15.next();
var21.write(var14);
}
//14.寫入屬性計數值, 代理類class檔案沒有屬性所以為0
var14.writeShort(0);
//轉換成二進位制陣列並返回
return var13.toByteArray();
} catch (IOException var9) {
throw new InternalError("unexpected I/O Exception", var9);
}
}
}
在得到二進位制陣列以後,apply方法返回:「return defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length);」 deffine方法的作用就是將前面得到的位元組碼,將其處理成真正的Java類,這樣一個動態生成代理類$Proxy0就被建立出來了。
第729行的「cl.getConstructor(constructorParams)」則是獲取代理類$Proxy0的構造器,引數是「constructorParams」:
其中引數constructorParms是一個Class[]陣列,只有一個元素:InvocationHandler.class
再回到我們上面看到的$Proxy0的內部,它的構造器只有一個:
可以看到$Proxy0的構造器呼叫了父類別Proxy類的構造器,這個構造方法中把h即MyProxy類賦值給了 Proxy類的成員變數h,至此我們證明了$Proxy0類中的「super.h」就是Proxy類的成員變數h,也就是我們的MyProxy類了,那麼「super.h.invoke(this, m3, (Object[])null);」就可以完全證實就是MyProxy類的invoke方法了,那麼前面的假設就是正確的了。
至此,"proxy.add()"的謎團已經完全解開了:
main方法中呼叫「proxy.add()」實際呼叫的是實現UserMapper、繼承Proxy類的$Proxy0類中的同名add方法,而這個同名的add方法又是呼叫的MyProxy類的invoke方法,而invoke方法中呼叫的又是通過反射呼叫的真實物件UserMapperImpl(即MyProxy類的target屬性)的add方法,而再呼叫前後都可以做一些附加操作,之後把返回值返回給$Proxy0的add方法,$Proxy0的add方法又把這個返回值返回main方法,這就完成了整個動態代理過程中方法的呼叫。如下圖所示:
其中newInstance方法的引數可以再次清楚的看到用的是我們傳過來的實現InvocationHandler類的MyProxy類,到這裡$Proxy0類的範例物件就被建立出來了。
綜上,動態代理主要可以歸納為3個步驟:
這就是為什麼newProxyInstance方法需要三個引數的原因:
第一個引數是類載入器,用於代理類的位元組碼檔案到jvm
第二個引數是真實物件實現的全部介面,因為動態生成代理類$Proxy0也需要實現這些介面
第三個引數是InvocationHandler 物件.,在代理類$Proxy0和實現的介面的同名方法中需要呼叫這個類中的invoke方法,然後這個方法反射呼叫Method類的invoke方法