java动态编译实践
从 JDK 1.6 开始,引入了 Java 代码重写的编译接口,使得我们可以在运行时编译 Java 代码,然后在通过类加载器将编译好的类加载进 JVM,这种在运行时编译代码的操作就叫做动态编译。
背景
- 看到一个在线执行java代码的工程,阅读代码后,记录实践的过程。
- 看编译原理,在分析java的“自举”编译器环节,涉及需要查看java代码编译中间过程(词法解析、语法分析等),需要打断点调试编译过程。
- 一些场景,需要用java代码生成字节码流,进行后续的处理
基于上面三点,文章主要内容
java自己的api编译java文件的相关过程分析
编译指定类
传统命令行的方式
1 |
|
获取编译器对像,调用run方式,指定参数。会编译Hello.java文件。与在工程目录下,用命令行中执行javac ./src/main/java/Hello.java
效果一样。
会生成Hello.class
的字节码文件。
1 |
|
in
,out
,err
对应输入、输出、错误流。arguments
是传的参数。
也可以verbose与-d指令,使之输出详细信息。
1 |
|
动态编译
1 |
|
我们发现执行编译的那个函数有一大堆入参需要提前准备,所以我们需要先来看一下这些入参都是什么,以及该怎么准备,getTask() 方法的声明如下:
1 |
|
这个方法一共有 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),重写
getJavaFileForOutput
与getJavaFileForInput
方法。 - 创建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/