为何JDK的动态代理要基于接口实现而不能基于继承实现?

扫描文末二维码或者微信搜索公众号菜鸟飞呀飞,便可关注微信公众号,阅读更多Spring源码分析文章java

1. 问题

在阅读本文以前,能够先思考一下下面几个问题。数组

  • 为何说JDK的动态代理要基于接口实现,而不能基于继承来实现?
  • 在JDK的动态代理中,在目标对象方法内部调用本身的另外一个方法时,另外一个方法在执行时,为何没有通过代理对象?

2. JDK的动态代理的固定写法

  • JDK的动态代理的写法比较固定,须要先定义一个接口和接口的实现类,而后再定义一个实现了InvocationHandler接口的实现类。而后调用Proxy类的newInstance()方法便可。示例代码以下:
  • 先定义一个接口:UserService,接口中有两个方法
public interface UserService {

    int insert();

    String query();
}
复制代码
  • 再定义一个UserService接口的实现类:UserServiceImpl
public class UserServiceImpl implements UserService{

    @Override
    public int insert() {
        System.out.println("insert");
        return 0;
    }

    @Override
    public String query() {
        System.out.println("query");
        return null;
    }
}
复制代码
  • 再定义一个InvocationHandler接口的实现类:UserServiceInvocationHandler。在自定义的InvocationHandler中,定义了一个属性:target,定义这个属性的目的是为了在InvocationHandler中持有对目标对象的引用,target属性的初始化是在构造器中进行初始化的。
public class UserServiceInvocationHandler implements InvocationHandler {

    // 持有目标对象
    private Object target;

    public UserServiceInvocationHandler(Object target){
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("invocation handler");
        // 经过反射调用目标对象的方法
        return method.invoke(target,args);
    }
}
复制代码
  • 经过Proxy.newProxyInstance()方法建立代理对象
public class MainApplication {

    public static void main(String[] args) {
        // 指明一个类加载器,要操做class文件,怎么少得了类加载器呢
        ClassLoader classLoader = MainApplication.class.getClassLoader();
        // 为代理对象指定要是实现哪些接口,这里咱们要为UserServiceImpl这个目标对象建立动态代理,因此须要为代理对象指定实现UserService接口
        Class[] classes = new Class[]{UserService.class};
        // 初始化一个InvocationHandler,并初始化InvocationHandler中的目标对象
        InvocationHandler invocationHandler = new UserServiceInvocationHandler(new UserServiceImpl());
        // 建立动态代理
        UserService userService = (UserService) Proxy.newProxyInstance(classLoader, classes, invocationHandler);
        // 执行代理对象的方法,经过观察控制台的结果,判断咱们是否对目标对象(UserServiceImpl)的方法进行了加强
        userService.insert();
    }
}
复制代码
  • 控制台打印结果以下,从打印结果来看,已经完成了对目标对象UserServiceImpl的代理(加强)。

控制台结果1

3. 如何查看代理类的class文件

阅读到这里,咱们基本知道了JDK动态代理的写法,功能虽然实现了,可是对于喜欢研究源码的同窗,可能会思考:为何调用Proxy.newProxyInstance()方法后,就产生了代理对象,对目标对象的方法进行了加强?原理是什么呢?微信

  • 一般学习某项技术的原理,都是看源文件.java或者借助反编译工具反编译.class文件后去分析。但是对于JDK的动态代理,它产生的代理对象是在运行时建立的,经过常规操做,咱们没办法获得这个代理对象对应的.class文件。若是能获得代理对象多对应的class文件,那动态代理的原理,咱们就好分析了。
  • 因此接下来介绍两种获取到代理对象的.class文件的方法

3.1 手动写到磁盘

  • 在调用Proxy.newProxyInstance()方法时,最终会调用到ProxyGenerator.generateProxyClass()方法,该方法的做用就是生成代理对象的class文件,返回值是一个byte[]数组。因此咱们能够将生成的byte[]数组经过输出流,将内容写出到磁盘。
public static void main(String[] args) throws IOException {
    String proxyName = "com.tiantang.study.$Proxy0";
    Class[] interfaces = new Class[]{UserService.class};
    int accessFlags = Modifier.PUBLIC;
    byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
            proxyName, interfaces, accessFlags);
    // 将字节数组写出到磁盘
    File file = new File("/Users/liujinkun/Downloads/dynamic/$Proxy0.class");
    OutputStream outputStream = new FileOutputStream(file);
    outputStream.write(proxyClassFile);
}
复制代码
  • 运行完main()方法后,从目录中/Users/liujinkun/Downloads/dynamic/$Proxy0.class找到生成的文件,因为是一个class文件,因此咱们须要把它有反编译器编译一下,例如:在idea中,将文件放到target目录下,打开文件就能看到反编译后的代码了。

3.2 自动写到磁盘

  • 第二种方法是经过百度查来的,在代码中添加一行代码:System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"),就能实现将程序运行过程当中产生的动态代理对象的class文件写入到磁盘。以下示例:
public class MainApplication {

    public static void main(String[] args) {
        // 让代理对象的class文件写入到磁盘
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
        // 指明一个类加载器,要操做class文件,怎么少得了类加载器呢
        ClassLoader classLoader = MainApplication.class.getClassLoader();
        // 为代理对象指定要是实现哪些接口,这里咱们要为UserServiceImpl这个目标对象建立动态代理,因此须要为代理对象指定实现UserService接口
        Class[] classes = new Class[]{UserService.class};
        // 初始化一个InvocationHandler,并初始化InvocationHandler中的目标对象
        InvocationHandler invocationHandler = new UserServiceInvocationHandler(new UserServiceImpl());
        // 建立动态代理
        UserService userService = (UserService) Proxy.newProxyInstance(classLoader, classes, invocationHandler);
        // 执行代理对象的方法,经过观察控制台的结果,判断咱们是否对目标对象(UserServiceImpl)的方法进行了加强
        userService.insert();
    }
}
复制代码
  • 运行程序,最终发如今项目的根目录下出现了一个包:com.sun.proxy。包下有一个文件$Proxy0.class。在idea打开,发现就是所产生代理类的源代码。

4. 问题解答

经过上面两种方法获取到代理对象的源代码以下:异步

package com.sun.proxy;

import com.tiantang.study.UserService;
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 UserService {
    private static Method m1;
    private static Method m3;
    private static Method m4;
    private static Method m2;
    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 int insert() throws {
        try {
            return (Integer)super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final String query() throws {
        try {
            return (String)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 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"));
            m3 = Class.forName("com.tiantang.study.UserService").getMethod("insert");
            m4 = Class.forName("com.tiantang.study.UserService").getMethod("query");
            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());
        }
    }
}
复制代码
  • 经过源码咱们发现,$Proxy0类继承了Proxy类,同时实现了UserService接口。到这里,咱们的问题一就能解释了,为何JDK的动态代理只能基于接口实现,不能基于继承来实现?由于Java中不支持多继承,而JDK的动态代理在建立代理对象时,默认让代理对象继承了Proxy类,因此JDK只能经过接口去实现动态代理。ide

  • $Proxy0实现了UserService接口,因此重写了接口中的两个方法($Proxy0同时还重写了Object类中的几个方法)。因此当咱们调用query()方法时,先是调用到$Proxy0.query()方法,在这个方法中,直接调用了super.h.invoke()方法,父类是Proxy,父类中的h就是咱们定义的InvocationHandler,因此这儿会调用到UserServiceInvocationHandler.invoke()方法。所以当咱们经过代理对象去执行目标对象的方法时,会先通过InvocationHandler的invoke()方法,而后在经过反射method.invoke()去调用目标对象的方法,所以每次都会先打印invocation handler这句话。工具

public class UserServiceInvocationHandler implements InvocationHandler {

    // 持有目标对象
    private Object target;

    public UserServiceInvocationHandler(Object target){
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("invocation handler");
        // 经过反射调用目标对象的方法
        return method.invoke(target,args);
    }
}
复制代码
  • 对于文章开头的第二个问题,咱们先来看下以下示例代码
public class UserServiceImpl implements UserService{

    @Override
    public int insert() {
        System.out.println("insert");
        query();
        return 0;
    }

    @Override
    public String query() {
        System.out.println("query");
        return null;
    }
}
复制代码
public class MainApplication {

    public static void main(String[] args) {
        // 让代理对象的class文件写入到磁盘
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
        // 指明一个类加载器,要操做class文件,怎么少得了类加载器呢
        ClassLoader classLoader = MainApplication.class.getClassLoader();
        // 为代理对象指定要是实现哪些接口,这里咱们要为UserServiceImpl这个目标对象建立动态代理,因此须要为代理对象指定实现UserService接口
        Class[] classes = new Class[]{UserService.class};
        // 初始化一个InvocationHandler,并初始化InvocationHandler中的目标对象
        InvocationHandler invocationHandler = new UserServiceInvocationHandler(new UserServiceImpl());
        // 建立动态代理
        UserService userService = (UserService) Proxy.newProxyInstance(classLoader, classes, invocationHandler);
        // 执行代理对象的方法,经过观察控制台的结果,判断咱们是否对目标对象(UserServiceImpl)的方法进行了加强
        userService.insert();
    }
}
复制代码
  • 在这段代码中,只修改了UserServiceImpl的insert()方法,在打印以后,咱们调用了一下query()方法。在看打印结果以前,咱们先来分析下打印结果:按照咱们的理解$Proxy0代理类对UserServiceImpl中的方法都进行了加强,每次调用UserServiceImpl中类的方法时,应该都会通过InvocationHandler中的invoke()方法,每次都会打印invocation handler这句话。因此当咱们调用userService.insert()时,打印结果应该是:
invocation handler
insert
invocation handler
query
复制代码
  • 而后,结果然的如咱们想象的那样吗?下面截图为程序运行的结果:

控制台结果2

  • 从结果中,咱们发现,只打印了一次invocation handler。在调用query()方法时,并无去执行InvocationHandler中的invoke()方法。为何呢?缘由就在这个地方:
public int insert() {
    System.out.println("insert");
    query();
    return 0;
}
复制代码
  • 在insert()方法中调用query()方法时,实际上调用的是this.query(),而此时this对象是UserServiceImpl,并非$Proxy0这个代理对象,只有在调用代理对象的query()方法时,才会通过InvocationHandler.invoke()方法,因此此时只会打印一次invocation handler。因此第二个问题的答案就是:在调用过程当中,this的指向发生了变化。

5. 总结与思考

  • 本文介绍了JDK动态代理的常规用法,而后经过特殊手段,获取到了代理对象的class文件,而后根据class文件,分析了JDK动态代理中两个比较常见的问题。
  • 看到这里相信你已经明白了JDK动态代理的原理,以及JDK动态代理所带来的问题——在目标对象的方法A内部调用目标对象的其余方法B,此时动态代理并不会对方法B进行加强。
  • 相信不少人曾经碰到过在Spring项目中,经过@Transactional来声明事务时,出现了事务不生效的状况,结合本文,如今相信你应该能明白这其中的问题了吧?(关于Spring事务失效的例子,能够百度搜索: JDK 动态代理 事务失效)
  • 一样的案例还有@Async等案例,在同一个类的两个方法中开启异步,而后在方法内部进行互相调用,最终致使异步失效的问题。
  • 既然JDK动态代理存在这些问题,那如今Spring中是如何避免这些问题的呢?固然是大名鼎鼎的CGLIB了,关于CGLIB,下一篇文章分析。

6. 猜你喜欢

扫描下方二维码便可关注微信公众号菜鸟飞呀飞,一块儿阅读更多Spring源码。源码分析

微信公众号
相关文章
相关标签/搜索