There is no way to use a ClassLoader
as reference, unless it is capable of providing the class bytes of its defined classes. I.e., if you have a Class
instance representing a top-level class, you can use classInstance.getResourceAsStream(classInstance.getSimpleName()+".class")
to try to get hands on the class bytes. If you have access to the bytes that make up the dynamic class, you can make them available to the java compiler via a JavaFileManager
implementation.
The compiler API is part of the standard API and doesn’t require 3rd party libraries. The following code demonstrates this by compiling a test class first, then setting the necessary environment to compile a second class depending on the class just created in the previous step:
// customize these, if you want, null triggers default behavior
DiagnosticListener<JavaFileObject> diagnosticListener = null;
Locale locale = null;
// the first class, to be present at runtime only
String class1 = "package test;\npublic class Class1 {}";
JavaCompiler c = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fm
= c.getStandardFileManager(diagnosticListener, locale, Charset.defaultCharset());
// define where to store compiled class files - use a temporary directory
fm.setLocation(StandardLocation.CLASS_OUTPUT, Collections.singleton(
Files.createTempDirectory("compile-test").toFile()));
JavaCompiler.CompilationTask task = c.getTask(null, fm,
diagnosticListener, Collections.emptySet(), Collections.emptySet(),
Collections.singleton(new SimpleJavaFileObject(
URI.create("string:///Class1.java"), Kind.SOURCE) {
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return class1;
}
}));
if(task.call()) {
FileObject fo = fm.getJavaFileForInput(
StandardLocation.CLASS_OUTPUT, "test.Class1", Kind.CLASS);
// these are the class bytes of the first class
byte[] class1bytes = Files.readAllBytes(Paths.get(fo.toUri()));
// the actual task: define a class dependent on the first class
String class2 = "package test;\npublic class Class2 { Class1 variable; }";
// create a file object representing the dynamic class
JavaFileObject jo = new SimpleJavaFileObject(
URI.create("runtime:///test/Class1.class"), Kind.CLASS) {
@Override public InputStream openInputStream() throws IOException {
return new ByteArrayInputStream(class1bytes);
}
};
// and a custom file manager knowing how to locate that class
JavaFileManager myFM = new ForwardingJavaFileManager(fm) {
@Override
public JavaFileObject getJavaFileForInput(
JavaFileManager.Location location, String className, Kind kind)
throws IOException {
if(location==StandardLocation.CLASS_PATH&&className.equals("test.Class1")) {
return jo;
}
return super.getJavaFileForInput(location, className, kind);
}
@Override
public boolean hasLocation(JavaFileManager.Location location) {
return location==StandardLocation.CLASS_PATH || super.hasLocation(location);
}
@Override
public Iterable list(JavaFileManager.Location location,
String packageName, Set kinds, boolean recurse) throws IOException {
if(location==StandardLocation.CLASS_PATH
&& (packageName.equals("test") || recurse&&packageName.isEmpty())) {
return Collections.singleton(jo);
}
return super.list(location, packageName, kinds, recurse);
}
@Override
public String inferBinaryName(
JavaFileManager.Location location, JavaFileObject file) {
if(file==jo) return "test.Class1";
return super.inferBinaryName(location, file);
}
};
// compile the second class using the custom file manager to locate dependencies
task = c.getTask(null, myFM,
diagnosticListener, Collections.emptySet(), Collections.emptySet(),
Collections.singleton(new SimpleJavaFileObject(
URI.create("string:///Class2.java"), Kind.SOURCE) {
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return class2;
}
}));
if(task.call()) {
fo = fm.getJavaFileForInput(
StandardLocation.CLASS_OUTPUT, "test.Class2", Kind.CLASS);
// there we have the compiled second class
byte[] class2bytes = Files.readAllBytes(Paths.get(fo.toUri()));
}
}
Of course, this is only for demonstrating the principle. You surely want to create factory methods for the file objects and use Map
s for remembering them, etc.
It’s also possible to replace the temporary directory with a custom in-memory storage. But the key point remains, that the compiler needs to be able to access the class bytes. It won’t use loaded runtime classes.