阅读 2979

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

前言

不知道,起这个名字算不算是标题党呢?内容很长,如果小伙伴们可以耐心看下去,相信会觉得不算标题党~

自己一直很想好好了解一波动态代理,无论是从技术角度,还是工作角度。而且就冲这个很洋气的名字,学是必须得学的。就算饿死,死外边,从这跳下去,我也要学明白动态代理。

如果感觉太啰嗦,可以直接拉至结尾处,看总结~

个人理解

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

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

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

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

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

由浅

Demo效果

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

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

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

开始编码

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

public interface IRentHouseProcessor {
    String rentHouse(String price);
}
复制代码

接下来,实现这个接口,充当我们倒霉的租客:

public class RentHouseProcessorImpl implements IRentHouseProcessor {
    @Override
    public String rentHouse(String price) {
        Log.d(MainActivity.TAG, "我还剩:"+price+"元");
        String content = "我是租客,我找中介租了一个房子,我感觉被坑了";
        return content;
    }
}
复制代码

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

按照官方的docs文章,对InvocationHandler的解释:每个代理实例(Proxy)都有一个关联的调用处理程序(InvocationHandler)。在代理实例上调用方法时,方法会被调度到invoke中。

这里提到的Proxy代理实例是哪个?不要着急,往下看。

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;
    }
}
复制代码

开始执行

开始我们的动态代理之路: 我们不使用代理,我们直接通过租客的实例调用自身实现的接口。这里没啥好说的~只是为了剧情需要,更好的理解流程。

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);
复制代码

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

所以说proxy对象是一个特别的存在。没错它就是:动态代理,动态生成出来的代理实例。而这个实例拥有我们接口对象的方法结构,因此它可以是我们的接口类型,进而也就可以调用我们的接口方法。

上述docs文档提到,当我们调用proxy对象中的接口方法时,实际上会调度到InvocationHandler方法中的invoke方法中。

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

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

入深

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

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

开启代码之旅

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

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

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

    //[ 标注1 ]
    //想办法获取一个代理类的Class对象
    Class<?> cl = getProxyClass0(loader, intfs);
	
	//省略:try-catch/权限检验
        
    //获取参数类型是InvocationHandler.class的代理类的构造方法对象
    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);
}
复制代码

跳过缓存,看背后

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

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

//代理类生成工厂
//一个工厂函数,在给定ClassLoader和接口数组的情况下生成,定义和返回代理类。
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数组进行验证, 主要做三件事情
            //1.intf是否可以由指定的类加载进行加载
            //2.intf是否是一个接口
            //3.intf在数组中是否有重复
        }
        //生成代理类的包名
        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");
        }

        //如果接口都是public的话, 那生成的代理类都放到默认的包下:com.sun.proxy
        if (proxyPkg == null) {
            proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
        }

        //生成代理类的序号
        long num = nextUniqueNumber.getAndIncrement();

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

        //!!接下来便进入重点了,用ProxyGenerator来生成字节码, 以byte[]的形式存放
        byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName,
                                  interfaces, accessFlags);
        try {
            //根据二进制文件生成相应的Class实例
            return defineClass0(loader, proxyName, proxyClassFile, 
                              0, proxyClassFile.length);
        } catch (ClassFormatError e) {
            throw new IllegalArgumentException(e.toString());
        }
    }
}
复制代码

[ 标注1 ]

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

构造代理Class

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

private byte[] generateClassFile() {
    //第一步, 将所有的方法组装成ProxyMethod对象
    //首先为代理类生成toString, hashCode, equals等代理方法
    addProxyMethod(hashCodeMethod, Object.class);
    addProxyMethod(equalsMethod, Object.class);
    addProxyMethod(toStringMethod, Object.class);
    //遍历每一个接口的每一个方法, 并且为其生成ProxyMethod对象
    for (int i = 0; i < interfaces.length; i++) {
        Method[] methods = interfaces[i].getMethods();
        for (int j = 0; j < methods.length; j++) {
            addProxyMethod(methods[j], interfaces[i]);
        }
    }
    //对于具有相同签名的代理方法, 检验方法的返回值是否兼容
    for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
        checkReturnTypes(sigmethods);
    }
    
    //第二步, 组装要生成的class文件的所有的字段信息和方法信息
    try {
        //添加构造器方法
        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());
    } catch (IOException e) {
        throw new InternalError("unexpected I/O Exception");
    }
    
    //验证方法和字段集合不能大于65535
    if (methods.size() > 65535) {
        throw new IllegalArgumentException("method limit exceeded");
    }
    if (fields.size() > 65535) {
        throw new IllegalArgumentException("field limit exceeded");
    }

    //第三步, 写入最终的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 {
        //1.写入魔数
        dout.writeInt(0xCAFEBABE);
        //2.写入次版本号
        dout.writeShort(CLASSFILE_MINOR_VERSION);
        //3.写入主版本号
        dout.writeShort(CLASSFILE_MAJOR_VERSION);
        //4.写入常量池
        cp.write(dout);
        //5.写入访问修饰符
        dout.writeShort(ACC_PUBLIC | ACC_FINAL | ACC_SUPER);
        //6.写入类索引
        dout.writeShort(cp.getClass(dotToSlash(className)));
        //7.写入父类索引, 生成的代理类都继承自Proxy
        dout.writeShort(cp.getClass(superclassName));
        //8.写入接口计数值
        dout.writeShort(interfaces.length);
        //9.写入接口集合
        for (int i = 0; i < interfaces.length; i++) {
            dout.writeShort(cp.getClass(dotToSlash(interfaces[i].getName())));
        }
        //10.写入字段计数值
        dout.writeShort(fields.size());
        //11.写入字段集合 
        for (FieldInfo f : fields) {
            f.write(dout);
        }
        //12.写入方法计数值
        dout.writeShort(methods.size());
        //13.写入方法集合
        for (MethodInfo m : methods) {
            m.write(dout);
        }
        //14.写入属性计数值, 代理类class文件没有属性所以为0
        dout.writeShort(0);
    } catch (IOException e) {
        throw new InternalError("unexpected I/O Exception");
    }
    //转换成二进制数组输出
    return bout.toByteArray();
}
复制代码

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

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


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

复制代码

总结

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方法中就很清晰了吧?


结语

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

最后打个广告(我们一起维护的学习公众号)

公众号主要面向的是初级/应届生。内容包含我们从应届生转换为职场开发所踩过的坑,以及我们每周的学习计划和学习总结。 内容会涉及计算机网络算法等基础;也会涉及前端,后台,Android等内容~

求关注,不迷路

我们基友团其他朋友的文章:

Android基友

关注下面的标签,发现更多相似文章
评论

查看更多 >