之前的文章中,提到了动态代理,先举一个比较常见的应用场景,在JPA中仅仅是实现了Jps两个接口,就自动生成了类,帮助我们完成Sql基本操作,并且层层封装,从配置、驱动、到SQL语句的写入都帮我们完成,换句话说,动态的生成了一个方法完成了操作,这也就是动态代理操作,但是文章中并没有很仔细的进行说明,因此在本文中将进行深入的分析。
概述
我们从原始的java类来进行源分析,通常是我们编写完程序,调用javac编译成class文件,通过类加载机制载入jvm,然后就可以运行了。
但是我们也可以不使用这个办法,也可以用官方的java.compiler去编译,因为只要是符合 jvm字节码标准的都可以拿来编译,而java.compiler库下的方法拥有和javac对等的编译方式,进一步思考,我们已经知道了jvm编译可以识别的字节码,那么,自然也可以用之前提到的cglib asm去转化实现。
面试角度来说,主要有以下几点:
- 字节码和类加载到底是怎么实现无缝转换的?发生在整个类加载过程的哪一步?
- 如何利用字节码操作技术、实现基本动态代理
- 除了动态代理,字节码操作还有哪些应用场景
扩展
首先了解一下类从字节码到Class对象的转换:
@Deprecated
protected final Class<?> defineClass(byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(null, b, off, len, null);
}
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}
可以看出,只要能生出规范的字节码,不管是作为byte数组形式还是放到ByteBuffer里,都可以平滑的完成字节码到Java对象的转换过程
JDK提出的defineClass方法,最终都是本地代码实现的
private native Class<?> defineClass1(String name, byte[] b, int off, int len,
ProtectionDomain pd, String source);
private native Class<?> defineClass2(String name, java.nio.ByteBuffer b,
int off, int len, ProtectionDomain pd,
String source);
更进一步,我们来看看JDK dynamic proxy的实现代码。你会发现,对应逻辑是实现在ProxyBuilder这个静态内部类中,ProxyGenerator生成字节码,并以byte数组的形式保 存,然后通过调用Unsafe提供的defneClass入口。
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags);
try {
return defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
throw new IllegalArgumentException(e.toString());
}
从以上的代码我们知道了字节码->Class对象的转换过程,但是还不知道如何生成自己需要的字节码,因此接下来看看字节码的操作逻辑:
JDK内部动态代理的逻辑,可以参考java.lang.refect.ProxyGenerator的内部实现。可以认为这是种另类的字节码操纵技术,其利用了DataOutputStrem提供的能力,配合hard-coded的各种JVM指令实现方法,生成所需的字节码数组,可以参考下面代码:
private void codeLocalLoadStore(int var1, int var2, int var3, DataOutputStream var4) throws IOException {
assert var1 >= 0 && var1 <= 65535;
if (var1 <= 3) {
var4.writeByte(var3 + var1);
} else if (var1 <= 255) {
var4.writeByte(var2);
var4.writeByte(var1 & 255);
} else {
var4.writeByte(196);
var4.writeByte(var2);
var4.writeShort(var1 & '\uffff');
}
}
我们可以看出,这种代码实现起来没有什么依赖关系,简单实用但是又需要知道一些JVM命令,知道如何处理偏移地址。因此不适合普通的开发场景
知道了字节码相关操作逻辑,可以简单思考一下,如果需要实现一个简单的动态代理,如何使用字节码技术来实现走通这个流程
对于一个普通的Java动态代理,实现过程可以简化为:
- 提供一个基础的接口(可以参照动态代理那篇专栏举的例子),根据接口对应的实现类,使用getClassLoader,利用Proxy的newProxyInstance方法实现调用接口即能直接实现最终的java实体类方法(JPA就是此原理)
可以看下面方法签名具体来了解:
public satic Object newProxyInsance(
ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h
)
字节码的操作技术,除了使用在动态代理,还可以使用在以下:
- Mock
- ORM
- IOC
- Profiler工具
- 生成形式化代码
甚至还可以认为,字节码操纵技术是工具和基础框架必不可少的部分,大大减少了开发者的负担。