不知道,起这个名字算不算是标题党呢?不过如果小伙伴们可以耐心看下去,因为会觉得不算标题党~
这是一个系列文章,目的在于通过动态代理这个很基础的技术,进而深入挖掘诸如:动态生成class;Class文件的结构;用到动态代理的框架源码分析。
对于三部曲来说,我初步打算:
对于这个系列的上篇来说,开篇我们先带着几个问题:
自己一直很想好好了解一波动态代理,无论是从技术角度,还是工作角度。
因为作为Android开发,我们日常开发离不开拥有着动态代理思想的Retrofit。而且就冲这个很洋气的名字,学是必须得学的。就算饿死,死外边,从这跳下去,我也要学明白动态代理。
按照正常动态代理的套路,我们需要写一个接口,然后实现接口,然后巴拉巴拉写一堆...写这么多为了干啥?谁呀?咋滴了?不知道啊?
不知道小伙伴们百度动态代理的文章时,是什么感受,反正我是上述的感受。写几行demo,就说深入理解动态代理了?那我学会写demo岂不是资深开发了?所以我个人认为,如果脱离业务去聊技术,恐怕没办法去深入理解这个项技术。所以关于动态代理我们(MDove+一支彩笔)会想办法写成一篇系列文章。后续我(MDove)会结合Android的部分,写一写能真正用起来的效果~
首先,先谈一谈我们对动态代理的理解。网上很多资源喜欢把动态代理和静态代理放在一起去对比。这里我们就先不这么来做了,个人感觉静态代理本身重的是一种思想,而本篇动态代理着重去思考它代码套路背后的流程,所以就不放在一起啦。如果有对静态代理感兴趣的小伙伴,可以直接自行了解吧~
关于动态代理,个人喜欢把动态和代理分开理解:
动态:可随时变化的。对应我们编程,可以理解为在运行期去搞事情。
代理:接管我们真正的事务,去代我们执行。在我们生活中有很多充当代理的角色,比如:租房中介。
接下来让我们通过一个:租客通过中介租房子的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方法的每个参数的含义。
编辑好了我们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、动态代理,所谓的“动态”,“代理”都在哪?
走到这,不知道小伙伴对动态代理的流程是不是有了一个清晰的认识。动态代理的过程还是套路性比较强的:实现一个InvocationHandler类,在invoke中接受处理proxy对象调度过来的方法(Method)信息,方法执行到此,我们就可以为所欲为的做我们想做的事情啦。而我们的代理类实例是由系统帮我们创建了,我们只需要处理invoke中被调度的方法即可。
接下来让我们了解一下这个被动态生成的代理类实例。“代理”是如何被创建出来的~
第一步,让我们通过动态代理最开始的方法,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});
}
}
这部分代码,我们可以看到,调用了一个参数是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);
}
上述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);
}
}
这部分,可能省略的比较多,因为内容主要是一些判断。这部分的做的事情是:遍历所有接口,看一下是不是public。如果不是,需要看一些些接口是不是在同一个包下,如果不是抛异常。这个很容易理解,非public接口还不在同一个包下,这没得搞啊~
接下来,让我们重点看一下ProxyGenerator.generateProxyClass(proxyName,interfaces, accessFlags);
也就是代理类生成的方法。ProxyGenerator类可以通过查看OpenJDK获取
接下来我们需要注意的是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);
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.2的过程,其实“动态”在哪这个问题的答案已经很明确了吧?ProxyGenerator.generateProxyClass(proxyName,interfaces, accessFlags);
方法之中JDK本身就直接帮我们动态的构建了我们所需要的$Proxy0类。
这个问题的答案,我们也可以从上诉的过程之中找到答案。在对应生成$Proxy的过程中,我们往DataOutputStream之中写入我们class文件所规定的内容;此外写入了我们字段/方法的描述符。然后通过DataOutputStram将我们的内容转成二进制数组。最后交由我们的native方法,去将此class文件加载到内存之中。
小伙伴们一步步追了下来,不知道有没有对动态代理的过程有了比较清晰的认识。
接下来的内容,会针对动态代理进行实际应用场景的编写;以及对Retrofit动态代理相关内容的分析。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。