1. 回顾Proxy类
- Proxy类是反射包中提供的,用于为真实主题生成代理对象。
- Proxy对外提供了4个常见API。
2. Proxy到底是如何生成动态字节码文件的啊?
玩转了Proxy的4个API,虽然它可以为我们生成动态字节码代理对象,也可以为我们获取动态字节码的class对象,也可以做判断是不是动态字节码对象等。
但是,玩了一圈,还是不知道生成的动态字节码文件到底长啥样?
看了一圈源码,最终发现,实际上生成动态字节码的过程是在 ProxyGenerator 类中的如下方法生成的:
public static byte[] generateProxyClass(final String name, Class<?>[] interfaces);
第一个参数 name:需要生成的动态字节码文件的名称。
第二个参数 interfaces:需要生成的动态字节码文件所对应的接口数组,即可以传递多个接口。
我们在前面代码的基础上,再准备一个类,专门用来存储ProxyGenerator动态生成的字节码到本地磁盘上。
ClassSaveUtil
package zeh.test.demo.com.proxy;
import java.io.FileOutputStream;
import java.io.IOException;
import sun.misc.ProxyGenerator;
// 生成动态字节码并保存到本地磁盘
public class ClassSaveUtil {
// 为目标接口生成指定名称的动态字节码,并将该字节码保存到本地磁盘中
public static void saveClassToDisk(String className, Class<?>[] interfaces) {
FileOutputStream out = null;
try {
// 将生成的动态字节码文件保存到本地磁盘,用于反编译查看
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(className, interfaces);
out = new FileOutputStream(ProxyMain.class.getResource("/").getPath() + className + ".class");
out.write(proxyClassFile);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (out != null) {
out.flush();
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("生成动态字节码文件结束");
}
}
}
2.1 指定目标字节码名称和目标接口
ProxyMain测试
package zeh.test.demo.com.proxy;
// 2021-03-30
public class ProxyMain {
// 指定目标字节码名称为 $Proxy0
// 指定目标接口为 UserImpl的接口
public static void main(String[] args) {
ClassSaveUtil.saveClassToDisk("$Proxy0", UserImpl.class.getInterfaces());
}
}
测试结果
生成动态字节码文件结束
Process finished with exit code 0
字节码文件如下
在本地磁盘生成的字节码文件为:$Proxy0.class
结果分析
反编译查看源码如下:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
import zeh.test.demo.com.proxy.IUser;
public final class $Proxy0 extends Proxy implements IUser {
private static Method m1;
private static Method m4;
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 void doOther() throws {
try {
super.h.invoke(this, m4, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
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 doSome() 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"));
m4 = Class.forName("zeh.test.demo.com.proxy.IUser").getMethod("doOther");
m2 = Class.forName("java.lang.Object").getMethod("toString");
m3 = Class.forName("zeh.test.demo.com.proxy.IUser").getMethod("doSome");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
结论
jdk的动态代理为我们生成的动态字节码文件,已经继承了Proxy类,并且实现了对应的主题接口IUser。
tips:因为java中是单继承,而 ProxyGenerator.generateProxyClass 方法生成的动态字节码已经继承了Proxy类,因此jdk的动态代理只能代理接口,即只能为接口生成动态字节码,而不能为普通类生成动态字节码。
2.2 指定目标字节码名称,目标接口传null
ProxyMain测试
package zeh.test.demo.com.proxy;
public class ProxyMain {
// 指定目标字节码名称为 $ProxyForInterfaceIsNull
// 指定目标接口为 null
public static void main(String[] args) {
ClassSaveUtil.saveClassToDisk("$ProxyForInterfaceIsNull", null);
}
}
测试结果
java.lang.NullPointerException
at sun.misc.ProxyGenerator.generateClassFile(ProxyGenerator.java:450)
at sun.misc.ProxyGenerator.generateProxyClass(ProxyGenerator.java:339)
at sun.misc.ProxyGenerator.generateProxyClass(ProxyGenerator.java:324)
at zeh.test.demo.com.proxy.ClassSaveUtil.saveClassToDisk(ClassSaveUtil.java:26)
at zeh.test.demo.com.proxy.ProxyMain.main(ProxyMain.java:18)
生成动态字节码文件结束
Process finished with exit code 0
结论:
ProxyGenerator.generateProxyClass生成目标字节码时,如果目标接口传递为null,则直接报空指针异常。
2.3 指定目标字节码名称,目标接口传一个空数组
ProxyMain测试
package zeh.test.demo.com.proxy;
public class ProxyMain {
// 指定目标字节码名称为 $ProxyForInterfaceIsEmptyArray
// 指定目标接口为 空数组
public static void main(String[] args) {
Class<?>[] interfaces = IUser.class.getInterfaces();
System.out.println("interfaces length is " + interfaces.length);
ClassSaveUtil.saveClassToDisk("$ProxyForInterfaceIsEmptyArray", interfaces);
}
}
测试结果
interfaces length is 0
生成动态字节码文件结束
Process finished with exit code 0
字节码文件如下
在本地磁盘生成的字节码文件为:$ProxyForInterfaceIsEmptyArray.class
结果分析
反编译查看源码如下:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
public final class $ProxyForInterfaceIsEmptyArray extends Proxy {
private static Method m1;
private static Method m2;
private static Method m0;
public $ProxyForInterfaceIsEmptyArray(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 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");
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类,而不会实现任何接口。
2.4 指定目标字节码名称,目标接口传一个普通类
ProxyMain测试
package zeh.test.demo.com.proxy;
public class ProxyMain {
// 指定目标字节码名称为 $ProxyForInterfaceIsClass
// 指定目标接口为 普通类的数组
public static void main(String[] args) {
Class<?>[] interfaces = new Class[]{UserImpl.class};
ClassSaveUtil.saveClassToDisk("$ProxyForInterfaceIsClass", interfaces);
}
}
测试结果
生成动态字节码文件结束
Process finished with exit code 0
字节码文件如下
在本地磁盘生成的字节码文件为:$ProxyForInterfaceIsClass.class
结果分析
反编译查看源码如下:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
import zeh.test.demo.com.proxy.UserImpl;
public final class $ProxyForInterfaceIsClass extends Proxy implements UserImpl {
private static Method m1;
private static Method m4;
private static Method m9;
private static Method m2;
private static Method m3;
private static Method m6;
private static Method m5;
private static Method m8;
private static Method m10;
private static Method m0;
private static Method m7;
public $ProxyForInterfaceIsClass(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 void doOther() throws {
try {
super.h.invoke(this, m4, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final void notify() throws {
try {
super.h.invoke(this, m9, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
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 doSome() throws {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final void wait(long var1) throws InterruptedException {
try {
super.h.invoke(this, m6, new Object[]{var1});
} catch (RuntimeException | InterruptedException | Error var4) {
throw var4;
} catch (Throwable var5) {
throw new UndeclaredThrowableException(var5);
}
}
public final void wait(long var1, int var3) throws InterruptedException {
try {
super.h.invoke(this, m5, new Object[]{var1, var3});
} catch (RuntimeException | InterruptedException | Error var5) {
throw var5;
} catch (Throwable var6) {
throw new UndeclaredThrowableException(var6);
}
}
public final Class getClass() throws {
try {
return (Class)super.h.invoke(this, m8, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final void notifyAll() throws {
try {
super.h.invoke(this, m10, (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);
}
}
public final void wait() throws InterruptedException {
try {
super.h.invoke(this, m7, (Object[])null);
} catch (RuntimeException | InterruptedException | 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"));
m4 = Class.forName("zeh.test.demo.com.proxy.UserImpl").getMethod("doOther");
m9 = Class.forName("zeh.test.demo.com.proxy.UserImpl").getMethod("notify");
m2 = Class.forName("java.lang.Object").getMethod("toString");
m3 = Class.forName("zeh.test.demo.com.proxy.UserImpl").getMethod("doSome");
m6 = Class.forName("zeh.test.demo.com.proxy.UserImpl").getMethod("wait", Long.TYPE);
m5 = Class.forName("zeh.test.demo.com.proxy.UserImpl").getMethod("wait", Long.TYPE, Integer.TYPE);
m8 = Class.forName("zeh.test.demo.com.proxy.UserImpl").getMethod("getClass");
m10 = Class.forName("zeh.test.demo.com.proxy.UserImpl").getMethod("notifyAll");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
m7 = Class.forName("zeh.test.demo.com.proxy.UserImpl").getMethod("wait");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
结论:
ProxyGenerator.generateProxyClass生成目标字节码时,如果目标接口传递为普通类,而不是接口,不会影响生成结果。
即 ProxyGenerator.generateProxyClass 在生成目标字节码时认为传入的只要是个Class类型即可,即便你尝试传递一个普通类型,它生成的字节码也会实现该普通类型,把普通类当做接口来看待。
但是,当我们使用Proxy.getProxyClass或者Proxy.newProxyInstance()间接调用 ProxyGenerator.generateProxyClass 去生成字节码时,如果传入的目标接口是普通类,则会抛出异常:
public static void main(String[] args) {
Class<?>[] interfaces = new Class[]{UserImpl.class};
ClassSaveUtil.saveClassToDisk("$ProxyForInterfaceIsClass", interfaces);
Proxy.getProxyClass(UserImpl.class.getClassLoader(),interfaces);
}
抛出如下异常:
生成动态字节码文件结束
Exception in thread "main" java.lang.IllegalArgumentException: zeh.test.demo.com.proxy.UserImpl is not an interface
at java.lang.reflect.Proxy$ProxyClassFactory.apply(Proxy.java:590)
at java.lang.reflect.Proxy$ProxyClassFactory.apply(Proxy.java:557)
at java.lang.reflect.WeakCache$Factory.get(WeakCache.java:230)
at java.lang.reflect.WeakCache.get(WeakCache.java:127)
at java.lang.reflect.Proxy.getProxyClass0(Proxy.java:419)
at java.lang.reflect.Proxy.getProxyClass(Proxy.java:371)
at zeh.test.demo.com.proxy.ProxyMain.main(ProxyMain.java:22)
Process finished with exit code 1
3. Proxy生成的字节码是在哪个包下的?
我们现在不直接调用 ProxyGenerator.generateProxyClass 去为目标接口生成子类字节码了,而是直接使用 Proxy 类的方法为目标接口尝试去生成子类字节码。
3.1 当目标接口为public修饰时
IUser 为 public 修饰
package zeh.test.demo.com.proxy;
// 2021-03-30
public interface IUser {
void doSome();
void doOther();
}
ProxyMain测试
package zeh.test.demo.com.proxy;
import java.lang.reflect.Proxy;
public class ProxyMain {
public static void main(String[] args) {
Class<?>[] interfaces = UserImpl.class.getInterfaces();
Class<?> clazz = Proxy.getProxyClass(UserImpl.class.getClassLoader(),interfaces);
System.out.println(clazz.getName());
}
}
测试结果
com.sun.proxy.$Proxy0
Process finished with exit code 0
结论:
当目标接口是public时,Proxy生成的动态字节码的所属包是: com.sun.proxy
3.2 当目标接口不是public修饰时
IUser 去掉 public 修饰
package zeh.test.demo.com.proxy;
interface IUser {
void doSome();
void doOther();
}
ProxyMain测试
package zeh.test.demo.com.proxy;
import java.lang.reflect.Proxy;
public class ProxyMain {
public static void main(String[] args) {
Class<?>[] interfaces = UserImpl.class.getInterfaces();
Class<?> clazz = Proxy.getProxyClass(UserImpl.class.getClassLoader(),interfaces);
System.out.println(clazz.getName());
}
}
测试结果
zeh.test.demo.com.proxy.$Proxy0
Process finished with exit code 0
结论:
当目标接口是public时,Proxy生成的动态字节码的所属包是: zeh.test.demo.com.proxy
即和所代理的接口在同一个包下。
4. Proxy生成的字节码为什么非要继承Proxy类?
从前面验证可看出,ProxyGenerator.generateProxyClass 方法生成的动态字节码已经继承了Proxy类,但,为啥呢?
观察生成的动态字节码:
通过上面生成的动态字节码案例可以看出, ProxyGenerator.generateProxyClass 方法为任何一个目标接口生成的子类字节码经过反编译后,仔细观察可以得出如下几点:
- 字节码没有使用 Proxy 类的任何属性和方法。
- 字节码的构造方法绑定了一个InvocationHandler参数,并且构造方法体还是直接调用的父类Proxy的构造,用来注入 InvocationHandler 参数。
- 字节码的构造方法如下:
public $ProxyForInterfaceIsClass(InvocationHandler var1) throws { // 调用父类Proxy的构造方法进行InvocationHandler回调器的注入 super(var1); }
自己手动让字节码持有回调器的引用也可以啊:
经过分析可以看到,实际上动态字节码继承 Proxy 类就干了一件事儿,复用 Proxy 类的一参构造来注入回调器 InvocationHandler。除此之外,再无其他调用。
这样看来,并不是非得继承 Proxy 啊,若仅仅为了注入回调器的话,完全可以在生成字节码时,就将InvocationHandler对象注入到class中不就行了:
private InvocationHandler h;
public $ProxyForInterfaceIsClass(InvocationHandler var1) throws {
// 手动注入回调器InvocationHandler对象,使得当前字节码可以持有一个回调器的引用
this.h = var1;
// super(var1);
}
动态字节码不继承Proxy存在的问题:
首先说明,从理论和实现上来讲,动态字节码不继承Proxy类完全可以。
- 在生成每一个代理类文件时都需要让其持有InvocationHandler的引用,这样导致代码冗余,而且影响生成文件的效率。
- 如果我们需要判断动态字节码是否是一个代理类,不太好判断;而继承Proxy之后,我们只需要使用 obj instanceof Proxy 便可以判断一个对象是否是动态代理对象。
- 在整个动态代理过程中,真实业务实际上是代理对象委托真实主题去处理的,代理对象的作用只是在委托真实主题处理前后做拦截处理,也就是做了一个方法转发的作用。如果要代理一个普通类的话,意味着代理对象需要继承普通类的所有成员,然而这些成员对代理对象而言是没用的,这无疑造成了内存空间的浪费。
- 如果代理普通类,代理对象需要继承普通类,浪费空间不说,致命的是,如果普通类中存在final方法,则代理类不能覆盖该方法,便不能完成对该方法的代理。
文档信息
- 本文作者:Marshall