In some cases, it’s really useful to be able to compile a class at runtime using the
java.compiler
module. You can e.g. load a Java source file from the database, compile it on the fly, and execute its code as if it were part of your application.
In the upcoming jOOR 0.9.8, this will be made possible through
https://github.com/jOOQ/jOOR/issues/51. As always with jOOR (and our other projects), we’re wrapping existing JDK API, simplifying the little details that you often don’t want to worry about. Using jOOR API, you can now write:
// Run this code from within the com.example package
Supplier<String> supplier = Reflect.compile(
"com.example.CompileTest",
"package com.example;\n" +
"class CompileTest\n" +
"implements java.util.function.Supplier<String> {\n" +
" public String get() {\n" +
" return \"Hello World!\";\n" +
" }\n" +
"}\n"
).create().get();
System.out.println(supplier.get());
And the result is, of course:
Hello World!
If we already had
JEP-326, this would be even cooler!
Supplier<String> supplier = Reflect.compile(
`org.joor.test.CompileTest`,
`package org.joor.test;
class CompileTest
implements java.util.function.Supplier<String> {
public String get() {
return "Hello World!"
}
}`
).create().get();
System.out.println(supplier.get());
What happens behind the scenes?
Again, as in our previous blog post, we need to ship two different versions of our code. One that works in Java 8 (where reflecting and accessing JDK internal API was possible), and one that works in Java 9+ (where this is forbidden). The full annotated API is here:
package org.joor;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import javax.tools.*;
import static java.lang.StackWalker.Option.RETAIN_CLASS_REFERENCE;
class Compile {
static Class<?> compile(String className, String content)
throws Exception {
Lookup lookup = MethodHandles.lookup();
// If we have already compiled our class, simply load it
try {
return lookup.lookupClass()
.getClassLoader()
.loadClass(className);
}
// Otherwise, let's try to compile it
catch (ClassNotFoundException ignore) {
return compile0(className, content, lookup);
}
}
static Class<?> compile0(
String className, String content, Lookup lookup)
throws Exception {
JavaCompiler compiler =
ToolProvider.getSystemJavaCompiler();
ClassFileManager manager = new ClassFileManager(
compiler.getStandardFileManager(null, null, null));
List<CharSequenceJavaFileObject> files = new ArrayList<>();
files.add(new CharSequenceJavaFileObject(
className, content));
compiler.getTask(null, manager, null, null, null, files)
.call();
Class<?> result = null;
// Implement a check whether we're on JDK 8. If so, use
// protected ClassLoader API, reflectively
if (onJava8()) {
ClassLoader cl = lookup.lookupClass().getClassLoader();
byte[] b = manager.o.getBytes();
result = Reflect.on(cl).call("defineClass",
className, b, 0, b.length).get();
}
// Lookup.defineClass() has only been introduced in Java 9.
// It is required to get private-access to interfaces in
// the class hierarchy
else {
// This method is called by client code from two levels
// up the current stack frame. We need a private-access
// lookup from the class in that stack frame in order
// to get private-access to any local interfaces at
// that location.
Class<?> caller = StackWalker
.getInstance(RETAIN_CLASS_REFERENCE)
.walk(s -> s
.skip(2)
.findFirst()
.get()
.getDeclaringClass());
// If the compiled class is in the same package as the
// caller class, then we can use the private-access
// Lookup of the caller class
if (className.startsWith(caller.getPackageName() )) {
result = MethodHandles
.privateLookupIn(caller, lookup)
.defineClass(fileManager.o.getBytes());
}
// Otherwise, use an arbitrary class loader. This
// approach doesn't allow for loading private-access
// interfaces in the compiled class's type hierarchy
else {
result = new ClassLoader() {
@Override
protected Class<?> findClass(String name)
throws ClassNotFoundException {
byte[] b = fileManager.o.getBytes();
int len = b.length;
return defineClass(className, b, 0, len);
}
}.loadClass(className);
}
}
return result;
}
// These are some utility classes needed for the JavaCompiler
// ----------------------------------------------------------
static final class JavaFileObject
extends SimpleJavaFileObject {
final ByteArrayOutputStream os =
new ByteArrayOutputStream();
JavaFileObject(String name, JavaFileObject.Kind kind) {
super(URI.create(
"string:///"
+ name.replace('.', '/')
+ kind.extension),
kind);
}
byte[] getBytes() {
return os.toByteArray();
}
@Override
public OutputStream openOutputStream() {
return os;
}
}
static final class ClassFileManager
extends ForwardingJavaFileManager<StandardJavaFileManager> {
JavaFileObject o;
ClassFileManager(StandardJavaFileManager m) {
super(m);
}
@Override
public JavaFileObject getJavaFileForOutput(
JavaFileManager.Location location,
String className,
JavaFileObject.Kind kind,
FileObject sibling
) {
return o = new JavaFileObject(className, kind);
}
}
static final class CharSequenceJavaFileObject
extends SimpleJavaFileObject {
final CharSequence content;
public CharSequenceJavaFileObject(
String className,
CharSequence content
) {
super(URI.create(
"string:///"
+ className.replace('.', '/')
+ JavaFileObject.Kind.SOURCE.extension),
JavaFileObject.Kind.SOURCE);
this.content = content;
}
@Override
public CharSequence getCharContent(
boolean ignoreEncodingErrors
) {
return content;
}
}
}
Notice how the JDK 9 version is a bit more complicated, as we have to:
- Find the caller class of our method
- Get a private method handle lookup for that class if the class being compiled is in the same package as the class calling the compilation
- Otherwise, use an arbitrary class loader to define the class
Reflection definitely hasn’t become simpler with Java 9!
Like this:
Like Loading...
Published by lukaseder
I made jOOQ
View all posts by lukaseder
Great post! Can you do the same with test class? I mean, compile the test class, run junit tests from that class and get a result if tests passed? All done at runtime.
Try it!
Well, I can’t download 0.9.8 version using maven so can’t try it yet. Is it possible though? To compile a class, run tests and get for example http://junit.sourceforge.net/javadoc/org/junit/runner/Result.html ? How would you implement that using jOOR? Love that it’s so easy to compile a class but trying something different here ;)
You can build jOOR from github: https://github.com/jOOQ/jOOR. Or you just copy paste the above code snippets. Keep trying and let me know if it works ;-)
I have jOOR working in my Java program, but I want to implement it in an Android program. I mostly interested in the feature of building up a method from a string. Right now I’m blocked because I can’t seem to import javax.tools. Any suggestions?
I don’t think you can use that part of jOOR on Android, which doesn’t ship with most of the JDK API. Do note that jOOR doesn’t officially support Android
Hi,
How can I make it work when the java file depends on external jar libraries files?
Thanks.
Thanks for your message. If this is a question about jOOR, may I ask you to please ask your question with some more details on the issue tracker? https://github.com/jOOQ/jOOR/issues/new