[动态代理三部曲:上] - 动态代理是如何"坑掉了"我4500块钱

发布时间:2020-08-04 03:13:29 作者:MDove
来源:网络 阅读:3792

前言

不知道,起这个名字算不算是标题党呢?不过如果小伙伴们可以耐心看下去,因为会觉得不算标题党~
这是一个系列文章,目的在于通过动态代理这个很基础的技术,进而深入挖掘诸如:动态生成class;Class文件的结构;用到动态代理的框架源码分析。
对于三部曲来说,我初步打算:


对于这个系列的上篇来说,开篇我们先带着几个问题:

自己一直很想好好了解一波动态代理,无论是从技术角度,还是工作角度。
因为作为Android开发,我们日常开发离不开拥有着动态代理思想的Retrofit。而且就冲这个很洋气的名字,学是必须得学的。就算饿死,死外边,从这跳下去,我也要学明白动态代理。

按照正常动态代理的套路,我们需要写一个接口,然后实现接口,然后巴拉巴拉写一堆...写这么多为了干啥?谁呀?咋滴了?不知道啊?

不知道小伙伴们百度动态代理的文章时,是什么感受,反正我是上述的感受。写几行demo,就说深入理解动态代理了?那我学会写demo岂不是资深开发了?所以我个人认为,如果脱离业务去聊技术,恐怕没办法去深入理解这个项技术。所以关于动态代理我们(MDove+一支彩笔)会想办法写成一篇系列文章。后续我(MDove)会结合Android的部分,写一写能真正用起来的效果~


个人理解

首先,先谈一谈我们对动态代理的理解。网上很多资源喜欢把动态代理和静态代理放在一起去对比。这里我们就先不这么来做了,个人感觉静态代理本身重的是一种思想,而本篇动态代理着重去思考它代码套路背后的流程,所以就不放在一起啦。如果有对静态代理感兴趣的小伙伴,可以直接自行了解吧~

关于动态代理,个人喜欢把动态和代理分开理解:

动态:可随时变化的。对应我们编程,可以理解为在运行期去搞事情。

代理:接管我们真正的事务,去代我们执行。在我们生活中有很多充当代理的角色,比如:租房中介。

接下来让我们通过一个:租客通过中介租房子的demo,来展开动态代理的过程。(demo结束之后,我们会从源码的角度,去理解动态代理)

由浅

Demo效果

demo的开始,我们依旧是按照动态代理的语法规则开始入手。简单交代一下demo的剧情~我们有一个租客,身上揣着5000元钱,来到一个陌生的城市里。他想租一个房子,但是人生地不熟的,所以他选择了一个房屋中介...结果中介收了他4500元钱,我们的租客被坑了...

编写代码之前,让我们先看一下效果。

[动态代理三部曲:上] - 动态代理是如何

记住这个效果,接下来让我们一步步,看看租客是怎么被坑的~

开始编码

第一步,我们先把上当受骗的租客写出来,定义一个租客的接口

public interface IRentHouseProcessor {
    String rentHouse(String price);
}

接下来,便是实现InvocationHandler,编写我们动态代理的重头角色。

按照官方的docs文章,对InvocationHandler的解释:每个代理实例(Proxy,这里提到的Proxy代理实例是哪个?不要着急,往下看。)都有一个关联的调用处理程序(InvocationHandler)。在代理实例上调用方法时,方法会被调度到invoke中。

InvocationHandler是我们动态代理核心方法的一个核心参数。它的实例会在构建Proxy实例的时候以参数的形式传递进入,并在Proxy实例被调用的时候,将真正执行的方法,调度到自身的invoke方法里(形成代理)。后文从我们反编译的Proxy.class可以证实这个问题。

public class RentHouseProcessorHandler implements InvocationHandler {
    private Object target;

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

    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        Log.d(MainActivity.TAG, "-----------------------------");
        Log.d(MainActivity.TAG, "我是中介,有人找我租房子了,看看他能出多少钱:" + args[0]);
        Log.d(MainActivity.TAG, "我既然是中介,那我收他4000元的好处费,500块钱给你组个地下室,不过分吧?!!");
        Object result = method.invoke(target, new Object[]{"500"});
        Log.d(MainActivity.TAG, "赚了一大笔钱,美滋滋~");
        return result;
    }
}

让我们通过一张图来,仔细的理解一下invoke方法的每个参数的含义。

[动态代理三部曲:上] - 动态代理是如何

1.1.3、代理开始

编辑好了我们demo故事中的角色,那就让我们开始动态代理之路吧:
首先,我们不使用代理,直接通过租客的实例调用自身实现的接口。这里没啥好说的~只是为了剧情需要,更好的理解流程。

RentHouseProcessorImpl dpImpl = new RentHouseProcessorImpl();
dpImpl.rentHouse("5000");
Log.d(TAG,"我准备找中介去组个房子。");

使用动态代理:

RentHouseProcessorHandler handler = new RentHouseProcessorHandler(dpImpl);
IRentHouseProcessor proxy = (IRentHouseProcessor) Proxy.newProxyInstance(
        dpImpl.getClass().getClassLoader(),
        dpImpl.getClass().getInterfaces(),
        handler);

String content = proxy.rentHouse("5000");
Log.d(TAG, content);

这一步执行完毕,就会得到我们开篇的那个效果。我们的租客本来身上揣了5000元钱,当找了代理之后,真正租房的过程变成了中介(代理)去完成,所以租房的过程变得并不透明(invoke中,进行了一些额外的操作),因此我们的租客被坑了。

这一步我们来解释一下上述提到的那个疑问:代理实例在哪?这个代理实例其实就是Proxy.newProxyInstance()的返回值,也就是IRentHouseProcessor proxy这个对象。这里有一个很严肃的问题?IRentHouseProcessor是一个接口,接口是不可能被new出来的。

所以说proxy对象是一个特别的存在。没错它就是:动态代理动态生成出来的代理实例。而这个实例被动态的实现了我们的IRentHouseProcessor接口,因此它可以被声明为我们的接口对象。

上述docs文档提到,当我们调用proxy对象中的接口方法时,实际上会调度到InvocationHandler方法中的invoke方法中(这个操作同样是在动态生成的Proxy对象中被调度过去的)。

当方法到invoke中,那么问题就出现了:invoke是我们自己重写的,那也就是说:我们拥有至高无上的权利!

所以在我们的租房这个故事中,中介就是在这个invoke方法中,黑掉了我们租户的钱!因为invoke方法中它拥有绝对的操作权限。想干什么就干什么,甚至不执行我们真正想要执行的方法,我们的租客也没办法怎么样。

1.2、入深:“代理”在哪?

接下来让我们走进源码,来解决第一个大问题:1、动态代理,所谓的“动态”,“代理”都在哪?

走到这,不知道小伙伴对动态代理的流程是不是有了一个清晰的认识。动态代理的过程还是套路性比较强的:实现一个InvocationHandler类,在invoke中接受处理proxy对象调度过来的方法(Method)信息,方法执行到此,我们就可以为所欲为的做我们想做的事情啦。而我们的代理类实例是由系统帮我们创建了,我们只需要处理invoke中被调度的方法即可。

接下来让我们了解一下这个被动态生成的代理类实例。“代理”是如何被创建出来的~

1.2.1、“代理”在哪里呀?

第一步,让我们通过动态代理最开始的方法,Proxy.newProxyInstance()入手。

下面的代码,省略了一些判空/try-catch的过程,如果觉得省略不当,可以自行搜索对应的源码。

public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h) throws IllegalArgumentException {
    //省略:一些判空,权限校验的操作

    //[ 标注1 ]
    //想办法获取一个代理类的Class对象($Proxy0)
    Class<?> cl = getProxyClass0(loader, intfs);

    //省略:try-catch/权限检验

    //获取参数类型是InvocationHandler.class的代理类的构造方法对象($Proxy的构造方法的参数就是InvocationHandler类型)
    final Constructor<?> cons = cl.getConstructor(constructorParams);
    final InvocationHandler ih = h;

    //省略:cons.setAccessible(true)过程

    //传入InvocationHandler的实例去,构造一个代理类的实例
    return cons.newInstance(new Object[]{h});
    }
}
[ 标注1 ]

这部分代码,我们可以看到,调用了一个参数是ClassLoader、以及接口类型数组的方法。并且返回值是一个Class对象。实际上这里返回的c1实际上是我们的代理类的Class对象。何以见得?让我们点进去一看究竟:

//从缓存中取代理类的Class对象,如果没有通过ProxyClassFactory->ProxyGenerator去生成
private static Class<?> getProxyClass0(ClassLoader loader,
                                           Class<?>... interfaces) {
    if (interfaces.length > 65535) {
        throw new IllegalArgumentException("interface limit exceeded");
    }

    // 如果存在实现给定接口的给定加载器定义的代理类,则只返回缓存副本; 否则,它将通过ProxyClassFactory创建代理类
    return proxyClassCache.get(loader, interfaces);
}

1.2.2、跳过缓存,看背后

上述getProxyClass0方法中,进来之后我们会发现,代码量及其的少。这里很明显是通过了一个Cache对象去想办法获取我们所需要的Class对象。这部分设计到了动态代理的缓存过程,其中用的思想和数据结构比较的多,暂时就先不展开了(篇幅原因,以及也不是我们本次文章重点关注的对象)。如果有感兴趣的小伙伴,可以自行搜索了解呦~

Cache的get过程,最终会转向ProxyClassFactory这个类,由这个类先生成需要的代理类的Class对象。

private static final class ProxyClassFactory 
                implements BiFunction<ClassLoader, Class<?>[], Class<?>> {
    //代理类名称前缀
    private static final String proxyClassNamePrefix = "$Proxy";
    //用原子类来生成代理类的序号, 以此来确定唯一的代理类
    private static final AtomicLong nextUniqueNumber = new AtomicLong();
    @Override
    public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
        Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
        for (Class<?> intf : interfaces) {
            //这里遍历interfaces数组进行验证:是否可以由指定的类加载进行加载;是否是一个接口;是否有重复
        }
        //生成代理类的包名
        String proxyPkg = null;
        //生成代理类的访问权限, 默认是public和final
        int accessFlags = Modifier.PUBLIC | Modifier.FINAL;
        for (Class<?> intf : interfaces) {
            //[ 标注1 ]
            // 省略:验证所有非public的代理接口是否在同一个包中。不在则抛异常
            throw new IllegalArgumentException("non-public interfaces from different packages");
        }

        //省略部分代码:生成代理类的全限定名, 包名+前缀+序号, 例如:com.sun.proxy.$Proxy0
        String proxyName = proxyPkg + proxyClassNamePrefix + num;

        //!!接下来便进入重点了,用ProxyGenerator来生成字节码, 以byte[]的形式存放
        byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName,
                                  interfaces, accessFlags);
        //省略try-catch,根据二进制文件生成相应的Class实例
        return defineClass0(loader, proxyName, proxyClassFile, 
                              0, proxyClassFile.length);
    }
}
[ 标注1 ]

这部分,可能省略的比较多,因为内容主要是一些判断。这部分的做的事情是:遍历所有接口,看一下是不是public。如果不是,需要看一些些接口是不是在同一个包下,如果不是抛异常。这个很容易理解,非public接口还不在同一个包下,这没得搞啊~

接下来,让我们重点看一下ProxyGenerator.generateProxyClass(proxyName,interfaces, accessFlags);也就是代理类生成的方法。ProxyGenerator类可以通过查看OpenJDK获取

1.2.3、构造代理Class

接下来我们需要注意的是generateProxyClass,这个方法便是:这个Class被构造出来的缘由:

private byte[] generateClassFile() {
    //首先为代理类生成toString, hashCode, equals等代理方法(组装成ProxyMethod对象)
    addProxyMethod(hashCodeMethod, Object.class);
    addProxyMethod(equalsMethod, Object.class);
    addProxyMethod(toStringMethod, Object.class);

    //省略:遍历每一个接口的每一个方法, 并且为其生成ProxyMethod对象(遍历,调用addProxyMethod()方法)。省略校验过程。

    //省略try-catch:组装要生成的class文件的所有的字段信息和方法信息
    //添加构造器方法(methods:MethodInfo类型的ArrayList)
    methods.add(generateConstructor());
    //遍历缓存中的代理方法
    for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
        for (ProxyMethod pm : sigmethods) {
            //添加代理类的静态字段, 例如:private static Method m1;
            fields.add(new FieldInfo(pm.methodFieldName,
                        "Ljava/lang/reflect/Method;", ACC_PRIVATE | ACC_STATIC));
            //添加代理类的代理方法
            methods.add(pm.generateMethod());
        }
    }
    //添加代理类的静态字段初始化方法
    methods.add(generateStaticInitializer());

    //省略校验

    //通过class文件规则,最终生成我们的$Proxy.class文件
    //验证常量池中存在代理类的全限定名
    cp.getClass(dotToSlash(className));
    //验证常量池中存在代理类父类的全限定名, 父类名为:"java/lang/reflect/Proxy"
    cp.getClass(superclassName);
    //验证常量池存在代理类接口的全限定名
    for (int i = 0; i < interfaces.length; i++) {
        cp.getClass(dotToSlash(interfaces[i].getName()));
    }
    //接下来要开始写入文件了,设置常量池只读
    cp.setReadOnly();

    ByteArrayOutputStream bout = new ByteArrayOutputStream();
    DataOutputStream dout = new DataOutputStream(bout);

    //省略try-catch:1、写入魔数
    dout.writeInt(0xCAFEBABE);
    //2、写入次版本号
    dout.writeShort(CLASSFILE_MINOR_VERSION);
    //3、写入主版本号
    dout.writeShort(CLASSFILE_MAJOR_VERSION);

    // 省略其他写入过程

    //转换成二进制数组输出
    return bout.toByteArray();
}

// 封装构造方法
private MethodInfo generateConstructor() throws IOException {
    MethodInfo minfo = new MethodInfo("<init>", "(Ljava/lang/reflect/InvocationHandler;)V",ACC_PUBLIC);    
    DataOutputStream out = new DataOutputStream(minfo.code);
    code_aload(0, out);
    code_aload(1, out);

    out.writeByte(opc_invokespecial);
    out.writeShort(cp.getMethodRef(superclassName,"<init>", "(Ljava/lang/reflect/InvocationHandler;)V"));

    out.writeByte(opc_return);

    minfo.maxStack = 10;
    minfo.maxLocals = 2;
    minfo.declaredExceptions = new short[0];

    return minfo;
}

以上注释的内容,如果小伙伴们看过字节码格式的话,应该不陌生。这一部分内容就是去创建我们的代理类的Class字节码文件(字段/方法的描述符)。并通过ByteArrayOutputStream的作用,将我们手动生成的字节码内容转成byte[],并调用defineClass0方法,将其加载到内存当中。

如果对class文件结构感觉的小伙伴,可以查找一些相关的资料,或者《Java虚拟机规范》。当然也可以继续往下看:3、 Class 文件的格式。

末尾return方法,是一个native方法,我们不需要看实现,应该也能猜到,这里的内容是把我们的构造的byte[]加载到内存当中,然后获得对应的Class对象,也就是我们的代理类的Class。

private static native Class<?> defineClass0(ClassLoader var0, String var1, byte[] var2, int var3, int var4);

1.2.4、$Proxy0.class是什么样子?

OK,到这一步,我们的代理类的Class对象就生成出来了。因此我们Proxy.newProxyInstance()所返回出来的类也就很明确了。就是一个:拥有我们所实现接口类的所有方法结构的全新Class对象。也就是我们所说的代理类。

因为拥有我们接口的方法结构,所以可能调用我们的方法。不过着这个过程中,我们所调用的方法,被调度到InvocationHandler中的invoke方法里了。这一步,可能有小伙伴会问,为什么说我们的方法被调度到invoke之中了?要回答这个问题,我们需要看一下我们生成的Proxy代理类是什么样子的。

我总结了网上各种各样查看动态代理生成的.class文件的方法,贴一种成本最小的方式:
使用Eclipse,运行我们的动态代理的方法。运行之前,加上这么一行代码:

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

当然这样运行大概率,ide会报错

Exception in thread "main" java.lang.InternalError: 
I/O exception saving generated file: java.io.FileNotFoundException: com\sun\proxy\$Proxy0.class (系统找不到指定的路径。)

那怎么办呢?很简单,在src同级的建三级的文件夹分别是:com/sun/proxy。然后运行,就可以看到我们的$Proxy0.class啦。然后我们把它拖到AndroidStudio当中,查看反编译之后的结果:

public final class $Proxy0 extends Proxy implements IRentHouseProcessor {
    private static Method m3;
    private static Method m1;
    private static Method m0;
    private static Method m2;

    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

    public final String rentHouse(String var1) throws  {
        try {
            return (String)super.h.invoke(this, m3, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final boolean equals(Object var1) throws  {
        try {
            return ((Boolean)super.h.invoke(this, m1, new Object[]{var1})).booleanValue();
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final int hashCode() throws  {
        try {
            return ((Integer)super.h.invoke(this, m0, (Object[])null)).intValue();
        } 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);
        }
    }

    static {
        try {
            m3 = Class.forName("proxy.IRentHouseProcessor").getMethod("rentHouse", new Class[]{Class.forName("java.lang.String")});
            m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[]{Class.forName("java.lang.Object")});
            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

看了Proxy的代码,为什么会被调度到invoke方法中就很清晰了吧?

1.3、入深:“动态”在哪?

我们走完上诉1.2的过程,其实“动态”在哪这个问题的答案已经很明确了吧?ProxyGenerator.generateProxyClass(proxyName,interfaces, accessFlags);方法之中JDK本身就直接帮我们动态的构建了我们所需要的$Proxy0类。

1.4、动态代理如何生成 Class 文件?

这个问题的答案,我们也可以从上诉的过程之中找到答案。在对应生成$Proxy的过程中,我们往DataOutputStream之中写入我们class文件所规定的内容;此外写入了我们字段/方法的描述符。然后通过DataOutputStram将我们的内容转成二进制数组。最后交由我们的native方法,去将此class文件加载到内存之中。

结语

小伙伴们一步步追了下来,不知道有没有对动态代理的过程有了比较清晰的认识。
接下来的内容,会针对动态代理进行实际应用场景的编写;以及对Retrofit动态代理相关内容的分析。

推荐阅读:
  1. 如何在Spring框架中实现动态代理
  2. 深入浅析java 1.8 中动态代理的原理

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

动态代理 java class

上一篇:PLSQL无法连接64位Oracle 解决方法

下一篇:云计算技术体系结构由这四点组成

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》