深入解析“Java 字节码 ” 之 「动态代理的实现」

Posted by tomas家的小拨浪鼓 on November 3, 2019

深入解析“Java 字节码 ” 之 「动态代理的实现」

本文主要是《深入探索 JVM》系列中『字节码篇』文章,主要通过案例来更加形象的了解「虚拟机字节码」的实现。系列文章目录见:《 深入探索 JVM 》文集


『字节码』篇文章推荐:
深入解析“Java 字节码 ” 之 「类文件结构」
深入解析“Java 字节码 ” 之 「从案例深度解读 Java 字节码」
深入解析“Java 字节码 ” 之 「进一步探究 Java 方法的字节码实现」
深入解析“Java 字节码 ” 之 「从案例解读虚拟机属性表」
深入解析“Java 字节码 ” 之 「虚拟机机字节码执行引擎」
深入解析“Java 字节码 ” 之 「动态代理的实现」


一,前言

在Java里面除了javac和字节码类库外,使用字节码生成的例子还有很多,如Web服务器中的JSP编译器,编译时植入的AOP框架,还有很常用的动态代理技术,甚至在使用反射的时候虚拟机都有可能会在运行时生成字节码来提高执行速度。


二,动态代理

对于 Spring 的 AOP,它会生成一个相应的代理类,代理我们真实目标的 been。
对于 Spring 来说,它主要基于两种手段来去实现 AOP 功能:
① 采用像 CGLIB 这样字节码的库,它可以在运行时动态的根据我们的指定的类型来去生成相应的 Class。
② 基于 Java 的动态代理。动态代理的限制:只能对接口进行代理,不能对具体的类进行代理。 因为我们使用 Spring 一般都是面向接口编程,因此 Spring 的对于接口的代理都会使用 Java 的动态代理来去实现。

  • 例子
# Subject
public interface Subject {
    void request();
}

# RealSubject
public class RealSubject implements Subject {
    @Override
    public void request() {
        System.out.println("From real subject");
    }
}
# DynamicSubject
public class DynamicSubject implements InvocationHandler{
    private Object sub;
    public DynamicSubject(Object obj) {
        this.sub = obj;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("before calling : "+ method);
        method.invoke(sub, args);
        System.out.println("after calling : " + method);
        return null;
    }
}

# Client
public class Client {
    public static void main(String[] args) {
        RealSubject rs = new RealSubject();
        InvocationHandler ds = new DynamicSubject(rs);
        Class<?> cls = rs.getClass();
        Subject subject = (Subject) Proxy.newProxyInstance(cls.getClassLoader(), cls.getInterfaces(), ds);
        subject.request();
        /*
            输出:class com.sun.proxy.$Proxy0
            这个类实际上是在程序运行期动态的创建出来的!
         */
        System.out.println(subject.getClass());
        // class java.lang.reflect.Proxy
        System.out.println(subject.getClass().getSuperclass());
    }
}
  • 从源码分析“动态代理”的字节码生成过程


① 查找或构建指定的代理类的Class(该过程就会动态生成指定代理类的字节码,然后通过加载动态生成的字节码实现这个动态代理类的加载)。
② 获取指定的代理类Class的特定Constructor,该构造方法要求具有唯一一个类型为InvocationHandler的参数。
③ 通过反射创建指定的代理类,这里会传入我们自己实现的 DynamicSubject 类(InvocationHandler 接口的实现类)作为构造方法的参数。


「getProxyClass0(loader, intfs);」

进入 「private static final class ProxyClassFactory implements BiFunction<ClassLoader, Class[], Class>」类的「public Class apply(ClassLoader loader, Class[] interfaces)」方法:

注意:上面有个「saveGeneratedFiles」字段可以控制是否将动态生成的对象的字节码文件保存到磁盘中。

private static final boolean saveGeneratedFiles = ((Boolean)AccessController.doPrivileged(new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles"))).booleanValue();

修改:

# Client
public class Client {
    public static void main(String[] args) {
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
        RealSubject rs = new RealSubject();
        InvocationHandler ds = new DynamicSubject(rs);
        Class<?> cls = rs.getClass();
        Subject subject = (Subject) Proxy.newProxyInstance(cls.getClassLoader(), cls.getInterfaces(), ds);
        subject.request();
        /*
            输出:class com.sun.proxy.$Proxy0
            这个类实际上是在程序运行期动态的创建出来的!
         */
        System.out.println(subject.getClass());
        // class java.lang.reflect.Proxy
        System.out.println(subject.getClass().getSuperclass());
        // interface com.bayern.shengsiyuan.jvm_lecture.bytecode.Subject
        Arrays.stream(subject.getClass().getInterfaces()).forEach(System.out::println);
    }
}

运行后得到:

这就是运行期所生成的动态代理类!

「$Proxy0.class」:

👆的「super.h.invoke(…)」本质上都会调用到我们自己实现的InvocationHandler的实现类 —— 「DynamicSubject#invoke」方法,「DynamicSubject#invoke」中会去调用真实对象的相应的方法。
也就是说,本例子中「equals」、「toString」、「hashCode」、「request」方法最终都会通过动态代理调用到真实的 RealSubject 的对应的方法。

可见,Java 的 Proxy 实现的动态代理,是会对代理类的所有方法生效的!!!而无法对不同的方法进行不同的代理实现!!!

同时注意,「equals」、「toString」、「hashCode」是从 Object 继承来的方法。也就是说,Object 的这三个方法是会通过动态代理来调用真实对象的对应的方法的,而 Object 中的其他方法则不会!

# Proxy doc 文档
* <li>An invocation of the {@code hashCode},
* {@code equals}, or {@code toString} methods declared in
* {@code java.lang.Object} on a proxy instance will be encoded and
* dispatched to the invocation handler's {@code invoke} method in
* the same manner as interface method invocations are encoded and
* dispatched, as described above.  The declaring class of the
* {@code Method} object passed to {@code invoke} will be
* {@code java.lang.Object}.  Other public methods of a proxy
* instance inherited from {@code java.lang.Object} are not
* overridden by a proxy class, so invocations of those methods behave
* like they do for instances of {@code java.lang.Object}.