Java动态类编译简析
借助动态编译机制可以在程序运行时编译java源代码文件并将其编译为class文件加入运行逻辑
一般的动态编译
基本的动态编译无非是在程序工作目录下新建.java源代码文件,写入定义的类,再交给编译器进行编译,最后拿到编译的.classz字节码文件进行类加载、实例化,这里演示在文件落地的情况下进行动态编译
前期工作,我们需要获取程序的工作目录并在相同目录写入.java文件
String classPath = CompileTest.class.getResource("/").getPath();
classPath = classPath.substring(1); FileWriter writer = new FileWriter(classPath+"TransformerTest.java"); String sourceCode = """ import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ConstantTransformer; public class TransformerTest implements CompileTest { public void test() { Transformer[] transformers = new Transformer[]{new ConstantTransformer(null)}; ConstantTransformer constantTransformer = new ConstantTransformer(transformers); System.out.println(constantTransformer.transform("hello").toString()); } }"""; writer.write(sourceCode); writer.close();
|
接下来,获取编译器,编译器可以通过ToolProvider类中的静态方法getSystemJavaCompiler()获取,此外还需要从编译器中获取StandardJavaFileManager实例,该实例用于将物理文件系统转化为 Java 编译器可以理解的对象
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); StandardJavaFileManager manager = compiler.getStandardFileManager(null, null, null);
|
接下来,构建编译任务,调用任务类的call()方法进行编译
JavaCompiler.CompilationTask task = compiler.getTask(null, manager, null, null, null, files); task.call();
|
接下来,从编译得到的class字节码文件获取类加载器,这里使用URLClassLoader类,之后就可以使用类加载器实例化类,并调用方法
URLClassLoader classLoader = new URLClassLoader(new URL[]{new URL("file:///"+classPath)}); CompileTest transformerTest = (CompileTest) classLoader.loadClass("TransformerTest").newInstance(); transformerTest.test();
|
成功编译并调用了方法

无文件落地的动态编译
是否有可能让一切过程都在内存中发生而不产生具体文件呢?答案是肯定的,在调用call()方法执行编译任务时也是需要用到源代码和字节码的,接下来可以尝试去改写编译器获取以上两个文件的逻辑
前面提到StandardJavaFileManager实例用于将物理文件系统转化为 Java 编译器可以理解的对象,我们对该实例调用了getJavaFileObjects()方法获得了一个迭代器,该迭代器内部有一个SimpleFileObject实例,内部主要是指向java源代码文件的url路径

其实SimpleFileObject这个类给我们预留了一个直接获取源代码的方法getCharContent(),不过该方法被设置为抛出异常。当动态编译开始时,程序会优先尝试该方法,若捕获到异常才会去进行url获取
@Override public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { throw new UnsupportedOperationException(); }
|
该方法如何重写?其实只需要返回字符串即可,从SimpleFileObject下的一个默认子类SourceMemoryJavaFileObject重写的getCharContent()也可以看出
class SourceMemoryJavaFileObject extends MemoryJavaFileObject { private final String src; private final Object origin;
SourceMemoryJavaFileObject(Object origin, String className, String code) { super(className, JavaFileObject.Kind.SOURCE); this.origin = origin; this.src = code; }
public Object getOrigin() { return origin; }
@Override public CharSequence getCharContent(boolean ignoreEncodingErrors) { return src; } }
|
那我们直接写一个类继承自SimpleJavaFileObject使其getCharContent()返回源代码即可
public static class SourceJavaFileObject extends SimpleJavaFileObject { String sourceCode;
public SourceJavaFileObject(String name, String sourceCode) throws URISyntaxException { super(URI.create("string:///" + name.replace('.','/') + Kind.SOURCE.extension), Kind.SOURCE); this.sourceCode = sourceCode; }
@Override public CharSequence getCharContent(boolean ignoreEncoding) { return sourceCode; } }
|
在编译任务完成后,call()方法会期望一个字节输出流用于接收编译完成的字节数组,在前面的例子中,该字节输出流指向我们一开始指定的文件地址,而现在需要直接获取输出流,我们可以重写openOutputStream()方法
public static class ClassJavaFileObject extends SimpleJavaFileObject { static ByteArrayOutputStream outputStream;
public ClassJavaFileObject(String className, Kind kind) throws URISyntaxException { super(URI.create("string:///" + className.replace('.', '/') + kind.extension), kind); outputStream = new ByteArrayOutputStream(); }
@Override public OutputStream openOutputStream() { return outputStream; }
public static byte[] getClassBytes() { return outputStream.toByteArray(); } }
|
此外,还需要重写StandardJavaFileManager,该类不仅帮助将url资源路径转换为抽象对象,还决定了编译后字节码导出的操作,若不进行重写则该对象仍会尝试将字节码文件写入文件。我们主要重写getJavaFileForOutput()方法用于发送前面构造的ClassJavaFileObject和getClassLoader()方法用于返回一个输出字节数组决定的类加载器
两方法在ForwardingJavaFileManager类内定义,我们继承它即可,因为该类实现了JavaFileManager接口,getTask()方法的参数列表原本就是接收该接口的实现类
public static class JavaFileManager extends ForwardingJavaFileManager {
public JavaFileManager(StandardJavaFileManager standardFileManager) { super(standardFileManager); }
@Override public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException { try { return new ClassJavaFileObject(className, kind); } catch (URISyntaxException e) { throw new RuntimeException(e); } }
@Override public ClassLoader getClassLoader(Location location) { return new ClassLoader() { @Override protected Class<?> findClass(String name) { byte[] classBytes = ClassJavaFileObject.getClassBytes(); return super.defineClass(name, classBytes, 0, classBytes.length); } }; } }
|
完整的内存内动态编译过程
public class CompileTest { public static void main(String[] args) throws URISyntaxException, ClassNotFoundException, InstantiationException, IllegalAccessException {
String sourceCode = """ import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ConstantTransformer; public class TransformerTest implements CompileTest { public void test() { Transformer[] transformers = new Transformer[]{new ConstantTransformer(null)}; ConstantTransformer constantTransformer = new ConstantTransformer(transformers); System.out.println(constantTransformer.transform("hello").toString()); System.out.println("hello world"); } }""";
SourceJavaFileObject sourceJavaFileObject = new SourceJavaFileObject("TransformerTest", sourceCode); JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); JavaFileManager manager = new JavaFileManager(compiler.getStandardFileManager(null, null, null));
JavaCompiler.CompilationTask task = compiler.getTask(null, manager, null, null, null, List.of(sourceJavaFileObject)); task.call();
ClassLoader classLoader = manager.getClassLoader(null); Class<?> transformerTestClass = classLoader.loadClass("TransformerTest"); CompileTest transformerTest = (CompileTest) transformerTestClass.newInstance(); transformerTest.test(); }
|
其实就改了两个载体一个处理逻辑,SourceJavaFileObject和ClassJavaFileObject保证了将输入的源代码和输出的字节码都保留在内存中,JavaFileManager保证从ClassJavaFileObject获取字节码数据并依此生成类加载器