java动态编译实践

从 JDK 1.6 开始,引入了 Java 代码重写的编译接口,使得我们可以在运行时编译 Java 代码,然后在通过类加载器将编译好的类加载进 JVM,这种在运行时编译代码的操作就叫做动态编译。

背景

  • 看到一个在线执行java代码的工程,阅读代码后,记录实践的过程。
  • 看编译原理,在分析java的“自举”编译器环节,涉及需要查看java代码编译中间过程(词法解析、语法分析等),需要打断点调试编译过程。
  • 一些场景,需要用java代码生成字节码流,进行后续的处理

基于上面三点,文章主要内容

java自己的api编译java文件的相关过程分析

编译指定类

传统命令行的方式

1
2
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
compiler.run(null, null, null, "./src/main/java/Hello.java");

获取编译器对像,调用run方式,指定参数。会编译Hello.java文件。与在工程目录下,用命令行中执行javac ./src/main/java/Hello.java效果一样。

会生成Hello.class的字节码文件。

1
int javax.tools.Tool.run(InputStream in, OutputStream out, OutputStream err, String... arguments)

in,out,err对应输入、输出、错误流。
arguments 是传的参数。

也可以verbose与-d指令,使之输出详细信息。

1
compiler.run(null,null, null, "-verbose","-d",".","./src/main/java/Hello.java");

动态编译

1
2
3
4
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); // 获取编译器对象
/* 准备执行编译需要的各种入参 */
Boolean result = compiler.getTask(null, manager, collector, options,
null, Arrays.asList(javaFileObject)).call(); // 执行编译

我们发现执行编译的那个函数有一大堆入参需要提前准备,所以我们需要先来看一下这些入参都是什么,以及该怎么准备,getTask() 方法的声明如下:

1
2
3
4
5
6
JavaCompiler.CompilationTask getTask(Writer out,
JavaFileManager fileManager,
DiagnosticListener<? super JavaFileObject> diagnosticListener,
Iterable<String> options,
Iterable<String> classes,
Iterable<? extends JavaFileObject> compilationUnits)

这个方法一共有 6 个入参,它们分别是:

  • out:编译器的一个额外的输出 Writer,为 null 的话就是 System.err;
  • fileManager:文件管理器;
  • diagnosticListener:诊断信息收集器;
  • options:编译器的配置;
  • classes:需要被 annotation processing 处理的类的类名;
  • compilationUnits:要被编译的单元们,就是一堆 JavaFileObject。

在这 基于 SpringBoot 的在线 Java IDE 样的一个工程中。
作者分析了执行的流程。
继承javaFileManager类,重写几个方法,创建JavaFileOjbect的子类,使在编译的流程中获取自己所需要的字节码流。

执行流程说明:

  • 编译器用compilationUnits中获取编译的单元。通过集合对像javaFileObject中JavaFileObject.getCharContent的方式获取到源代码。
  • 编译器会对得到的源码进行编译,得到字节码,并且会将得到的字节码封装进一个JavaFileObject对象。(这里看到源代码与字节码文件都是抽象成JavaFileObject)。
  • 上面参数的JavaFileManager 就是用于管理JavaFileObject对象,当编译器生成字节码后,会调用JavaFileManager.getJavaFileForOutput 来获取存放字节码的对象。(JavaFileObject.openOutputStream()是外部提供写入的接口)

** 实际上做的事:**

  • 创建JavaFileManager的子类(继承于ForwardingJavaFileManager),重写getJavaFileForOutputgetJavaFileForInput方法。
  • 创建JavaFileObject的子类(继承于SimpleJavaFileObject),重写构造方法与openOutputStream(用于指定自己存方字节码文件的位置)
  • 注意JavaFileObject的构造方法中需要一个URI参数,按重写的格式String:///<类名>.<扩展文件名>

最后在JavaFileManager中可以拿到编译好的字节码文件。(项目中是用一个静态的hashmap做的中转的)

一些小坑

  • jd-gui-windows-1.5.0(06cba56abdbf98be6e0f3cad48c00550) 对内部类的识别有限
  • JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); 在jre环境下,会因找不到tools.jar而返回空(解决方式安装jdk,指定lib的查找路径)

参考


java动态编译实践
https://blog.fengcl.com/2021/02/27/java-file-dynamic-compile/
作者
frank
发布于
2021年2月27日
许可协议