I am facing a problem with compiling Java generated classes in-memory and I hope someone can point to where my problem is. I will simply the example and try to explain as easy as I can.
I have some code that is being generated that looks like this:
package packOne;
public final class TestClassOne {
public static String doSomething() {
return "one";
}
}
package packTwo;
public final class TestClassTwo {
public void printSomething() {
System.out.println(packOne.TestClassOne.doSomething());
}
}
These are generated at runtime and I need to compile them and have them available. For this, I am using JavaCompiler
in a Compiler
class which (again, simplified) looks like this:
public class Compiler {
private final JavaCompiler javaCompiler;
private final DynamicClassLoader dynamicClassLoader;
private final DiagnosticCollector<JavaFileObject> diagnostics;
private final ByteCodeGeneratingFileManager fileManager;
public Compiler(DynamicClassLoader dynamicClassLoader) {
this.dynamicClassLoader = dynamicClassLoader;
this.javaCompiler = ToolProvider.getSystemJavaCompiler();
this.diagnostics = new DiagnosticCollector<>();
this.fileManager = new ByteCodeGeneratingFileManager(javaCompiler.getStandardFileManager(diagnostics, null, null));
}
public byte[] compile(String className, String classContent) {
// Compile the code
JavaFileObject javaFileObject = new SourceCodeObject(className, classContent);
JavaCompiler.CompilationTask task = javaCompiler.getTask(null, fileManager, diagnostics, null, null, List.of(javaFileObject));
Boolean result = task.call();
if (result == null || !result) {
throw new CompilationException(className, "Compilation failed.");
}
return fileManager.getBytecode();
}
public void load(String className, byte[] bytecode) {
dynamicClassLoader.loadClass(className, bytecode);
}
}
public class DynamicClassLoader extends ClassLoader {
public Class<?> loadClass(String className, byte[] bytecode) {
return defineClass(className, bytecode, 0, bytecode.length);
}
}
class ByteCodeGeneratingFileManager extends ForwardingJavaFileManager<JavaFileManager> {
private ByteArrayOutputStream bytecode;
ByteCodeGeneratingFileManager(JavaFileManager fileManager) {
super(fileManager);
}
@Override
public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) {
return new ByteCodeObject(className, kind);
}
public byte[] getBytecode() {
return bytecode.toByteArray();
}
class ByteCodeObject extends SimpleJavaFileObject {
ByteCodeObject(String name, JavaFileObject.Kind kind) {
super(URI.create("bytecode:///" + name + kind.extension), kind);
}
@Override
public OutputStream openOutputStream() {
bytecode = new ByteArrayOutputStream();
return bytecode;
}
}
}
I first compile the code for TestClassOne
and than using the compiler I call compiler.load("packOne.TestClassOne", byteCode)
to load the class. This Works as intended.
The problem is when I try to compile TestClassTwo
. When I do this, I get the following error:
error: package packOne does not exist
, just as if it does not recognize the first generated class. I am using the same compiler instance and hence the same fileManager instance.
Can anyone please let me know why is this happening and what would be a possible fix?
You can easily retrace the problem with non-dynamic compilation, e.g. javac
on the command line, when using a target directory as with the -d
option. Compiling the second class in a separate pass will not find the class generated in the first pass, unless you add the output directory to the class path.
Since you’re not storing the generated classes in a directory you don’t have a directory to add to the class path. Instead, your custom file manager must be prepared to provide the stored class files when the compiler asks for it. But your file manager has the fundamental problem of not being designed to remember more than one class file. Likewise, your class loader is not prepared to provide classes on demand. So it will only work for the simplest incremental build as you’re trying now but, e.g. just having an inner class would already break it.
Therefore, the recommended steps are:
compile
method to return the class file from the file manager’s map matching the desired class, in case the source file produces more than one class filegetJavaFileForInput
to check whether the class is among the generated class files (and kind == CLASS
) to return it (and delegate to super
otherwise to find the predefined classes)findClass
, to also check whether a class file for the requested name exists in the map and define and return the class (findClass
will only be invoked for not-yet-defined classes)