# Java 动态代理
Java
反射提供了一种类动态代理机制,可以通过代理接口实现类来完成程序无侵入式扩展。
Java 动态代理主要使用场景:
- 统计方法执行所耗时间。
- 在方法执行前后添加日志。
- 检测方法的参数或返回值。
- 方法访问权限控制。
- 方法
Mock
测试。
# 动态代理 API
创建动态代理类会使用到 java.lang.reflect.Proxy
类和 java.lang.reflect.InvocationHandler
接口。 java.lang.reflect.Proxy
主要用于生成动态代理类 Class
、创建代理类实例,该类实现了 java.io.Serializable
接口。
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; | |
} |
高版本的 jdk 已经去掉了 proxy 类的 definclass
package org.chenluo.servlet; | |
import java.lang.reflect.Method; | |
import java.lang.reflect.Proxy; | |
import static org.chenluo.servlet.TestClassLoader.TEST_CLASS_BYTES; | |
import static org.chenluo.servlet.TestClassLoader.TEST_CLASS_NAME; | |
import java.lang.invoke.MethodHandles; | |
import java.lang.invoke.MethodHandles.Lookup; | |
import java.lang.reflect.Method; | |
import java.nio.file.Files; | |
import java.nio.file.Paths; | |
import static org.chenluo.servlet.TestClassLoader.TEST_CLASS_BYTES; | |
import static org.chenluo.servlet.TestClassLoader.TEST_CLASS_NAME; | |
public class ProxyDefineClassTest { | |
public static void main(String[] args) { | |
try { | |
// 获取系统类加载器 | |
ClassLoader classLoader = ClassLoader.getSystemClassLoader(); | |
// 假设我们已经有字节码文件在指定路径 | |
byte[] classBytes = TEST_CLASS_BYTES; | |
// 获取 Lookup 对象 | |
Lookup lookup = MethodHandles.lookup(); | |
// 通过反射获取 Lookup 类中的 defineClass 方法 | |
Method defineClassMethod = Lookup.class.getDeclaredMethod("defineClass", byte[].class); | |
defineClassMethod.setAccessible(true); | |
// 调用 defineClass 方法定义类 | |
Class<?> helloWorldClass = (Class<?>) defineClassMethod.invoke(lookup, (Object) classBytes); | |
// 输出 TestHelloWorld 类对象 | |
System.out.println(helloWorldClass); | |
// 创建实例并调用方法 | |
Object instance = helloWorldClass.getDeclaredConstructor().newInstance(); | |
Method sayHelloMethod = helloWorldClass.getMethod("sayHello"); | |
sayHelloMethod.invoke(instance); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} | |
} | |
} |
# 详细讲解
-
包和导入声明:
package org.chenluo.servlet;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.reflect.Method;
import static org.chenluo.servlet.TestClassLoader.TEST_CLASS_BYTES;
import static org.chenluo.servlet.TestClassLoader.TEST_CLASS_NAME;
- 定义包名为
org.chenluo.servlet
。 - 导入了
java.lang.invoke.MethodHandles
和java.lang.invoke.MethodHandles.Lookup
,用于处理方法句柄。 - 导入了
java.lang.reflect.Method
,用于反射操作。 - 导入了
TestClassLoader
中的常量TEST_CLASS_BYTES
和TEST_CLASS_NAME
。
- 定义包名为
-
主类和主方法:
public class ProxyDefineClassTest {
public static void main(String[] args) {
try {
// 获取系统类加载器
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
- 定义了一个名为
ProxyDefineClassTest
的公共类。 - 在
main
方法中,获取系统类加载器。
- 定义了一个名为
-
读取字节码:
// 假设我们已经有字节码文件在指定路径
byte[] classBytes = TEST_CLASS_BYTES;
- 假设已经有字节码文件,使用
TEST_CLASS_BYTES
变量获取字节码。
- 假设已经有字节码文件,使用
-
获取
Lookup
对象:// 获取 Lookup 对象
Lookup lookup = MethodHandles.lookup();
- 使用
MethodHandles.lookup()
方法获取Lookup
对象,用于后续的反射操作。
- 使用
-
通过反射获取
defineClass
方法:// 通过反射获取 Lookup 类中的 defineClass 方法
Method defineClassMethod = Lookup.class.getDeclaredMethod("defineClass", byte[].class);
defineClassMethod.setAccessible(true);
- 通过反射获取
Lookup
类中的defineClass
方法,该方法用于定义一个类。 - 将该方法的访问权限设置为可访问。
- 通过反射获取
-
调用
defineClass
方法定义类:// 调用 defineClass 方法定义类
Class<?> helloWorldClass = (Class<?>) defineClassMethod.invoke(lookup, (Object) classBytes);
- 调用
defineClass
方法,将字节码转换为类对象。 - 通过
lookup
对象调用defineClassMethod
,并传入字节码数组classBytes
。
- 调用
-
输出定义的类对象:
// 输出 TestHelloWorld 类对象
System.out.println(helloWorldClass);
- 输出定义的类对象的信息。
-
创建实例并调用方法:
// 创建实例并调用方法
Object instance = helloWorldClass.getDeclaredConstructor().newInstance();
Method sayHelloMethod = helloWorldClass.getMethod("sayHello");
sayHelloMethod.invoke(instance);
- 使用反射创建定义的类的实例。
- 获取并调用实例的
sayHello
方法。
-
异常处理:
} catch (Exception e) {
e.printStackTrace();
}
- 捕获并处理所有异常,打印异常堆栈信息。
# 运行逻辑
- 首先,通过系统类加载器加载字节码文件。
- 然后,通过
MethodHandles.Lookup
获取反射操作对象,并通过反射获取defineClass
方法。 - 使用
defineClass
方法将字节码转换为类对象,并输出该类对象的信息。 - 最后,使用反射创建类的实例,并调用其中的方法,验证类定义是否成功。
# 动态代理添加方法调用日志示例
假设我们有一个叫做 FileSystem
接口, UnixFileSystem
类实现了 FileSystem
接口,我们可以使用 JDK动态代理
的方式给 FileSystem
的接口方法执行前后都添加日志输出。
org.chenluo.servlet.FileSystem 示例代码:
public interface FileSystem extends Serializable { | |
String[] list(File file); | |
} |
org.chenluo.servlet.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; | |
} | |
} |
org.chenluo.servlet.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("----------------------------------------------------------------------------------------"); | |
} | |
} |
# 动态代理类生成的 $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 |
# 主类和主方法
public class FileSystemProxySerializationTest { | |
public static void main(String[] args) { | |
try { | |
// 创建 UnixFileSystem 类实例 | |
FileSystem fileSystem = new UnixFileSystem(); |
- 定义了
FileSystemProxySerializationTest
主类。 - 在
main
方法中,创建一个UnixFileSystem
实例。
# 创建动态代理对象
// 使用 JDK 动态代理生成 FileSystem 动态代理类实例 | |
FileSystem proxyInstance = (FileSystem) Proxy.newProxyInstance( | |
FileSystem.class.getClassLoader(), // 指定动态代理类的类加载器 | |
new Class[]{FileSystem.class}, // 定义动态代理生成的类实现的接口 | |
new JDKInvocationHandler(fileSystem) // 动态代理处理类 | |
); |
- 使用
Proxy.newProxyInstance
创建一个动态代理对象proxyInstance
。 - 指定类加载器、接口和处理器(
JDKInvocationHandler
)用于创建代理对象。
# 序列化动态代理对象
ByteArrayOutputStream baos = new ByteArrayOutputStream(); | |
ObjectOutputStream out = new ObjectOutputStream(baos); | |
// 序列化动态代理类 | |
out.writeObject(proxyInstance); | |
out.flush(); | |
out.close(); |
- 创建
ByteArrayOutputStream
和ObjectOutputStream
对象,用于序列化代理对象。 - 将代理对象写入
ObjectOutputStream
,并刷新和关闭流。
# 反序列化动态代理对象
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); | |
ObjectInputStream in = new ObjectInputStream(bais); | |
// 反序列化输入流数据为 FileSystem 对象 | |
FileSystem test = (FileSystem) in.readObject(); |
- 创建
ByteArrayInputStream
和ObjectInputStream
对象,用于反序列化代理对象。 - 从
ObjectInputStream
读取对象,并将其转为FileSystem
类型。
# 输出反序列化对象的信息
System.out.println("反序列化类实例类名:" + test.getClass()); | |
System.out.println("反序列化类实例toString:" + test.toString()); | |
} catch (IOException e) { | |
e.printStackTrace(); | |
} catch (ClassNotFoundException e) { | |
e.printStackTrace(); | |
} | |
} | |
} |
- 输出反序列化对象的类名和
toString
方法的结果。 - 捕获并处理
IOException
和ClassNotFoundException
异常。
动态代理生成的类在 反序列化/反序列化
时不会序列化该类的成员变量,并且 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
方法来转换成类对象的。