1. 步骤
假如我们需要动态执行一段字符串形式的java
代码,大概需要这样几步:
- 生成文件(具体的文件/内存中的文件)
- 调用
javac
编译 - 通过反射执行
2. 生成文件
- 确定字符串的形式
- 字符串已经是完整的
.java
形式,那么就不再需要再做操作,写入文件即可 - 字符串只是一个方法,需要听过
UUID
为其包装出一个类名,再写入文件 - 字符串只是一段字符串,就需要即为其加上方法名,又要加上类名,然后再写入文件
- 字符串已经是完整的
2.1 IO写入具体的磁盘文件
- 阻塞字节流输出
public class ToFile {
//字节流方式
public static void main(String[] args) throws IOException {
String code = new String("public class A{}");
FileOutputStream outputStream = new FileOutputStream("E:\\spring\\dynamicJava\\src\\main\\resources\\T1.java");
byte[] codeBytes = code.getBytes(StandardCharsets.UTF_8);
outputStream.write(codeBytes);
outputStream.close();
}
}
- 字符流输出
public class ToFileByWriter {
//字符流方式
public static void main(String[] args) throws IOException {
String code = new String("public class B{}");
FileWriter writer = new FileWriter("E:\\spring\\dynamicJava\\src\\main\\resources\\T2.java");
writer.write(code);
writer.flush();
writer.close();
}
}
RandomAccessFile
的方式
RandomAccessFile
是 Java 输入/输出流体系中功能最丰富的文件内容访问类,它提供了众多的方法来访问文件内容,它既可以读取文件内容,也可以向文件输出数据。由于 RandomAccessFile
可以从任意位置访问文件,所以在只需要访问文件部分内容的情况下,使用 RandonAccessFile 类是一个很好的选择。
RandomAccessFile
对象包含了一个记录指针,用以标识当前读写处的位置,当程序新创建一个 RandomAccessFile 对象时,该对象的文件记录指针位于文件头(也就是 0 处),当读/写了 n 个字节后,文件记录指针将会向后移动 n 个字节。除此之外,RandonAccessFile 可以自由移动该记录指针,既可以向前移动,也可以向后移动。
public class ToFileByRandomAccessFile {
public static void main(String[] args) throws IOException {
String code = new String("public class C{}");
File file = new File("E:\\spring\\dynamicJava\\src\\main\\resources\\T3.java");
RandomAccessFile accessFile = new RandomAccessFile(file,"rw");
accessFile.write(code.getBytes());
accessFile.close();
}
}
NIO
的方式
public class ToFileByNio {
//通过javaNio的方式进行写入
public static void main(String[] args) throws IOException {
String code = new String("public class D{}");
//1. 打开channel
RandomAccessFile accessFile = new RandomAccessFile("E:\\spring\\dynamicJava\\src\\main\\resources\\T4.java", "rw");
FileChannel channel = accessFile.getChannel();
//2. 创建buffer对象并填入内容(buffer默认是写入模式)
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put(code.getBytes(StandardCharsets.UTF_8));
//3. 反转buffer
buffer.flip();
//4. 写入channel
while (buffer.hasRemaining()) {
channel.write(buffer);
}
//5. 关闭channel
channel.close();
}
}
2.2 生成内存的JavaFileObject
对象
JavaFileObject
源码
/**
* File abstraction for tools operating on Java™ programming language
* source and class files.
*
* <p>All methods in this interface might throw a SecurityException if
* a security exception occurs.
*
* <p>Unless explicitly allowed, all methods in this interface might
* throw a NullPointerException if given a {@code null} argument.
*
* @author Peter von der Ahé
* @author Jonathan Gibbons
* @see JavaFileManager
* @since 1.6
*/
public interface JavaFileObject extends FileObject {
enum Kind {
SOURCE(".java"),
CLASS(".class"),
HTML(".html"),
OTHER("");
public final String extension;
private Kind(String extension) {
extension.getClass(); // null check
this.extension = extension;
}
};
Kind getKind();
boolean isNameCompatible(String simpleName, Kind kind);
NestingKind getNestingKind();
Modifier getAccessLevel();
}
可以把一个JavaFileObject
当成是一个不同类型文件在内存中的抽象。一般情况下,我们自己新建一个类完成SimpleJavaFileObject
的创建,当然需要继承SimpleJavaFileObject
。
public class MySimpleJavaFileObject extends SimpleJavaFileObject {
private String contents = null;
private String className;
public MySimpleJavaFileObject(String className, String contents) {
super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
this.className = className;
this.contents = contents;
}
public CharSequence getCharContent(boolean ignoredEncodingErrors) throws IOException {
return contents;
}
public String getClassName() {
return className;
}
}
3. 动态编译
由于编译对象的不同,具体的动态编译的输出形式也不同,可以实现:
- 从源文件到字节码文件的编译方式
- 从源文件到内存的编译方式
- 从内存到内存的编译方式
3.1 JavaCompiler.run()
在javax.tools
包下的tools
类中实现了一个run
方法,如下:
/**
* Common interface for tools that can be invoked from a program.
* A tool is traditionally a command line program such as a compiler.
* The set of tools available with a platform is defined by the
* vendor.
*
* <p>Tools can be located using {@link
* java.util.ServiceLoader#load(Class)}.
*
* @author Neal M Gafter
* @author Peter von der Ahé
* @author Jonathan Gibbons
* @since 1.6
*/
public interface Tool {
/**
* Run the tool with the given I/O channels and arguments. By
* convention a tool returns 0 for success and nonzero for errors.
* Any diagnostics generated will be written to either {@code out}
* or {@code err} in some unspecified format.
*
* @param in "standard" input; use System.in if null
* @param out "standard" output; use System.out if null
* @param err "standard" error; use System.err if null
* @param arguments arguments to pass to the tool
* @return 0 for success; nonzero otherwise
* @throws NullPointerException if the array of arguments contains
* any {@code null} elements.
*/
int run(InputStream in, OutputStream out, OutputStream err, String... arguments);
/**
* Gets the source versions of the Java™ programming language
* supported by this tool.
* @return a set of supported source versions
*/
Set<SourceVersion> getSourceVersions();
}
可以发现,Tool
接口是一个可以从程序中调用的工具的公共接口,其中JavaCompiler(从程序中调用Java编程语言编译器的接口)
、Diagnostic(用于来自工具的诊断的接口)
都是继承的他,其中有一个run()
方法,接收参数由四个,分别为标准输入,标准输出,标准错误输出,最后一个参数为具体接口/实现类的参数。
JavaCompiler.run()
类似于执行javac
,第四个参数argument
就是javac ./test.java
中test.java
。通过这样的方式可以在当前文件的目录下生成一个class文件。
public class CompilerRun {
public static void main(String[] args) {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
int run = compiler.run(null, null, null, "E:\\spring\\dynamicJava\\src\\main\\java\\com\\yuan\\compiler\\User.java");
System.out.println("result = " + run);
}
}
最终执行结果:
0表示编译成功,同时对应目录下出现相应的User.class
。
3.2 JavaCompiler.getTask()
编译硬盘中的代码
使用JavaCompiler.run()
方法非常简单,但它确不能更有效地得到我们所需要的信息。一般来说我们都会使用StandardJavaFileManager
类(jdk 6或以上),这个类可以很好地控制输入、输出,并且可以通过DiagnosticListener
得到诊断信息,而DiagnosticCollector
类就是listener(监听)的实现。
需要注意的是
DiagnosticCollector
在被解析的java文件没问题的情况下是不会收集信息的,我们这里引入一个lombok,相当于java文件中有不存在的包,可以发现以下的诊断信息:
执行前目录结构如下,目标是把编译后的结果都放置在下面的classes包中。
public class Test {
public static void main(String[] args) throws IOException {
Test.compiler();
}
public static void compiler() throws IOException {
//1.获得系统编译器
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
//2. 建立DiagnosticCollector对象
DiagnosticCollector<Object> diagnosticCollector = new DiagnosticCollector<>();
StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnosticCollector, null, null);
//3. 建立源文件对象,每一个文件都被保存在一个JavaFileObject继承的类中
Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjectsFromStrings(
Arrays.asList("E:\\spring\\dynamicJava\\src\\main\\java\\com\\yuan\\getTaskByIO\\Person.java",
"E:\\spring\\dynamicJava\\src\\main\\java\\com\\yuan\\getTaskByIO\\Student.java"));
//4. 确定options命令行选项
List<String> options = Arrays.asList("-d", "E:\\spring\\dynamicJava\\src\\main\\java\\com\\yuan\\getTaskByIO\\classes");
//5. 获取编译任务
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnosticCollector, options, null, compilationUnits);
//6. 编译源程序
Boolean success = task.call();
fileManager.close();
System.out.println(success ? "编译成功" : "编译失败");
//7. 打印信息
for (Diagnostic<?> diagnostic : diagnosticCollector.getDiagnostics()) {
System.out.printf("Code: %s%n" + "Kind: %s%n" + "Position: %s%n" + "Start Position: %s%n"
+ "End Position: %s%n" + "Source: %s%n" + "Message: %s%n", diagnostic.getCode(),
diagnostic.getKind(), diagnostic.getPosition(), diagnostic.getStartPosition(),
diagnostic.getEndPosition(), diagnostic.getSource(), diagnostic.getMessage(null));
}
}
}
结果如下:
可以发现,由于我们的每一个类的.java
文件中是如下规定的:
所以会在classes
目录下建立相应的子包,删除这一行package
试试,结果正常了。
3.3 JavaCompoler.getTask()
编译内存中的代码
JavaCompiler
不仅可以编译硬盘上的Java
文件,而且还可以编译内存中的Java代码,然后使用reflection来运行它们。我们可以编写一个MyJavaSimpleObject
类,通过这个类可以输入Java源代码。
public class MySimpleJavaFileObject extends SimpleJavaFileObject {
private String contents = null;
private String className;
public MySimpleJavaFileObject(String className, String contents) {
super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
this.className = className;
this.contents = contents;
}
public CharSequence getCharContent(boolean ignoredEncodingErrors) throws IOException {
return contents;
}
public String getClassName() {
return className;
}
}
public class Test {
public static void main(String[] args) throws Exception {
Test.compiler2();
}
public static void compiler2() throws IOException, IllegalAccessException, IllegalArgumentException,
InvocationTargetException, NoSuchMethodException, SecurityException, ClassNotFoundException {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
DiagnosticCollector diagnostics = new DiagnosticCollector();
//自己手写java代码
String code = "public class HelloWorld{" +
"public static void main(String[] args){" +
"System.out.println(\"Hello World\");}" +
"}";
StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null);
MySimpleJavaFileObject simpleJavaFileObject = new MySimpleJavaFileObject("HelloWorld", code);
Iterable compilationUnits = Arrays.asList(simpleJavaFileObject);
// options命令行选项
Iterable<String> options = Arrays.asList("-d",
"E:\\spring\\dynamicJava\\src\\main\\resources");// 指定的路径一定要存在,javac不会自己创建文件夹
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnostics, options, null,
compilationUnits);
boolean success = task.call();
System.out.println((success) ? "编译成功" : "编译失败");
for (Object object : diagnostics.getDiagnostics()) {
Diagnostic diagnostic = (Diagnostic) object;
System.out.printf("Code: %s%n" + "Kind: %s%n" + "Position: %s%n" + "Start Position: %s%n"
+ "End Position: %s%n" + "Source: %s%n" + "Message: %s%n", diagnostic.getCode(),
diagnostic.getKind(), diagnostic.getPosition(), diagnostic.getStartPosition(),
diagnostic.getEndPosition(), diagnostic.getSource(), diagnostic.getMessage(null));
}
}
}
最终结果如下:
3.4 总结
其实可以发现动态编译的基本流程如下:
public class CompileFileToFile{
public static void main(String[] args) {
//获取系统Java编译器
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
//获取Java文件管理器
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
//定义要编译的源文件
File file = new File("/path/to/file");
//通过源文件获取到要编译的Java类源码迭代器,包括所有内部类,其中每个类都是一个 JavaFileObject,也被称为一个汇编单元
Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjects(file);
//生成编译任务
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, null, null, compilationUnits);
//执行编译任务
task.call();
}
}
其实由上面的执行流程可以发现,重要的就是task.call()
,可以来跟踪一下task.call()
的执行流程。
可以发现最终是由输出流写入到实际的磁盘class
文件中的。
4. 输出到内存中而不是磁盘
从前面的分析我们看到,JavaFileObject
的 openOutputStream()
方法控制了编译后字节码的输出行为,也就意味着我们可以根据需要定制自己的 Java 文件对象。比如,当编译完源文件之后,我们不想将字节码输出到文件,而是留在内存中以便后续加载,那么我们可以实现自己的输出文件类 JavaFileObject。由于输出文件对象是从文件管理器的 getJavaFileForOutput() 方法获取的,所以我们还应该重写文件管理器的这一行为,综合起来的代码如下:
JavaFileManager jfm = new ForwardingJavaFileManager(fileManager) {
public JavaFileObject getJavaFileForOutput(JavaFileManager.Location location,
String className,
JavaFileObject.Kind kind,
FileObject sibling) throws IOException {
if(kind == JavaFileObject.Kind.CLASS) {
return new SimpleJavaFileObject(URI.create(className + ".class"), JavaFileObject.Kind.CLASS) {
public OutputStream openOutputStream() {
return new FilterOutputStream(new ByteArrayOutputStream()) {
public void close() throws IOException{
out.close();
ByteArrayOutputStream bos = (ByteArrayOutputStream) out;
bytes.put(className, bos.toByteArray());
}
};
}
};
}else{
return super.getJavaFileForOutput(location, className, kind, sibling);
}
}
};
我们以前会默认将class文件存储到磁盘是由于以下两个原因:
FileManger
返回的时默认的JavaFileObject
- 默认的
JavaFileObject
的openOutStream
的方法如下:
@Override @DefinedBy(Api.COMPILER)
public OutputStream openOutputStream() throws IOException {
fileManager.updateLastUsedTime();
fileManager.flushCache(this);
ensureParentDirectoriesExist();
return Files.newOutputStream(path);
}
`ClassWriter`所用的输出流时NIO包的File类,会默认存储到我们设定的路径上。
写入到内存:
所以如果我们需要写入到内存,就需要修改JavaFileObject
的openOutStream()
方法;同时这个JavaFileObject()
是通过文件管理器的getJavaFileForOutput()
方法获得的,默认的文件管理器获得的JavaFileObject
中的输出方法就是输出到磁盘上,所以我们还需要一个实现一个自身的FileManager
。
5. 从内存到内存的动态编译+动态执行
5.1. 自定义的Java源码文件类
public class MySimpleJavaFileObject extends SimpleJavaFileObject {
private String contents = null;
private String className;
public MySimpleJavaFileObject(String className, String contents) {
super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
this.className = className;
this.contents = contents;
}
public CharSequence getCharContent(boolean ignoredEncodingErrors) throws IOException {
return contents;
}
public String getClassName() {
return className;
}
}
5.2. 自定义的Java字节码文件类
这里需要重写openOutStream()
方法,不输出字节码文件到文件,而是直接保存在一个输出流中。
public class MyJavaClassFileObject extends SimpleJavaFileObject {
private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
public MyJavaClassFileObject(String name, JavaFileObject.Kind kind) {
super(URI.create("string:///" + name.replace('.', '/') + kind.extension), kind);
}
public byte[] getBytes() {
return outputStream.toByteArray();
}
//编译时候会调用openOutputStream获取输出流,并写数据
@Override
public OutputStream openOutputStream() throws IOException {
return outputStream;
}
}
5.3 自定义的文件管理器
这里需要重写的方法时getJavaFileOutput()
方法,输出我们自己写的Java字节码文件类。
public class MyFileManager extends ForwardingJavaFileManager {
private MyJavaClassFileObject javaClassObject;
protected MyFileManager(StandardJavaFileManager standardJavaFileManager) {
super(standardJavaFileManager);
}
public MyJavaClassFileObject getJavaClassObject(){
return javaClassObject;
}
@Override
public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind,FileObject sibling) {
this.javaClassObject = new MyJavaClassFileObject(className, kind);
return javaClassObject;
}
}
5.4 自定义的类加载器
这里需要注意,默认的ClassLoader的defineClass()
方法第一个参数接受的是全限定类名,classData是字节数组。
public class MyClassLoader extends ClassLoader {
public Class loadClass(String fullName, MyJavaClassFileObject javaClassObject) {
byte[] classData = javaClassObject.getBytes();
return this.defineClass(fullName, classData, 0, classData.length);
}
}
5.5 自定义的Java编译器
我们这里将编译器抽象出来。
public class DynamicCompiler {
/**
* 编译出类
*
* @param fullClassName 全路径的类名
* @param javaCode java代码
* @return 目标类
*/
public Class<?> compileToClass(String fullClassName, String javaCode) throws Exception {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
MyFileManager fileManager = new MyFileManager(compiler.getStandardFileManager(diagnostics, null, null));
List<JavaFileObject> jfiles = new ArrayList<>();
jfiles.add(new MySimpleJavaFileObject(fullClassName, javaCode));
List<String> options = new ArrayList<>();
options.add("-encoding");
options.add("UTF-8");
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnostics, options, null, jfiles);
boolean success = task.call();
if (success) {
MyJavaClassFileObject javaClassObject = fileManager.getJavaClassObject();
MyClassLoader dynamicClassLoader = new MyClassLoader();
//加载至内存
return dynamicClassLoader.loadClass(fullClassName, javaClassObject);
} else {
for (Diagnostic diagnostic : diagnostics.getDiagnostics()) {
String error = compileError(diagnostic);
throw new RuntimeException(error);
}
throw new RuntimeException("compile error");
}
}
private String compileError(Diagnostic diagnostic) {
StringBuilder res = new StringBuilder();
res.append("LineNumber:[").append(diagnostic.getLineNumber()).append("]\n");
res.append("ColumnNumber:[").append(diagnostic.getColumnNumber()).append("]\n");
res.append("Message:[").append(diagnostic.getMessage(null)).append("]\n");
return res.toString();
}
}
步骤依旧和动态编译的时候一致,只不过在编译成功的时候,通过文件管理器获得我们自己写的class源码文件类,然后通过自定义的classLoader读取字节数组,实现创建一个类。
5.6 Main
public class Main {
public static void main(String[] args) throws Exception {
DynamicCompiler dynamicCompiler = new DynamicCompiler();
String code = "package com.yuan.dynamic;\n" +
"\n" +
"/**\n" +
" * Created by yuantb on 21/12/14.\n" +
" */\n" +
"public class Test {\n" +
" @Override\n" +
" public String toString() {\n" +
" return \"yuantb\"\n" + ";" +
" }\n" +
"}\n";
Class<?> clazz = dynamicCompiler.compileToClass("com.yuan.dynamic.Test", code);
System.out.println(clazz.newInstance());
}
}
结果: