共计 4155 个字符,预计需要花费 11 分钟才能阅读完成。
概述
咱们能够从常见的 Java 类起源剖析,通常的开发过程是,开发者编写 Java 代码,调用 javac 编译成 class 文件,而后通过类加载机制载入 JVM,就成为利用运行时能够应用的 Java 类了。
从下面过程失去启发,其中一个间接的形式是从源码动手,能够利用 Java 程序生成一段源码,而后保留到文件等,上面就只须要解决编译问题了。
有一种笨办法,间接用 ProcessBuilder 之类启动 javac 过程,并指定下面生成的文件作为输出,进行编译。最初,再利用类加载器,在运行时加载即可。
后面的办法,实质上还是在以后程序过程之外编译的,那么还有没有不这么 low 的方法呢?
你能够思考应用 Java Compiler API,这是 JDK 提供的规范 API,外面提供了与 javac 对等的编译器性能,具体请参考 java.compiler 相干文档。
进一步思考,咱们始终围绕 Java 源码编译成为 JVM 能够了解的字节码,换句话说,只有是合乎 JVM 标准的字节码,不论它是如何生成的,是不是都能够被 JVM 加载呢?咱们能不能间接生成相应的字节码,而后交给类加载器去加载呢?当然也能够,不过间接去写字节码难度太大,通常咱们能够利用 Java 字节码操纵工具和类库来实现.
注释
首先来了解一下,类从字节码到 Class 对象的转换,在类加载过程中,这一步是通过上面的办法提供的性能,或者 defineClass 的其余本地对等实现。
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,
ProtectionDomain protectionDomain)
复制代码
这里只选取了最根底的两个典型的 defineClass 实现,Java 重载了几个不同的办法。
能够看出,只有可能生成出标准的字节码,不论是作为 byte 数组的模式,还是放到 ByteBuffer 里,都能够平滑地实现字节码到 Java 对象的转换过程。
JDK 提供的 defineClass 办法,最终都是本地代码实现的。
static native Class<?> defineClass1(ClassLoader loader, String name, byte[] b, int off, int len,
ProtectionDomain pd, String source);
static native Class<?> defineClass2(ClassLoader loader, String name, java.nio.ByteBuffer b,
int off, int len, ProtectionDomain pd, String source);
复制代码
更进一步,咱们来看看 JDK dynamic proxy 的实现代码。你会发现,对应逻辑是实现在 ProxyBuilder 这个动态外部类中,ProxyGenerator 生成字节码,并以 byte 数组的模式保留,而后通过调用 Unsafe 提供的 defineClass 入口。
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces.toArray(EMPTY_CLASS_ARRAY), accessFlags);
try {
Class<?> pc = UNSAFE.defineClass(proxyName, proxyClassFile,
0, proxyClassFile.length,
loader, null);
reverseProxyCache.sub(pc).putIfAbsent(loader, Boolean.TRUE);
return pc;
} catch (ClassFormatError e) {.
// 如果呈现 ClassFormatError,很可能是输出参数有问题,比方,ProxyGenerator 有 bug
}
复制代码
后面理顺了二进制的字节码信息到 Class 对象的转换过程,仿佛咱们还没有剖析如何生成本人须要的字节码,接下来一起来看看相干的字节码操纵逻辑。
JDK 外部动静代理的逻辑,能够参考 java.lang.reflect.ProxyGenerator 的外部实现。我感觉能够认为这是种另类的字节码操纵技术,其利用了 DataOutputStrem 提供的能力,配合 hard-coded 的各种 JVM 指令实现办法,生成所需的字节码数组。
private void codeLocalLoadStore(int lvar, int opcode, int opcode_0,
DataOutputStream out)
throws IOException
{
assert lvar >= 0 && lvar <= 0xFFFF;
// 依据变量数值,以不同格局,dump 操作码
if (lvar <= 3) {out.writeByte(opcode_0 + lvar);
} else if (lvar <= 0xFF) {
out.writeByte(opcode);
out.writeByte(lvar & 0xFF);
} else {
// 应用宽指令修饰符,如果变量索引不能用无符号 byte
out.writeByte(opc_wide);
out.writeByte(opcode);
out.writeShort(lvar & 0xFFFF);
}
}
复制代码
这种实现形式的益处是没有太多依赖关系,简略实用,然而前提是你须要懂各种 JVM 指令,晓得怎么解决那些偏移地址等,理论门槛十分高,所以并不适宜大多数的一般开发场景。
幸好,Java 社区专家提供了各种从底层到更高形象程度的字节码操作类库,咱们不须要什么都本人从头做。JDK 外部就集成了 ASM 类库,尽管并未作为公共 API 裸露进去,然而它广泛应用在,如 java.lang.instrumentation API 底层实现,或者 Lambda Call Site 生成的外部逻辑中,这些代码的实现我就不在这里开展了,如果你的确有趣味或有须要,能够参考相似 LamdaForm 的字节码生成逻辑:java.lang.invoke.InvokerBytecodeGenerator。
从绝对实用的角度思考一下,实现一个简略的动静代理,都要做什么?如何应用字节码操纵技术,走通这个过程呢?
对于一个一般的 Java 动静代理,其实现过程能够简化成为:
提供一个根底的接口,作为被调用类型(com.mycorp.HelloImpl)和代理类之间的对立入口,如 com.mycorp.Hello。
实现 InvocationHandler,对代理对象办法的调用,会被分派到其 invoke 办法来真正实现动作。
通过 Proxy 类,调用其 newProxyInstance 办法,生成一个实现了相应根底接口的代理类实例,能够看上面的办法签名。
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
复制代码
咱们剖析一下,动静代码生成是具体产生在什么阶段呢?
不错,就是在 newProxyInstance 生成代理类实例的时候。我选取了 JDK 本人采纳的 ASM 作为示例,一起来看看用 ASM 实现的简要过程,请参考上面的示例代码片段。
第一步,生成对应的类,其实和咱们去写 Java 代码很相似,只不过改为用 ASM 办法和指定参数,代替了咱们书写的源码。
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
cw.visit(V1_8, // 指定 Java 版本
ACC_PUBLIC, // 阐明是 public 类型
"com/mycorp/HelloProxy", // 指定包和类的名称
null, // 签名,null 示意不是泛型
"java/lang/Object", // 指定父类
new String[]{ "com/mycorp/Hello"}); // 指定须要实现的接口
复制代码
更进一步,咱们能够依照须要为代理对象实例,生成须要的办法和逻辑。
MethodVisitor mv = cw.visitMethod(
ACC_PUBLIC, // 申明公共办法
"sayHello", // 办法名称
"()Ljava/lang/Object;", // 描述符
null, // 签名,null 示意不是泛型
null); // 可能抛出的异样,如果有,则指定字符串数组
mv.visitCode();
// 省略代码逻辑实现细节
cw.visitEnd(); // 完结类字节码生成
复制代码
下面的代码尽管有些艰涩,但总体还是能多少了解其用意,不同的 visitX 办法提供了创立类型,创立各种办法等逻辑。ASM API,宽泛的应用了 Visitor 模式,如果你相熟这个模式,就会晓得它所针对的场景是将算法和对象构造解耦,非常适合字节码操纵的场合,因为咱们大部分状况都是依赖于特定构造批改或者增加新的办法、变量或者类型等。
依照后面的剖析,字节码操作最初大都应该是生成 byte 数组,ClassWriter 提供了一个简便的办法。
cw.toByteArray();
复制代码
而后,就能够进入咱们熟知的类加载过程了;
最初一个问题,字节码操纵技术,除了动静代理,还能够利用在什么中央?
这个技术仿佛离咱们日常开发边远,但其实曾经深刻到各个方面,兴许很多你当初正在应用的框架、工具就利用该技术,上面是我能想到的几个常见畛域。
各种 Mock 框架
ORM 框架
IOC 容器
局部 Profiler 工具,或者运行时诊断工具等
生成形式化代码的工具
甚至能够认为,字节码操纵技术是工具和根底框架必不可少的局部,大大减少了开发者的累赘。
后记
以上就是【JAVA】不会有人不晓得 Java 类可能在运行时动静生成吧?的所有内容了;
探讨了更加深刻的类加载和字节码操作方面技术。为了了解底层的原理,选取的例子是比拟偏底层的、能力全面的类库,如果理论我的项目中须要进行根底的字节码操作,能够思考应用更加高层次视角的类库。