Java动态类编译简析

借助动态编译机制可以在程序运行时编译java源代码文件并将其编译为class文件加入运行逻辑


一般的动态编译

基本的动态编译无非是在程序工作目录下新建.java源代码文件,写入定义的类,再交给编译器进行编译,最后拿到编译的.classz字节码文件进行类加载、实例化,这里演示在文件落地的情况下进行动态编译

前期工作,我们需要获取程序的工作目录并在相同目录写入.java文件

String classPath = CompileTest.class.getResource("/").getPath();
//Windows环境下需删去路径开头的"/"
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();

成功编译并调用了方法

image-20260112191928307


无文件落地的动态编译

是否有可能让一切过程都在内存中发生而不产生具体文件呢?答案是肯定的,在调用call()方法执行编译任务时也是需要用到源代码和字节码的,接下来可以尝试去改写编译器获取以上两个文件的逻辑

前面提到StandardJavaFileManager实例用于将物理文件系统转化为 Java 编译器可以理解的对象,我们对该实例调用了getJavaFileObjects()方法获得了一个迭代器,该迭代器内部有一个SimpleFileObject实例,内部主要是指向java源代码文件的url路径

image-20260112202039305

其实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();
}

其实就改了两个载体一个处理逻辑,SourceJavaFileObjectClassJavaFileObject保证了将输入的源代码和输出的字节码都保留在内存中,JavaFileManager保证从ClassJavaFileObject获取字节码数据并依此生成类加载器