Proxy如何生成动态字节码?

2021/04/11 Java高级知识 共 14232 字,约 41 分钟
闷骚的程序员

1. 回顾Proxy类

  1. Proxy类是反射包中提供的,用于为真实主题生成代理对象。
  2. 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&lt;?&gt;[] 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&lt;?&gt;[] 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 方法为任何一个目标接口生成的子类字节码经过反编译后,仔细观察可以得出如下几点:

  1. 字节码没有使用 Proxy 类的任何属性和方法。
  2. 字节码的构造方法绑定了一个InvocationHandler参数,并且构造方法体还是直接调用的父类Proxy的构造,用来注入 InvocationHandler 参数。
  3. 字节码的构造方法如下:
    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类完全可以。

  1. 在生成每一个代理类文件时都需要让其持有InvocationHandler的引用,这样导致代码冗余,而且影响生成文件的效率。
  2. 如果我们需要判断动态字节码是否是一个代理类,不太好判断;而继承Proxy之后,我们只需要使用 obj instanceof Proxy 便可以判断一个对象是否是动态代理对象。
  3. 在整个动态代理过程中,真实业务实际上是代理对象委托真实主题去处理的,代理对象的作用只是在委托真实主题处理前后做拦截处理,也就是做了一个方法转发的作用。如果要代理一个普通类的话,意味着代理对象需要继承普通类的所有成员,然而这些成员对代理对象而言是没用的,这无疑造成了内存空间的浪费。
  4. 如果代理普通类,代理对象需要继承普通类,浪费空间不说,致命的是,如果普通类中存在final方法,则代理类不能覆盖该方法,便不能完成对该方法的代理。

文档信息

Search

    Table of Contents