Search code examples
javajvm

JavaCompiler does not recognize dynamically loaded classes


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?


Solution

  • 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:

    • Redesign the file manager to maintain a map from name to generated class file
    • Change the 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 file
    • Override getJavaFileForInput 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)
    • Redesign the class loader to have a reference to the file manager and override 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)