Reputation: 21281
Say I have a String
containing the content of a .java
file. Are any APIs out there that would allow me to compile this source file into a virtual .class
file (i.e. generate and store the content in memory, NOT creating an actual physical .class
file on disk)? And this "virtual" .class
would then be loaded and executed in the JVM?
Edit 1: The only reason I want to do this is because sometimes, my application might not have the write permission.
Upvotes: 2
Views: 1509
Reputation: 3587
javax.tools
has everything you need, but needs a bit of coaxing to stop storing class files on the file system. Fortunately, it can be done with a single class of about 100 lines of code:
package simple.tools;
import java.io.*;
import java.net.URI;
import java.util.*;
import javax.tools.*;
import javax.tools.JavaCompiler.CompilationTask;
public class SimpleCompiler {
public static JavaFileObject sourceFile(String name, String source) {
return inputFile(name, JavaFileObject.Kind.SOURCE, source);
}
private static URI uri(String name, JavaFileObject.Kind kind) {
return URI.create(name.replace('.', '/') + kind.extension);
}
private final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
private final JavaFileManager manager =
new ForwardingJavaFileManager<JavaFileManager>(compiler.getStandardFileManager(null, null, null)) {
@Override
public JavaFileObject getJavaFileForOutput(Location l, String name, JavaFileObject.Kind kind, FileObject f) {
return outputFile(name, kind);
}
};
private static JavaFileObject inputFile(String name, JavaFileObject.Kind kind, String content) {
return new SimpleJavaFileObject(uri(name, kind), kind) {
@Override
public CharSequence getCharContent(boolean b) {
return content;
}
};
}
private JavaFileObject outputFile(String name, JavaFileObject.Kind kind) {
return new SimpleJavaFileObject(uri(name, kind), kind) {
@Override
public OutputStream openOutputStream() {
return outputStream(name);
}
};
}
private final Map<String, byte[]> classes = new HashMap<>();
private OutputStream outputStream(String name) {
return new ByteArrayOutputStream() {
@Override
public void close() {
classes.put(name, toByteArray());
}
};
}
private final ClassLoader loader = new ClassLoader() {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] bytes = classes.get(name);
if (bytes == null) throw new ClassNotFoundException(name);
return super.defineClass(name, bytes, 0, bytes.length);
}
};
public Class<?> compile(String name, String source) {
compile(sourceFile(name, source));
try { return loadClass(name); }
catch (ClassNotFoundException e) { throw new IllegalStateException(e.toString(), e); }
}
public void compile(JavaFileObject... files) {
compile(Arrays.asList(files));
}
public void compile(List<JavaFileObject> files) {
if (files.isEmpty()) throw new RuntimeException("No input files");
DiagnosticCollector<JavaFileObject> collector = new DiagnosticCollector<>();
CompilationTask task = compiler.getTask(null, manager, collector, null, null, files);
boolean success = task.call();
check(success, collector);
}
private void check(boolean success, DiagnosticCollector<?> collector) {
for (Diagnostic<?> diagnostic : collector.getDiagnostics()) {
if (diagnostic.getKind() == Diagnostic.Kind.ERROR) {
String message = diagnostic.getMessage(Locale.US);
throw new RuntimeException(message.split("[\r\n]")[0]);
}
}
if (! success) throw new RuntimeException("Unknown error");
}
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loader.loadClass(name);
}
}
See https://gitlab.com/jcsahnwaldt/simple-java-tools.
Upvotes: 0
Reputation: 11185
Java does have a compilation API to compile files dynamically, but I'm not aware of an option that would not persist the class files to disk. You can always use a ClassLoader
and load those classes dynamically and then use them. You might be able to load the classes in memory by overriding the getFileForOutput method.
Optionally, this file manager might consider the sibling as a hint for where to place the output. The exact semantics of this hint is unspecified. The JDK compiler, javac, for example, will place class files in the same directories as originating source files unless a class file output directory is provided. To facilitate this behavior, javac might provide the originating source file as sibling when calling this method.
Another option is to use an Interpreter like BeanShell that will run the java code for you. It executes script like code and can work in repl mode.
Upvotes: 1
Reputation: 168815
Use the JavaCompiler
for this. I think the trick will be to define a custom JavaFileManager
.
Upvotes: 1