【Java动态编译】动态编译的应用
1、动态编译
动态编译,简单来说就是在Java程序运行时编译源代码。
从JDK1.6开始,引入了Java代码重写过的编译器接口,使得我们可以在运行时编译Java源代码,然后再通过类加载器将编译好的类加载进JVM,这种在运行时编译代码的操作就叫做动态编译。
静态编译:编译时就把所有用到的Java代码全都编译成字节码,是一次性编译。
动态编译:在Java程序运行时才把需要的Java代码的编译成字节码,是按需编译。
静态编译示例:
静态编译实际上就是在程序运行前将所有代码进行编译,我们在运行程序前用Javac命令或点击IDE的编译按钮进行编译都属于静态编译。
比如,我们编写了一个xxx.java
文件,里面是一个功能类,如果我们的程序想要使用这个类,就必须在程序启动前,先调用Javac编译器来生成字节码文件。
如果使用动态编译,则可以在程序运行过程中再对xxx.java
文件进行编译,之后再通过类加载器对编译好的类进行加载,同样能正常使用这个功能类。
动态编译示例:
JDK提供了对应的JavaComplier接口来实现动态编译(rt.jar中的javax.tools包提供的编译器接口,使用的是JDK自带的Javac编译器)。
一个用来进行动态编译的类:
public class TestHello { public void sayHello(){ System.out.println("hello word"); } }
编写一个程序来对它进行动态编译:
public class TestDynamicCompilation { public static void main(String[] args) { //获取Javac编译器对象 JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); //获取文件管理器:负责管理类文件的输入输出 StandardJavaFileManager fileManager = compiler.getStandardFileManager(null,null,null); //获取要被编译的Java源文件 File file = new File("/project/test/TestHello.java"); //通过源文件获取到要编译的Java类源码迭代器,包括所有内部类,其中每个类都是一个JavaFileObject Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjects(file); //生成编译任务 JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, null, null, compilationUnits); //执行编译任务 task.call(); } }
启动main函数,会发现在程序运行过程中,使用了Javac编译器对类TestHello进行了编译,并生成了字节码文件TestHello.class。
以上就是动态编译的简单使用。如果我们想使用这个类TestHello,也可以在程序运行中通过类加载器对这个已经编译的类进行加载。
使用JavaComplier接口来实现动态编译时JDK1.6才引入的,在此之前,也可以通过如下方式实现动态编译:
Runtime run = Runtime.getRuntime(); Process process = run.exec("javac -cp e:/project/test/TestHello.java");
该方法的本质是启动一个新的进程来使用Javac进行编译。
2、动态编译的应用
(1)、从源码文件编译得到字节码文件
刚才我们使用动态编译完成了输入一个Java源文件(.java),再到输出字节码文件(.class)的操作。这是从源码文件编译得到字节码文件的方式,实质上也是从磁盘输入,再输出到磁盘的方式。
(2)、从源码字符串编译得到字节码文件
假如现在有一串字符串形式的Java代码,那如何使用动态编译将这些字符串代码编译成字节码文件?这是从源码字符串编译得到字节码文件的方式,实质上也是从内存中得到源码,再输出到磁盘的方式。
根据刚才的代码,我们知道编译任务getTask()
这个方法一共有 6 个参数,它们分别是:
Writer out
:编译器的一个额外的输出 Writer,为 null 的话就是 System.err;JavaFileManager fileManager
:文件管理器;DiagnosticListener<? super JavaFileObject> diagnosticListener
:诊断信息收集器;Iterable<String> options
:编译器的配置;Iterable<String> classes
:需要被 annotation processing 处理的类的类名;Iterable<? extends JavaFileObject> compilationUnits
:要被编译的单元们,就是一堆 JavaFileObject。
根据getTask()
的参数,我们知道编译器执行编译所需要的对象类型并不是文件File
对象,而是JavaFileObject
对象。因此,要实现从字符串源码编译得到字节码文件,只需要把字符串源码变为JavaFileObject
对象即可。
但JavaFileObject
是一个接口,它的标准实现类SimpleJavaFileObject
提供的一些方法是面向类源码文件(.java)和字节码文件(.class)的,而我们进行动态编译时输入的是字符串源码,所以我们需要自行实现JavaFileObject
,以使JavaFileObject
对象能装入我们的字符串源码。
具体的实现方法就是可以直接继承SimpleJavaFileObject
类,再重写其中的一些方法使它能够装入字符串即可。
可以通过查看compiler.getTask().call()
的源代码来查看具体用到了SimpleJavaFileObject
的那些方法,这样我们才知道需要重写 SimpleJavaFileObject
的哪些方法。
一篇大佬分析getTask().call()
源代码执行流程的文章介绍得很十分详细,强烈推荐:Java 类运行时动态编译技术.https://seanwangjs.github.io/2018/03/13/java-runtime-compile.html。
简单的流程如下:
在上图中,getTask().call()
会通过调用作为参数传入的JavaFileObject
对象的getCharContent()
方法获得字符串序列,即源码的读取是通过 JavaFileObject
的 getCharContent()
方法,那我们只需要重写getCharContent()
方法,即可将我们的字符串源码装进JavaFileObject
了。
构造SourceJavaFileObject
实现定制的JavaFileObject
对象,用于存储字符串源码:
public class SourceJavaFileObject extends SimpleJavaFileObject { private String source; //源码字符串 //返回源码字符串 public SourceJavaFileObject(String name, String sourceStr){ super(URI.create("String:///" + name + Kind.SOURCE.extension),Kind.SOURCE); this.source = sourceStr; } @Override public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException{ if(source == null) throw new IllegalArgumentException("source == null"); else return source; } }
则创建JavaFileObject
对象时,变为了:
//使用重写getCharContent方法后的JavaFileObject构造参数 JavaFileObject sourceFileObject = new SourceJavaFileObject(className, source); //执行编译 Boolean result = compiler.getTask(null, fileManager, null, null, null, Arrays.asList(sourceFileObject)).call();
由于我们自定了JavaFileObject
,文件管理器 fileManager
更像是一个工具类用于把 File
对象数组自动转换成JavaFileObject 列表
,换成手动生成 compilationUnits
列表并传入也是可行的。(上述代码就是使用了Arrays.asList()
手动生成 compilationUnits
列表)。
至此,只需要调用getTask().call()
就能将字符串形式的源码编译成字节码文件了。
(3)、从源码字符串编译得到字节码数组
如果我们进行动态编译时,想要直接输入源码字符串并且输出的是字节码数组,而不是输出字节码文件,又该如何实现?实际上,这是从内存中得到源码,再输出到内存的方式。
在getTask().call()
源代码执行流程图中,我们可以发现JavaFileObject
的 openOutputStream()
方法控制了编译后字节码的输出行为,编译完成后会调用openOutputStream
获取输出流,并写数据(字节码)。所以我们需要重写JavaFileObject
的 openOutputStream()
方法。
同时在执行流程图中,我们还发现用于输出的JavaFileObject
对象是JavaFileManager
的getJavaFileForOutput()
方法提供的,所以为了让编译器编译完成后,将编译得到的字节码输出到我们自己构造的JavaFileObject
对象,我们还需要重写JavaFileManager
。
构造ClassFileObject
,实现定制的JavaFileObject
对象,用于存储编译后得到的字节码:
public static class ClassFileObject extends SimpleJavaFileObject { private ByteArrayOutputStream byteArrayOutputStream; //字节数组输出流 //编译完成后会回调OutputStream,回调成功后,我们就可以通过下面的getByteCode()方法获取编译后的字节码字节数组 @Override public OutputStream openOutputStream() throws IOException { return byteArrayOutputStream; } //将输出流中的字节码转换为字节数组 public byte[] getCompiledBytes() { return byteArrayOutputStream.toByteArray(); } }
这样,我们就拥有了自定义的用于存储字节码的JavaFileObject
。同时还通过添加getByteCode()
方法来获得JavaFileObject
对象中用于存放字节码的输出流,并将其转换为字节数组。
接下来,就需要重写JavaFileManager
,使编译器编译完成后,将字节码存放在我们的ClassFileObject
。具体做法是直接继承ForwardingJavaFileManager
,再重写需要的getJavaFileForOutput()
方法即可。
public static class MyJavaObjectManager extends ForwardingJavaFileManager<JavaFileManager>{ private ClassFileObject classObject; //我们自定义的JavaFileObject //重写该方法,使其返回我们的ClassJavaFileObject @Override public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException { classObject= new ClassJavaFileObject (className, kind); return classObject; } }
构造完毕,接下来直接传入getTask执行即可:
//执行编译 Boolean result = compiler.getTask(null,new MyJavaObjectManager(), null, null, null, Arrays.asList(sourceFileObject)).call();
注意这里传入的JavaFileObject
,是前面构造的存储字符串源码的sourceFileObject
,而不是我们用来存储字节码的sourceFileObject
。
至此,我们使用动态编译完成了将字符串源码编译成字节码数组。随后我们可以使用类加载器加载 byte[]中的字节码即可。
3、总结
动态编译是在Java程序运行时编译源代码,动态编译配合类加载器就可以在程序运行时编译源代码,并动态加载。
JDK提供了对应的JavaComplier接口来实现动态编译。
动态编译中存放源码和字节码的对象都是JavaFileObject
,因此如果我们想要修改源码的输入方式或者字节码的输出方式的,可以自主实现JavaFileObject
接口。同时,由于编译器是通过JavaFileManager
来管理输入输出的,因此也需要自主实现JavaFileManager
接口。
由于能力有限,可能存在错误,感谢指出。以上内容为本人在学习过程中所做的笔记。参考的书籍、文章或博客如下:
[1]seanwangjs. Java 类运行时动态编译技术.https://seanwangjs.github.io/2018/03/13/java-runtime-compile.html
[2]Throwable.深入理解Java的动态编译.博客园.https://www.cnblogs.com/throwable/p/13053582.html
[3]执笔记忆的空白.java动态编译实现.腾讯云云社区.https://cloud.tencent.com/developer/article/1764721?from=information.detail