0%

java-动态编译

动态编译

动态编译,就是在程序运行时产生java类,并编译成class文件。

动态编译的实现方式

正常情况下先编译(明文源码到字节码),后执行(JVM加载字节码,获得类模板,实例化,方法使用)。

程序流程图

相关类介绍

JavaCompiler: 负责读取源代码,编译诊断,输出class
JavaFileObject: 文件抽象,代表源代码或者编译后的class
JavaFileManager: 管理JavaFileObject,负责JavaFileObject的创建和保存位置
ClassLoader: 根据字节码,生成类模板

使用方式

由于代码在编译的时候,类定义甚至类名称还不存在,所以没法直接声明使用的。只能定义一个接口代替之,具体实现留给后面的动态编译。

1
2
3
public interface Printer {
public void print();
}

使用 JavaCompiler 接口来编译 java 源程序(最简单的)

通过 ToolProvider 类的静态方法 getSystemJavaCompiler 来得到一个 JavaCompiler 接 口的实例。

1
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

javaCompiler 中最核心的方法是 run。通过这个方法能编译 java 源程序。这个方法有 3 个固 定参数和 1 个可变参数。前 3 个参数分别用来为 java 编译器提供参数、得到 Java 编译器的输出信息及接收编译器的 错误信息,后面的可变参数能传入一个或多个 Java 源程式文件。如果 run 编译成功,返回 0。

1
2
3
int result = compiler.run(null,null,null,sourceFile);
System.out.println(result==0?"编译成功""编译失败");
return result;
  • 第一个参数:为java编译器提供参数

  • 第二个参数:得到java编译器的输出信息

  • 第三个参数:接收编译器的错误信息

  • 第四个参数:可变参数(String数组)能传入一个或者多个java源文件

  • 返回值:0表示编译成功,非0表示编译失败

  • 如果前 3 个参数传入的是 null,那么 run 方法将以标准的输入、输出代替,即 System.in、 System.out 和 System.err。如果我们要编译一个 hello.java 文件,并将使用标准输入输出,run 的使用方法如下:

    1
    int results = tool.run(null, null, null, "Hello.java");

使用 StandardJavaFileManager 编译 Java 源程序

StandardJavaFileManager 类能非常好地控制输入、输出,并且能通过 DiagnosticListener 得到诊断信息,而 DiagnosticCollector 类就是 listener 的实现。

使用 StandardJavaFileManager 需要两步。
首先建立一个 DiagnosticCollector 实例及通过 JavaCompiler 的 getStandardFileManager()方法得到一个 StandardFileManager 对象。
最后通过 CompilationTask 中的 call 方法编译源程序

源代码的文件级动态编译

java源码以文件的形式存在本地,程序去指定路径加载源文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
String classPath = File2Class.class.getResource("/").getPath();
//在这里我们是动态生成定义,然后写入文件。也可以直接读一个已经存在的文件
String str = "import classloader.Printer;"
+ "public class MyPrinter1 implements Printer {"
+ "public void print() {"
+ "System.out.println(\"test1\");"
+ "}}";
FileWriter writer = new FileWriter(classPath + "MyPrinter1.java");
writer.write(str);;
writer.close();
//获得系统编译器
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
//获得java文件管理器
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null,null, null);
//读入源文件
Iterable fileObject = fileManager.getJavaFileObjects(classPath + "MyPrinter1.java");
//生成编译任务
JavaCompiler.CompilationTask task = compiler.getTask(
null, fileManager, null, null, null, fileObject);
//执行编译任务
task.call();
fileManager.close();
//指定class路径,默认和源代码路径一致,加载class
URLClassLoader classLoader = new URLClassLoader(new URL[]{new URL("file:" + classPath)});
Printer printer = (Printer)classLoader.loadClass("MyPrinter1").newInstance();
printer.print();

源代码的内存级动态编译

上节源代码落地了,这节让我们看下源代码和class全程在内存不落地,如何实现动态编译。思路是生成源代码对应的JavaFileObject时,从内存string读取;生成class对应的JavaFileObject时,以字节数组的形式存到内存。JavaFileObject是一个interface, SimpleJavaFileObject是JavaFileObject的一个基本实现,当自定义JavaFileObject时,继承SimpleJavaFileObject,然后改写部分函数。
自定义JavaSourceFromString,作为源代码的抽象文件(来自JDK API文档)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* A file object used to represent source coming from a string.
*/
public class JavaSourceFromString extends SimpleJavaFileObject {
/**
* The source code of this "file".
*/
final String code;
/**
* Constructs a new JavaSourceFromString.
* @param name the name of the compilation unit represented by this file object
* @param code the source code for the compilation unit represented by this file object
*/
JavaSourceFromString(String name, String code) {
super(URI.create("string:///" + name.replace('.','/') + Kind.SOURCE.extension), Kind.SOURCE);
this.code = code;
}

@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return code;
}
}

JavaClassFileObject,代表class的文件抽象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class JavaClassFileObject extends SimpleJavaFileObject {
//用于存储class字节
ByteArrayOutputStream outputStream;

public JavaClassFileObject(String className, Kind kind) {
super(URI.create("string:///" + className.replace('.', '/') + kind.extension), kind);
outputStream = new ByteArrayOutputStream();
}

@Override
public OutputStream openOutputStream() throws IOException {
return outputStream;
}

public byte[] getClassBytes() {
return outputStream.toByteArray();
}
}

ClassFileManager,修改JavaFileManager生成class的JavaFileObject的行为,另外返回一个自定义ClassLoader用于返回内存中的字节码对应的类模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class ClassFileManager extends ForwardingJavaFileManager {

private JavaClassFileObject classFileObject;
/**
* Creates a new instance of ForwardingJavaFileManager.
*
* @param fileManager delegate to this file manager
*/
protected ClassFileManager(JavaFileManager fileManager) {
super(fileManager);
}

/**
* Gets a JavaFileObject file object for output
* representing the specified class of the specified kind in the given location.
*/
@Override
public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind,
FileObject sibling) throws IOException {
classFileObject = new JavaClassFileObject(className, kind);
return classFileObject;
}

@Override
//获得一个定制ClassLoader,返回我们保存在内存的类
public ClassLoader getClassLoader(Location location) {
return new ClassLoader() {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classBytes = classFileObject.getClassBytes();
return super.defineClass(name, classBytes, 0, classBytes.length);
}
};
}
}

用自定义的JavaFileObject/JavaFileManager来动态编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
String str = "import Printer;" 
+ "public class MyPrinter2 implements Printer {"
+ "public void print() {"
+ "System.out.println(\"test2\");"
+ "}}";
//生成源代码的JavaFileObject
SimpleJavaFileObject fileObject = new JavaSourceFromString("MyPrinter2", str);
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
//被修改后的JavaFileManager
JavaFileManager fileManager = new ClassFileManager(compiler.getStandardFileManager(null, null, null));
//执行编译
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, null, null, Arrays.asList(fileObject));
task.call();
//获得ClassLoader,加载class文件
ClassLoader classLoader = fileManager.getClassLoader(null);
Class printerClass = classLoader.loadClass("MyPrinter2");
//获得实例
Printer printer = (Printer) printerClass.newInstance();
printer.print();

慎用动态编译

  • 在框架中谨慎使用
  • 不要在要求高性能的项目使用
    动态编译毕竟需要一个编译过程,与静态编译相比多了一个执行环节,因此在高性能项目中不要使用动态编译。
  • 动态编译要考虑安全问题
    它是非常典型的注入漏洞,只要上传一个恶意Java程序就可以让你所有的安全工作毁于一旦。
  • 记录动态编译过程
    建议记录源文件、目标文件、编译过程、执行过程等日志,不仅仅是为了诊断,还是为了安全和审计,对Java项目来说,空中编译和运行是很不让人放心的,留下这些依据可以更好地优化程序

参考文章

Java 类运行时动态编译技术 - Fenrier Lab (seanwangjs.github.io)

深入理解Java的动态编译 - 掘金 (juejin.cn)