「Java」静态代理、动态代理及 $Proxy0 类源码分析

从前从前,有个面试官问我动态代理和静态代理的区别,我当时支支吾吾没说清楚,只提到了动态代理须要实现InvocationHandler接口,而后使用Proxy类反射建立实例云云。至于静态代理……这玩意不就是一种设计思想?java

面试官笑了笑,今后天涯路人不相逢。程序员

我痛定思痛,必定要把代理这一块搞懂,因而乎有了这篇文章。之后不再怕面试官问我关于静态代理和动态代理的问题了!面试

1. 什么是代理

说到代理,就不得不提设计模式中的代理模式,代理模式就是对代理思想的一种设计模式实现。数据库

百度百科对于代理模式的定义是这样的:设计模式

为其余对象提供一种代理以控制对这个对象的访问。在某些状况下,一个对象不适合或者不能直接引用另外一个对象,而代理对象能够在客户端和目标对象之间起到中介的做用。

在这段定义中有这样两个字:中介。app

我想下面这个例子能够比较好的解释代理模式。ide

相信在一个陌生的城市打拼的程序员们在初期都会遇到这样一个问题:租房。咱们一般有三种方式,第一能够本身在闲鱼、豆瓣、自如等信息网站去找房源,第二直接去心仪的小区公告栏看看有没有招租信息(固然可能被中介的广告霸占),第三就是联系房产中介,中介会帮你挑选你想要租的房子,只不过须要付一笔服务费。工具

2020052803

假设我选择委托中介来租房,在这个过程当中就能够把房子抽象为一个类,这是我最终想要获得的东西。而后把帮我租房的中介抽象为一个类,经过委托中介,我能够获得本身想要的房子。同时,这两个类实现了相同的接口,能够这么去理解这里相同接口的做用:房子经过接口注册在数据库中,中介经过接口找到了注册在数据库中的房子。源码分析

而我委托中介帮我找房子的这个过程,就是代理。测试

2. 定义与类图

在根据上面说的租房的例子来编写实际的代码做为静态代理的示例以前,首先得了解一下代理模式中的几个角色,代理模式中有三个主要角色,即抽象主题角色、真实角色(被代理角色)以及代理类角色。

2.1. 主题角色 (Subject)

主题角色能够是接口,也能够是抽象类。它定义了真实角色和代理类角色共有的方法,主题类让这二者具备一致性。

同时,也正是基于主题角色,才能实现代理的功能。

2.2. 真实角色 (RealSubject)

真实角色就是被代理类,在上面的例子中就是房子,同时也是具体业务逻辑的执行者。

2.3. 代理类角色 (Proxy)

代理类角色的内部含有对真实角色 RealSubject 的引用,它负责对被代理角色的调用,并在被代理角色处理先后作预处理和后处理。

在上面租房的例子中就是房产中介。

2.4. Client

有人可能会问了,那“我”呢?简单点说,其实“我”就是测试方法中的 main 方法,负责调用代理角色的方法。

2.5 类图

2020052804

图片来源于 Proxy模式——静态代理

3. 静态代理

所谓的静态代理,就是在程序启动以前代理类的 .class 文件就已经存在。而代理类多是程序员直接建立的 .java 文件,或者是借助某些工具生成的 .java 文件,但无一例外都必须再由编译器编译成 .class 文件以后再启动程序。

3.1. 静态代理实现

基于上面租房的例子使用代码实现。

首先建立主题角色,它是一个接口,这个接口拥有一个方法,而这个方法是须要被其余两个角色重写的。

public interface Subject {

    /**
     * 各个角色的公用方法
     */
    void job();
}

而后是真实角色,也就是被代理角色、真正的业务逻辑执行者。

public class House implements Subject {

    @Override
    public void job() {
        System.out.println("我是客户想要的房子,经过 job 方法注册在数据库中");
    }
}

而后代理类,它可以加强被代理角色的方法。代理类就是帮助我”找到好房子“的房产中介。

当一个房产中介拥有一个客户(RealSubject)时,才会发挥他的做用,在个人”意图“被实现先后,它分别能够对个人”意图“进行加强。

public class Proxy implements Subject {

    private RealSubject realSubject;

    public Proxy(RealSubject realSubject) {
        this.realSubject = realSubject;
    }

    @Override
    public void job() {
        System.out.println("我是中介,我会在数据库中检索,帮助客户找到心仪的房子");
        house.job();
        System.out.println("我是中介,找到了数据库中符合客户需求的方法");
    }
}

最后,“我”出场了,“我”委托中介寻找房子。

public class Client {
    public static void main(String[] args) {
        // 我可能心仪的房子
        House house = new House();
        // 代理类——房产中介
        Proxy proxy = new Proxy(house);
        // "我"委托中介去寻找房子
        proxy.job();
    }   
}

最终“我”执行的是代理类(中介)的 job 方法,因为代理类持有一个真实角色(房子),程序又会执行真实角色的 job 方法,这样就实现了“我”委托中介找到房子的静态代理过程。

3.2. 静态代理的优缺点

3.2.1. 优势

  1. 业务类只须要关注业务逻辑自己,保证了业务类的重用性。
  2. 客户端只须要知道代理,无需关注具体实现。
中介只须要关注本身能找房子的效率和质量就能够了,不管谁想来委托中介,都能找到房子。而“我”不须要知道中介是如何找房子的,只要他帮我找到房子,就能够了。

3.2.2. 缺点

  1. 因为代理类和被代理类都实现了主题接口,它们都有相同的方法,致使大量代码重复。同时若是主题接口新增了一个方法,那么代理类与被代理类也都须要实现这个方法,增长了维护代码的复杂度。
  2. 若是代理类要为其余真实角色提供委托服务的话,就须要实现其余的接口,当规模变大时也会增长代码复杂度。
若是中介不只提供租房服务,还提供打游戏、卖房子、卖电影票、卖彩票、陪聊天、陪玩游戏等等一系列服务,那么他将变得无比庞杂,没有人敢动他(这里的他指代码)。

4. 动态代理

上面讨论的是静态代理,接下来再聊聊动态代理。那么什么是动态代理呢?

所谓的动态代理,就是在程序运行时建立代理类的代理方式。而这也是静态代理和动态代理的区别。

4.1. 动态代理实现

既然是在程序运行时生成的代理类,那么必然须要借助其余的工具来生成,而在 Java 中就是经过 java.lang.reflect.Proxy 类来生成代理类的。同时,还须要实现InvocationHandler接口来实现方法调用,下面就用代码来实现动态代理。

一样是上面租房的例子,接口Subject不变,被代理类House也不变,须要新建一个动态代理类。

public class DynamicProxy implements InvocationHandler {

    private Object target;

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

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("被代理的类:" + proxy.getClass());
        System.out.println("被代理的类的执行方法:" + method.getName());
        Object object = method.invoke(target, args);
        System.out.println("被代理的类的方法执行完成");
        return object;
    }
}

动态代理类实现了InvocationHandler接口,同时与静态代理同样,在它内部也持有一个对象,这个对象正是被代理对象,而后在代理类的invoke方法中调用具体的方法。

最后再客户端,也就Client角色中编写测试代码。

import java.lang.reflect.Proxy;

public class Client {

    public static void main(String[] args) {
    // 我可能心仪的房子
    Subject subject = new House();
    // 代理类——房产中介
    DynamicProxy dynamicProxy = new DynamicProxy(subject);
    // 获取代理类
    Subject proxyInstance = (Subject) Proxy.newProxyInstance(subject.getClass().getClassLoader(),
            subject.getClass().getInterfaces(),
            dynamicProxy);
    // "我"委托中介去寻找房子
    proxyInstance.job();
    }
}

与静态代理不一样的是,咱们须要经过Proxy.newProxyInstance方法来实例化动态生成的代理类,而这个方法中的参数分别表明的意义是:

  • ClassLoader loader: 被代理类的类加载器
  • Class<?>[] interfaces: 被代理类实现的接口
  • InvocationHandler h: 实现指定接口InvocationHandler的实现类

须要这三个参数的缘由是:须要经过与被代理类相同的类加载器去加载动态生成的代理类,同时代理类须要实现与被代理类相同的接口,最后须要经过实现指定接口InvocationHandler的实现类来完成代理调用方法的功能。

最终的输出结果是:

被代理的类:class com.sun.proxy.$Proxy0
被代理的类的执行方法:job
我是客户想要的房子,经过 job 方法注册在数据库中
被代理的类的方法执行完成

咱们能够看到生成的代理类是com.sun.proxy.$Proxy0,经过动态代理它完成了与静态代理同样的委托任务。

4.2. 动态代理的优缺点

与静态代理相比,动态代理还具备不须要本身写代理类的优势,由于代理类时运行时程序自动生成的。

同时,动态代理的必须先实现InvocationHandler接口,而后使用Proxy类中的newProxyInstance方法动态的建立代理类,这就致使了动态代理只能代理接口。

5. 动态代理类源码分析

上文说到运行时生成的动态代理类会继承于java.lang.reflect.Proxy类,这是为何呢?

5.1. 获取动态代理类源码

咱们能够经过设置系统参数来保存动态生成的代理类。

System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");

你问我是怎么知道这个参数的?我也不知道,是 jdk 源码里面写死的。

有兴趣的同窗能够跟踪一下Proxy.newProxyInstance这个方法,在通过数次跳转后,你就能找到这个系统参数了,下面给出调用链。

Proxy.newProxyInstance
->getProxyClass0
->proxyClassCache.get(loader, interfaces)
->subKeyFactory.apply(key, parameter)
->ProxyClassFactory.apply
->ProxyGenerator.generateProxyClass
->saveGeneratedFiles
->private static final boolean saveGeneratedFiles = (Boolean)AccessController.doPrivileged(new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles"))

你问我怎么知道调用链是这样的?看注释啊...

在开启saveGeneratedFiles参数后,咱们会发如今项目中多出了com.sun.proxy.$Proxy0类,打开它就是生成的动态代理类源码。

public final class $Proxy0 extends Proxy implements Subject {
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;
    // equals 和 hashCode 方法省略...

    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 job() throws  {
        try {
            super.h.invoke(this, m3, (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");
            m3 = Class.forName("org.planeswalker.proxy.statical.Subject").getMethod("job");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

5.2. 为何要重写equalstoStringhashCode方法

能够看到,在动态代理类中有四个私有静态成员变量,结合 static 代码块,咱们知道这四个Method分别表明了equalstoStringjobhashCode方法。

job方法很好理解,由于这是我须要动态代理类去调用被代理类的方法。而另外三个方法,为何须要重写?

从源码中能够看到,这三个方法实际是调用了InvocationHandler接口实现类的相应方法。而咱们知道动态代理类其实至关于一个中间件,经过动态代理类咱们实际想要调用的是被代理类的方法,这么一想就很好理解了——重写这三个方法的缘由是为了让动态代理类与被代理类划上”≈“号。

若是没有重写这三个方法,那么它们的hashcodetoString将会返回不一样值,这样实现的动态代理类也就不完善了。

为何说是”≈“号而不是”=“号呢?由于动态代理类实际是一个com.sun.proxy.$Proxy0类,虽然它具备与被代理类相同的状态(包括大部分方法与属性),但实际上这两个类经过equals方法来比较返回的会是 false,由于它们的内存地址是不同的。

被代理类未重写 equals方法,因此调用的是 Object#equals,而这里比较的是内存地址。

5.3 为何动态代理类要继承 Proxy 类

这个问题其实应该去问 jdk 的实现者,这是他们规定的,哪来的为何?

我也去网上搜索了不少相关的问题,大部分仍是指向了一个答案——继承 Proxy 类能够减小代码的冗余度。

在上面给出的动态生成的代理类源码中咱们能够知道,动态代理类其实只是作了一个转发,调用的仍是被代理类的方法。若是咱们将被代理类的属性和方法都写在动态代理类中,而在调用方法时依旧是经过转发,那么这些被继承的属性和方法其实是没有用到的,这对于内存空间来讲是一种浪费。

6. 小结

本文讲述了代理模式、代理模式中的角色、静态代理代码实现以及优缺点、静态代理与动态代理的区别、动态代理代码实现及优缺点。

同时还提出了两个问题以及我对这两个的理解。

但愿能够帮助到你们。

以上。

相关文章
相关标签/搜索