Java 動態代理
Java反射提供了一種類動態代理機制,可以通過代理接口實現類來完成程序無侵入式擴展。
Java動態代理主要使用場景:
- 統計方法執行所耗時間。
- 在方法執行前后添加日志。
- 檢測方法的參數或返回值。
- 方法訪問權限控制。
- 方法
Mock測試。
動態代理API
創建動態代理類會使用到java.lang.reflect.Proxy類和java.lang.reflect.InvocationHandler接口。java.lang.reflect.Proxy主要用于生成動態代理類Class、創建代理類實例,該類實現了java.io.Serializable接口。
java.lang.reflect.Proxy類主要方法如下:
package java.lang.reflect;
import java.lang.reflect.InvocationHandler;
/**
* Creator: yz
* Date: 2020/1/15
*/
public class Proxy implements java.io.Serializable {
// 省去成員變量和部分類方法...
/**
* 獲取動態代理處理類對象
*
* @param proxy 返回調用處理程序的代理實例
* @return 代理實例的調用處理程序
* @throws IllegalArgumentException 如果參數不是一個代理實例
*/
public static InvocationHandler getInvocationHandler(Object proxy)
throws IllegalArgumentException {
...
}
/**
* 創建動態代理類實例
*
* @param loader 指定動態代理類的類加載器
* @param interfaces 指定動態代理類的類需要實現的接口數組
* @param h 動態代理處理類
* @return 返回動態代理生成的代理類實例
* @throws IllegalArgumentException 不正確的參數異常
*/
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
throws IllegalArgumentException {
...
}
/**
* 創建動態代理類
*
* @param loader 定義代理類的類加載器
* @param interfaces 代理類要實現的接口列表
* @return 用指定的類加載器定義的代理類,它可以實現指定的接口
*/
public static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces) {
...
}
/**
* 檢測某個類是否是動態代理類
*
* @param cl 要測試的類
* @return 如該類為代理類,則為 true,否則為 false
*/
public static boolean isProxyClass(Class<?> cl) {
return java.lang.reflect.Proxy.class.isAssignableFrom(cl) && proxyClassCache.containsValue(cl);
}
/**
* 向指定的類加載器中定義一個類對象
*
* @param loader 類加載器
* @param name 類名
* @param b 類字節碼
* @param off 截取開始位置
* @param len 截取長度
* @return JVM創建的類Class對象
*/
private static native Class defineClass0(ClassLoader loader, String name, byte[] b, int off, int len);
}
java.lang.reflect.InvocationHandler接口用于調用Proxy類生成的代理類方法,該類只有一個invoke方法。
java.lang.reflect.InvocationHandler接口代碼(注釋直接搬的JDK6中文版文檔):
package java.lang.reflect;
import java.lang.reflect.Method;
/**
* 每個代理實例都具有一個關聯的調用處理程序。對代理實例調用方法時,將對方法調用進行編碼并
* 將其指派到它的調用處理程序的 invoke 方法。
*/
public interface InvocationHandler {
/**
* 在代理實例上處理方法調用并返回結果。在與方法關聯的代理實例上調用方法時,將在調用處理程序上調用此方法。
*
* @param proxy 在其上調用方法的代理實例
* @param method 對應于在代理實例上調用的接口方法的 Method 實例。Method 對象的聲明類將是在其中聲明
* 方法的接口,該接口可以是代理類賴以繼承方法的代理接口的超接口。
* @param args 包含傳入代理實例上方法調用的參數值的對象數組,如果接口方法不使用參數,
* 則為 null。基本類型的參數被包裝在適當基本包裝器類(如 java.lang.Integer
* 或 java.lang.Boolean)的實例中。
* @return 從代理實例的方法調用返回的值。如果接口方法的聲明返回類型是基本類型,
* 則此方法返回的值一定是相應基本包裝對象類的實例;否則,它一定是可分配到聲明返回類型的類型。
* 如果此方法返回的值為 null 并且接口方法的返回類型是基本類型,則代理實例上的方法調用將拋出
* NullPointerException。否則,如果此方法返回的值與上述接口方法的聲明返回類型不兼容,
* 則代理實例上的方法調用將拋出 ClassCastException。
* @throws Throwable 從代理實例上的方法調用拋出的異常。該異常的類型必須可以分配到在接口方法的
* throws 子句中聲明的任一異常類型或未經檢查的異常類型 java.lang.RuntimeException 或
* java.lang.Error。如果此方法拋出經過檢查的異常,該異常不可分配到在接口方法的 throws 子句中
* 聲明的任一異常類型,代理實例的方法調用將拋出包含此方法曾拋出的異常的
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}
使用java.lang.reflect.Proxy動態創建類對象
前面章節我們講到了ClassLoader和Unsafe都有一個叫做defineClassXXX的native方法,我們可以通過調用這個native方法動態的向JVM創建一個類對象,而java.lang.reflect.Proxy類恰好也有這么一個native方法,所以我們也將可以通過調用java.lang.reflect.Proxy類defineClass0方法實現動態創建類對象。
ProxyDefineClassTest示例代碼:
package com.anbai.sec.proxy;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import static com.anbai.sec.classloader.TestClassLoader.TEST_CLASS_BYTES;
import static com.anbai.sec.classloader.TestClassLoader.TEST_CLASS_NAME;
/**
* Creator: yz
* Date: 2020/1/15
*/
public class ProxyDefineClassTest {
public static void main(String[] args) {
// 獲取系統的類加載器,可以根據具體情況換成一個存在的類加載器
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
try {
// 反射java.lang.reflect.Proxy類獲取其中的defineClass0方法
Method method = Proxy.class.getDeclaredMethod("defineClass0", new Class[]{
ClassLoader.class, String.class, byte[].class, int.class, int.class
});
// 修改方法的訪問權限
method.setAccessible(true);
// 反射調用java.lang.reflect.Proxy.defineClass0()方法,動態向JVM注冊
// com.anbai.sec.classloader.TestHelloWorld類對象
Class helloWorldClass = (Class) method.invoke(null, new Object[]{
classLoader, TEST_CLASS_NAME, TEST_CLASS_BYTES, 0, TEST_CLASS_BYTES.length
});
// 輸出TestHelloWorld類對象
System.out.println(helloWorldClass);
} catch (Exception e) {
e.printStackTrace();
}
}
}
程序運行結果:
class com.anbai.sec.classloader.TestHelloWorld
創建代理類實例
我們可以使用Proxy.newProxyInstance來創建動態代理類實例,或者使用Proxy.getProxyClass()獲取代理類對象再反射的方式來創建,下面我們以com.anbai.sec.proxy.FileSystem接口為例,演示如何創建其動態代理類實例。
Proxy.newProxyInstance示例代碼:
// 創建UnixFileSystem類實例
FileSystem fileSystem = new UnixFileSystem();
// 使用JDK動態代理生成FileSystem動態代理類實例
FileSystem proxyInstance = (FileSystem) Proxy.newProxyInstance(
FileSystem.class.getClassLoader(),// 指定動態代理類的類加載器
new Class[]{FileSystem.class}, // 定義動態代理生成的類實現的接口
new JDKInvocationHandler(fileSystem)// 動態代理處理類
);
Proxy.getProxyClass反射示例代碼:
// 創建UnixFileSystem類實例
FileSystem fileSystem = new UnixFileSystem();
// 創建動態代理處理類
InvocationHandler handler = new JDKInvocationHandler(fileSystem);
// 通過指定類加載器、類實現的接口數組生成一個動態代理類
Class proxyClass = Proxy.getProxyClass(
FileSystem.class.getClassLoader(),// 指定動態代理類的類加載器
new Class[]{FileSystem.class}// 定義動態代理生成的類實現的接口
);
// 使用反射獲取Proxy類構造器并創建動態代理類實例
FileSystem proxyInstance = (FileSystem) proxyClass.getConstructor(
new Class[]{InvocationHandler.class}).newInstance(new Object[]{handler}
);
動態代理添加方法調用日志示例
假設我們有一個叫做FileSystem接口,UnixFileSystem類實現了FileSystem接口,我們可以使用JDK動態代理的方式給FileSystem的接口方法執行前后都添加日志輸出。
com.anbai.sec.proxy.FileSystem示例代碼:
package com.anbai.sec.proxy;
import java.io.File;
import java.io.Serializable;
/**
* Creator: yz
* Date: 2020/1/14
*/
public interface FileSystem extends Serializable {
String[] list(File file);
}
com.anbai.sec.proxy.UnixFileSystem示例代碼:
package com.anbai.sec.proxy;
import java.io.File;
/**
* Creator: yz
* Date: 2020/1/14
*/
public class UnixFileSystem implements FileSystem {
/* -- Disk usage -- */
public int spaceTotal = 996;
@Override
public String[] list(File file) {
System.out.println("正在執行[" + this.getClass().getName() + "]類的list方法,參數:[" + file + "]");
return file.list();
}
}
com.anbai.sec.proxy.JDKInvocationHandler示例代碼:
package com.anbai.sec.proxy;
import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
/**
* Creator: yz
* Date: 2020/1/14
*/
public class JDKInvocationHandler implements InvocationHandler, Serializable {
private final Object target;
public JDKInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 為了不影響測試Demo的輸出結果,這里忽略掉toString方法
if ("toString".equals(method.getName())) {
return method.invoke(target, args);
}
System.out.println("即將調用[" + target.getClass().getName() + "]類的[" + method.getName() + "]方法...");
Object obj = method.invoke(target, args);
System.out.println("已完成[" + target.getClass().getName() + "]類的[" + method.getName() + "]方法調用...");
return obj;
}
}
com.anbai.sec.proxy.FileSystemProxyTest示例代碼:
package com.anbai.sec.proxy;
import java.io.File;
import java.lang.reflect.Proxy;
import java.util.Arrays;
/**
* Creator: yz
* Date: 2020/1/14
*/
public class FileSystemProxyTest {
public static void main(String[] args) {
// 創建UnixFileSystem類實例
FileSystem fileSystem = new UnixFileSystem();
// 使用JDK動態代理生成FileSystem動態代理類實例
FileSystem proxyInstance = (FileSystem) Proxy.newProxyInstance(
FileSystem.class.getClassLoader(),// 指定動態代理類的類加載器
new Class[]{FileSystem.class}, // 定義動態代理生成的類實現的接口
new JDKInvocationHandler(fileSystem)// 動態代理處理類
);
System.out.println("動態代理生成的類名:" + proxyInstance.getClass());
System.out.println("----------------------------------------------------------------------------------------");
System.out.println("動態代理生成的類名toString:" + proxyInstance.toString());
System.out.println("----------------------------------------------------------------------------------------");
// 使用動態代理的方式UnixFileSystem方法
String[] files = proxyInstance.list(new File("."));
System.out.println("----------------------------------------------------------------------------------------");
System.out.println("UnixFileSystem.list方法執行結果:" + Arrays.toString(files));
System.out.println("----------------------------------------------------------------------------------------");
boolean isFileSystem = proxyInstance instanceof FileSystem;
boolean isUnixFileSystem = proxyInstance instanceof UnixFileSystem;
System.out.println("動態代理類[" + proxyInstance.getClass() + "]是否是FileSystem類的實例:" + isFileSystem);
System.out.println("----------------------------------------------------------------------------------------");
System.out.println("動態代理類[" + proxyInstance.getClass() + "]是否是UnixFileSystem類的實例:" + isUnixFileSystem);
System.out.println("----------------------------------------------------------------------------------------");
}
}
程序執行結果:
動態代理生成的類名:class com.sun.proxy.$Proxy0
----------------------------------------------------------------------------------------
動態代理生成的類名toString:com.anbai.sec.proxy.UnixFileSystem@194d6112
----------------------------------------------------------------------------------------
即將調用[com.anbai.sec.proxy.UnixFileSystem]類的[list]方法...
正在執行[com.anbai.sec.proxy.UnixFileSystem]類的list方法,參數:[.]
已完成[com.anbai.sec.proxy.UnixFileSystem]類的[list]方法調用...
----------------------------------------------------------------------------------------
UnixFileSystem.list方法執行結果:[javaweb-sec.iml, javaweb-sec-source, pom.xml, README.md, .gitignore, gitbook, .git, jni, .idea]
----------------------------------------------------------------------------------------
動態代理類[class com.sun.proxy.$Proxy0]是否是FileSystem類的實例:true
----------------------------------------------------------------------------------------
動態代理類[class com.sun.proxy.$Proxy0]是否是UnixFileSystem類的實例:false
----------------------------------------------------------------------------------------
動態代理類生成的$ProxyXXX類代碼分析
java.lang.reflect.Proxy類是通過創建一個新的Java類(類名為com.sun.proxy.$ProxyXXX)的方式來實現無侵入的類方法代理功能的。
動態代理生成出來的類有如下技術細節和特性:
- 動態代理的必須是接口類,通過
動態生成一個接口實現類來代理接口的方法調用(反射機制)。 - 動態代理類會由
java.lang.reflect.Proxy.ProxyClassFactory創建。 ProxyClassFactory會調用sun.misc.ProxyGenerator類生成該類的字節碼,并調用java.lang.reflect.Proxy.defineClass0()方法將該類注冊到JVM。- 該類繼承于
java.lang.reflect.Proxy并實現了需要被代理的接口類,因為java.lang.reflect.Proxy類實現了java.io.Serializable接口,所以被代理的類支持序列化/反序列化。 - 該類實現了代理接口類(示例中的接口類是
com.anbai.sec.proxy.FileSystem),會通過ProxyGenerator動態生成接口類(FileSystem)的所有方法, - 該類因為實現了代理的接口類,所以當前類是代理的接口類的實例(
proxyInstance instanceof FileSystem為true),但不是代理接口類的實現類的實例(proxyInstance instanceof UnixFileSystem為false)。 - 該類方法中包含了被代理的接口類的所有方法,通過調用動態代理處理類(
InvocationHandler)的invoke方法獲取方法執行結果。 - 該類代理的方式重寫了
java.lang.Object類的toString、hashCode、equals方法。 - 如果動過動態代理生成了多個動態代理類,新生成的類名中的
0會自增,如com.sun.proxy.$Proxy0/$Proxy1/$Proxy2。
動態代理生成的com.sun.proxy.$Proxy0類代碼:
package com.sun.proxy.$Proxy0;
import java.io.File;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
public final class $Proxy0 extends Proxy implements FileSystem {
private static Method m1;
// 實現的FileSystem接口方法,如果FileSystem里面有多個方法那么在這個類中將從m3開始n個成員變量
private static Method m3;
private static Method m0;
private static Method m2;
public $Proxy0(InvocationHandler var1) {
super(var1);
}
public final boolean equals(Object var1) {
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[] list(File var1) {
try {
return (String[]) super.h.invoke(this, m3, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final int hashCode() {
try {
return (Integer) super.h.invoke(this, m0, (Object[]) null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final String toString() {
try {
return (String) super.h.invoke(this, m2, (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"));
m3 = Class.forName("com.anbai.sec.proxy.FileSystem").getMethod("list", Class.forName("java.io.File"));
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
m2 = Class.forName("java.lang.Object").getMethod("toString");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
動態代理類實例序列化問題
動態代理類符合Java對象序列化條件,并且在序列化/反序列化時會被ObjectInputStream/ObjectOutputStream特殊處理。
FileSystemProxySerializationTest示例代碼:
package com.anbai.sec.proxy;
import java.io.*;
import java.lang.reflect.Proxy;
/**
* Creator: yz
* Date: 2020/1/14
*/
public class FileSystemProxySerializationTest {
public static void main(String[] args) {
try {
// 創建UnixFileSystem類實例
FileSystem fileSystem = new UnixFileSystem();
// 使用JDK動態代理生成FileSystem動態代理類實例
FileSystem proxyInstance = (FileSystem) Proxy.newProxyInstance(
FileSystem.class.getClassLoader(),// 指定動態代理類的類加載器
new Class[]{FileSystem.class}, // 定義動態代理生成的類實現的接口
new JDKInvocationHandler(fileSystem)// 動態代理處理類
);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 創建Java對象序列化輸出流對象
ObjectOutputStream out = new ObjectOutputStream(baos);
// 序列化動態代理類
out.writeObject(proxyInstance);
out.flush();
out.close();
// 利用動態代理類生成的二進制數組創建二進制輸入流對象用于反序列化操作
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
// 通過反序列化輸入流(bais),創建Java對象輸入流(ObjectInputStream)對象
ObjectInputStream in = new ObjectInputStream(bais);
// 反序列化輸入流數據為FileSystem對象
FileSystem test = (FileSystem) in.readObject();
System.out.println("反序列化類實例類名:" + test.getClass());
System.out.println("反序列化類實例toString:" + test.toString());
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
程序執行結果:
反序列化類實例類名:class com.sun.proxy.$Proxy0
反序列化類實例toString:com.anbai.sec.proxy.UnixFileSystem@b07848
動態代理生成的類在反序列化/反序列化時不會序列化該類的成員變量,并且serialVersionUID為0L ,也將是說將該類的Class對象傳遞給java.io.ObjectStreamClass的靜態lookup方法時,返回的ObjectStreamClass實例將具有以下特性:
- 調用其
getSerialVersionUID方法將返回0L。 - 調用其
getFields方法將返回長度為零的數組。 - 調用其
getField方法將返回null。
但其父類(java.lang.reflect.Proxy)在序列化時不受影響,父類中的h變量(InvocationHandler)將會被序列化,這個h存儲了動態代理類的處理類實例以及動態代理的接口類的實現類的實例。
動態代理生成的對象(com.sun.proxy.$ProxyXXX)序列化的時候會使用一個特殊的協議:TC_PROXYCLASSDESC(0x7D),這個常量在java.io.ObjectStreamConstants中定義的。在反序列化時也不會調用java.io.ObjectInputStream類的resolveClass方法而是調用resolveProxyClass方法來轉換成類對象的。
Java Web安全
推薦文章: