动态编译 动态编译,就是在程序运行时产生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();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(); 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 public class JavaSourceFromString extends SimpleJavaFileObject {final String code;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 { 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; protected ClassFileManager (JavaFileManager fileManager) { super (fileManager); } @Override public JavaFileObject getJavaFileForOutput (Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException { classFileObject = new JavaClassFileObject (className, kind); return classFileObject; } @Override 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\");" + "}}" ; SimpleJavaFileObject fileObject = new JavaSourceFromString ("MyPrinter2" , str);JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();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 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)