【Java动态编译】动态编译的应⽤
1、动态编译
动态编译,简单来说就是在Java程序运⾏时编译源代码。
java源码阅读工具
从JDK1.6开始,引⼊了Java代码重写过的编译器接⼝,使得我们可以在运⾏时编译Java源代码,然后再通过类加载器将编译好的类加载进JVM,这种在运⾏时编译代码的操作就叫做动态编译。
静态编译:编译时就把所有⽤到的Java代码全都编译成字节码,是⼀次性编译。
动态编译:在Java程序运⾏时才把需要的Java代码的编译成字节码,是按需编译。
静态编译⽰例:
静态编译实际上就是在程序运⾏前将所有代码进⾏编译,我们在运⾏程序前⽤Javac命令或点击IDE的编译按钮进⾏编译都属于静态编译。
⽐如,我们编写了⼀个xxx.java⽂件,⾥⾯是⼀个功能类,如果我们的程序想要使⽤这个类,就必须在程序启动前,先调⽤Javac编译器来⽣成字节码⽂件。
如果使⽤动态编译,则可以在程序运⾏过程中再对xxx.java⽂件进⾏编译,之后再通过类加载器对编译好的类进⾏加载,同样能正常使⽤这个功能类。
动态编译⽰例:
JDK提供了对应的JavaComplier接⼝来实现动态编译(rt.jar中的ls包提供的编译器接⼝,使⽤的是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 = SystemJavaCompiler();
//获取⽂件管理器:负责管理类⽂件的输⼊输出
StandardJavaFileManager fileManager = StandardFileManager(null,null,null);
//获取要被编译的Java源⽂件
File file = new File("/project/test/TestHello.java");
//通过源⽂件获取到要编译的Java类源码迭代器,包括所有内部类,其中每个类都是⼀个JavaFileObject
Iterable<? extends JavaFileObject> compilationUnits = JavaFileObjects(file);
//⽣成编译任务
JavaCompiler.CompilationTask task = Task(null, fileManager, null, null, null, compilationUnits);
//执⾏编译任务
task.call();
}
}
启动main函数,会发现在程序运⾏过程中,使⽤了Javac编译器对类TestHello进⾏了编译,并⽣成了字节码⽂件TestHello.class。
以上就是动态编译的简单使⽤。如果我们想使⽤这个类TestHello,也可以在程序运⾏中通过类加载器对这个已经编译的类进⾏加载。
使⽤JavaComplier接⼝来实现动态编译时JDK1.6才引⼊的,在此之前,也可以通过如下⽅式实现动态编译:
Runtime run = Runtime();
Process process = ("javac -cp e:/project/test/TestHello.java");
该⽅法的本质是启动⼀个新的进程来使⽤Javac进⾏编译。
2、动态编译的应⽤
(1)、从源码⽂件编译得到字节码⽂件
刚才我们使⽤动态编译完成了输⼊⼀个Java源⽂件(.java),再到输出字节码⽂件(.class)的操作。这是从源码⽂件编译得到字节码⽂件的⽅式,实质上也是从磁盘输⼊,再输出到磁盘的⽅式。
(2)、从源码字符串编译得到字节码⽂件
假如现在有⼀串字符串形式的Java代码,那如何使⽤动态编译将这些字符串代码编译成字节码⽂件?这是从源码字符串编译得到字节码⽂件的⽅式,实质上也是从内存中得到源码,再输出到磁盘的⽅式。
根据刚才的代码,我们知道编译任务getTask()这个⽅法⼀共有 6 个参数,它们分别是:
Writer out:编译器的⼀个额外的输出 Writer,为 null 的话就是 ;
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类,再重写其中的⼀些⽅法使它能够装⼊字符串即可。
可以通过查看Task().call()的源代码来查看具体⽤到了SimpleJavaFileObject的那些⽅法,这样我们才知道需要重写SimpleJavaFileObject 的哪些⽅法。
简单的流程如下:
在上图中,getTask().call()会通过调⽤作为参数传⼊的JavaFileObject对象的getCharContent() ⽅法获得字符串序列,即源码的读取是通过JavaFileObject 的getCharContent() ⽅法,那我们只需要重写getCharContent() ⽅法,即可将我们的字符串源码装进JavaFileObject了。
构造SourceJavaFileObject 实现定制的JavaFileObject对象,⽤于存储字符串源码:
public class SourceJavaFileObject extends SimpleJavaFileObject {
private String source; //源码字符串
//返回源码字符串
public SourceJavaFileObject(String name, String sourceStr){
ate("String:///" + name + sion),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 = Task(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() {
ByteArray();
}
}
这样,我们就拥有了⾃定义的⽤于存储字节码的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 = Task(null,new MyJavaObjectManager(), null, null, null, Arrays.asList(sourceFileObject)).call();
注意这⾥传⼊的JavaFileObject,是前⾯构造的存储字符串源码的sourceFileObject,⽽不是我们⽤来存储字节码的sourceFileObject。
⾄此,我们使⽤动态编译完成了将字符串源码编译成字节码数组。随后我们可以使⽤类加载器加载 byte[]
中的字节码即可。
3、总结
动态编译是在Java程序运⾏时编译源代码,动态编译配合类加载器就可以在程序运⾏时编译源代码,并动态加载。
JDK提供了对应的JavaComplier接⼝来实现动态编译。
动态编译中存放源码和字节码的对象都是JavaFileObject,因此如果我们想要修改源码的输⼊⽅式或者字节码的输出⽅式的,可以⾃主实
现JavaFileObject接⼝。同时,由于编译器是通过JavaFileManager 来管理输⼊输出的,因此也需要⾃主实现JavaFileManager 接⼝。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。