I am trying to compile and load a class dynamically during runtime using the JavaCompiler API. I store the compiled bytecode in memory. So I use a custom class loader to load the class.
public class CompilerAPITest {
static String sourceCode = "package in.test;" +
"public class DynamicCompilationHelloWorld implements TestInterface{" +
"public void test (){" +
"System.out.println (\"Hello, dynamic compilation world!\");" +
"new in.test.another.SomeClass().fun();" +
"}" +
"}" ;
public void doCompilation (){
SimpleJavaFileObject fileObject = new DynamicJavaSourceCodeObject ("in.test.DynamicCompilationHelloWorld", sourceCode) ;
JavaFileObject javaFileObjects[] = new JavaFileObject[]{fileObject} ;
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
JavaFileManager stdFileManager = new
CompilerAPITest.ClassFileManager(compiler
.getStandardFileManager(null, null, null));
Iterable<? extends JavaFileObject> compilationUnits = Arrays.asList(javaFileObjects);
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<JavaFileObject>();
List<String> options = Arrays.asList("-cp", System.getProperty("java.class.path")
+ ":" + getPath(CompilerAPITest.class));
CompilationTask compilerTask = compiler.getTask(null, stdFileManager, diagnostics, options, null, compilationUnits) ;
boolean status = compilerTask.call();
if (!status){//If compilation error occurs
/*Iterate through each compilation problem and print it*/
for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()){
System.out.format("Error on line %d in %s", diagnostic.getLineNumber(), diagnostic);
}
}
try {
stdFileManager.close() ;//Close the file manager
} catch (IOException e) {
e.printStackTrace();
}
try {
stdFileManager.getClassLoader(null)
.loadClass("in.test.DynamicCompilationHelloWorld").asSubclass(TestInterface.class).newInstance().test();
} catch (ClassNotFoundException e) {
e.printStackTrace();
//This does nothing.
} catch (InstantiationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void main(String args[]){
new CompilerAPITest ().doCompilation() ;
}
class DynamicJavaSourceCodeObject extends SimpleJavaFileObject{
private String qualifiedName ;
private String sourceCode ;
protected DynamicJavaSourceCodeObject(String name, String code) {
super(URI.create("string:///" +name.replaceAll("\\.", "/") + Kind.SOURCE.extension), Kind.SOURCE);
this.qualifiedName = name ;
this.sourceCode = code ;
}
#Override
public CharSequence getCharContent(boolean ignoreEncodingErrors)
throws IOException {
return sourceCode ;
}
public String getQualifiedName() {
return qualifiedName;
}
public void setQualifiedName(String qualifiedName) {
this.qualifiedName = qualifiedName;
}
public String getSourceCode() {
return sourceCode;
}
public void setSourceCode(String sourceCode) {
this.sourceCode = sourceCode;
}
}
private static class ClassFileManager extends
ForwardingJavaFileManager<JavaFileManager> {
private JavaClassObject jclassObject;
public ClassFileManager(StandardJavaFileManager
standardManager) {
super(standardManager);
}
#Override
public ClassLoader getClassLoader(Location location) {
return new java.security.SecureClassLoader() {
#Override
protected Class<?> findClass(String name)
throws ClassNotFoundException {
byte[] b = jclassObject.getBytes();
return super.defineClass(name, jclassObject
.getBytes(), 0, b.length);
}
};
}
#Override
public JavaFileObject getJavaFileForOutput(Location location,
String className, Kind kind, FileObject sibling)
throws IOException {
jclassObject = new JavaClassObject(className, kind);
return jclassObject;
}
}
private static class JavaClassObject extends SimpleJavaFileObject {
protected final ByteArrayOutputStream bos =
new ByteArrayOutputStream();
public JavaClassObject(String name, Kind kind) {
super(URI.create("string:///" + name.replace('.', '/')
+ kind.extension), kind);
}
public byte[] getBytes() {
return bos.toByteArray();
}
#Override
public OutputStream openOutputStream() throws IOException {
return bos;
}
}
}
This works fine when run in a standalone setup. However when i call doCompilation() in my production setup, which runs on JBoss, i get the following exception.
java.lang.NoClassDefFoundError:
in/test/TestInterface(wrong name:
in/test/DynamicCompilationHelloWorld)
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:621)
at java.lang.ClassLoader.defineClass(ClassLoader.java:466)
at in.test.CompilerAPITest$ClassFileManager$1.findClass(CompilerAPITest.java:126)
What could be the problem here?
Found the issue after some googling.
Parent class loader needs to be set for the custom class loader used.
Class loading hierarchy is slightly different in JBoss. Here UnifiedClassLoader is used, which is used for checking even the peer class loaders, in addition to the parents, before throwing ClassNotFoundException. So when a custom class loader is used, it needs to delegate the defineClass calls to the UnifiedClassLoader, when it can not load the class.
Here is an example code snippet for a custom class loader.
private static class ByteClassLoader extends ClassLoader{
private Map<String, JavaFileObject> store = new HashMap<String, JavaFileObject>();
public ByteClassLoader(Map<String, JavaFileObject> str)
{
super( ByteClassLoader.class.getClassLoader() ); // set parent
store = str;
}
protected Class<?> findClass(String name)
throws ClassNotFoundException{
JavaFileObject jfo = store.get(name);
if (jfo == null){
throw new ClassNotFoundException(name);
}
byte[] bytes = ((JavaClassObject)jfo).getBytes();
Class<?> cl = defineClass(name, bytes, 0, bytes.length);
if (cl == null){
throw new ClassNotFoundException(name);
}
return cl;
}
}
And for loading the class,
ByteClassLoader cl = new ByteClassLoader(store);
cl.loadClass(className);
needs to be used.
Here is an excellent link on dynamic compilation and class loading.
http://fivedots.coe.psu.ac.th/~ad/jg/javaArt1/onTheFlyArt1.pdf
Related
I am attempting to instantiate a dynamically loaded class (with a custom class loader) with a constructor that contains both a String object and a custom class I have created.
I have re-created the issue outside of my project, this is assuming that A.class is already in the target folder.
Main method:
public class simplifiedMainMethod {
static boolean propagate = false;
static Class<?> projectActions = null;
static Object oInstance;
public static void main (String[] args)
{
String projectName = "A";
String baseUrl = "https://www.google.com/";
simplifiedReport reporter = new simplifiedReport();
try
{
ClassLoader cl = Thread.currentThread().getContextClassLoader();
Reloader r = new Reloader(cl);
if (propagate)
Thread.currentThread().setContextClassLoader(r);
projectActions = r.loadClass("autobots_framework."+projectName);
Constructor<?> c = projectActions.getConstructor(String.class, simplifiedReport.class);
oInstance = c.newInstance(baseUrl, reporter);
}
catch (Exception e) {
e.printStackTrace();
}
}
}
simplifiedReport.java:
public class simplifiedReport
{
public simplifiedReport()
{
}
public void testMethod()
{
System.out.println("Hello World");
}
}
A.java:
public class A
{
public A(String testString, simplifiedReport testReport)
{
System.out.println(testString);
testReport.testMethod();
}
}
Here is the CustomClassLoader I am using:
public class ClassLoaderCompiler
{
public ClassLoaderCompiler()
{}
public static boolean compileCode(String fileDirectory, String projectName) throws IOException
{
JavaCompiler compiler = new EclipseCompiler();
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<JavaFileObject>();
StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null);
Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjectsFromStrings(Arrays.asList(fileDirectory));
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnostics, null, null, compilationUnits);
try
{
task.call();
fileManager.close();
return true;
}
catch(Exception e)
{
fileManager.close();
return false;
}
}
public static void moveClassFile(String projectName)
{
try
{
Files.deleteIfExists(new File(System.getProperty("user.dir")+"/target/classes/autobots_framework/"+projectName+".class").toPath());
Files.deleteIfExists(new File(System.getProperty("user.dir")+"/target/classes/autobots_framework/L_"+projectName+".class").toPath());
if (Files.exists(Paths.get(System.getProperty("user.dir")+"/projects/"+projectName+"/"+projectName+".class")))
{
Path temp = Files.move (Paths.get(System.getProperty("user.dir")+"/projects/"+projectName+"/"+projectName+".class"),Paths.get(System.getProperty("user.dir")+"/target/classes/autobots_framework/"+projectName+".class"));
Path tempLocator = Files.move (Paths.get(System.getProperty("user.dir")+"/projects/"+projectName+"/L_"+projectName+".class"),Paths.get(System.getProperty("user.dir")+"/target/classes/autobots_framework/L_"+projectName+".class"));
}
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
/**
* adapted from http://stackoverflow.com/a/3971771/7849
*/
class Reloader extends ClassLoader {
static URL url;
ClassLoader orig;
Reloader(ClassLoader orig) {
this.orig = orig;
}
#Override
public Class<?> loadClass(String s) {
return findClass(s);
}
#Override
public Class<?> findClass(String s) {
try {
byte[] bytes = loadClassData(s);
return defineClass(s, bytes, 0, bytes.length);
} catch (IOException ioe) {
try {
return super.loadClass(s);
} catch (ClassNotFoundException ignore) {
ignore.printStackTrace(System.out);
}
ioe.printStackTrace(System.out);
return null;
}
}
private byte[] loadClassData(String className) throws IOException {
try {
/*
* get the actual path using the original classloader
*/
Class<?> clazz = orig.loadClass(className);
url = clazz.getResource(clazz.getSimpleName() + ".class");
/*
* force reload
*/
File f = new File(url.toURI());
int size = (int) f.length();
byte buff[] = new byte[size];
FileInputStream fis = new FileInputStream(f);
DataInputStream dis = new DataInputStream(fis);
dis.readFully(buff);
dis.close();
return buff;
} catch (Exception ex) {
throw new IOException(ex);
}
}
}
I am getting this error as a result:
java.lang.NoSuchMethodException: autobots_framework.A.<init>(java.lang.String, autobots_framework.simplifiedReport)
at java.lang.Class.getConstructor0(Unknown Source)
at java.lang.Class.getConstructor(Unknown Source)
I figured this was due to the parameters not matching, so to confirm I tried finding what the constructor is implicitly looking for:
for (Constructor c : projectActions.getDeclaredConstructors())
{
System.out.print(c.toGenericString());
}
Yielding this result:
public autobots_framework.A(java.lang.String,autobots_framework.simplifiedReport)
Which is frustratingly looking a lot like what I have put here:
Constructor<?> c = projectActions.getConstructor(String.class, simplifiedReport.class);
oInstance = c.newInstance(baseUrl, reporter);
I understand that this has something to do with my class, as when I leave out simplifiedReport in the constructor and only instantiating it with String.class, I am successful. What modifier am I missing in my class to make this work?
This is all a function of my application to dynamically compile code and then instantiate them to run custom code and methods.
I made a java project, the project only contais this class:
package test.processor;
public abstract class Processor {
public abstract void loadData(String objectId);
public abstract void processData();
public abstract void saveData(String objectId);
}
The project is exported as a jar file (processor.jar)
Then I made another project that imports processor.jar and there is a class that extends Processor:
package test.process;
import test.processor.Processor;
public class Process extends Processor{
#Override
public void loadData(String objectId) {
System.out.println("LOAD DATAAAAAAAAAAAA");
}
#Override
public void processData() {
System.out.println("PROCESS DATAAAAAAAAAAAA");
}
#Override
public void saveData(String objectId) {
System.out.println("SAVE DATAAAAAAAAAAAA");
}
}
This project is also exported as jar (plugin.jar).
Finally, I coded something to load the plugins dynamically:
import test.processor.Processor;
public class Test {
public void testPlugins(){
Processor plugin = (Processor) loadJar(
"C:\\Users\\...\\Desktop\\plugin.jar",
"test.process.Process");
processor.loadData("dada");
}
private Object loadJar(String jar, String className){
File jarFile = new File(jar);
Object instance = null;
try {
URL jarpath = jarFile.toURI().toURL();
String jarUrl = "jar:" + jarpath + "!/";
URL urls[] = { new URL(jarUrl) };
URLClassLoader child = new URLClassLoader(urls);
Class classToLoad = Class.forName(nomeClasse, true, child);
instance = classToLoad.newInstance();
} catch (Exception ex) {
ex.printStackTrace();
}
return instance;
}
}
If I run that code inside a main method it works correctly, once I try to run it in the server there is a problem when loading the class, I get a ClassNotFoundException (Processor).
I tried putting the jar in the tomcat/lib, project/WEB-INF/lib and nothing changed.
Any idea of what Im doing wrong?
I didn't solve it the way I wanted, but I solved it:
First I tried loading the process.jar manually:
private Object loadJars(String processJar, String pluginJar, String className){
File processJarFile = new File(processJar);
File pluginJarFile = new File(pluginJar);
Object instance = null;
try {
URL processJarPath = processJarFile.toURI().toURL();
String processJarUrl = "jar:" + processJarPath + "!/";
URL pluginJarPath = pluginJarFile.toURI().toURL();
String pluginJarUrl = "jar:" + pluginJarPath + "!/";
URL urls[] = { new URL(processJarUrl), new URL(pluginJarUrl) };
URLClassLoader child = new URLClassLoader(urls);
Class classToLoad = Class.forName(nomeClasse, true, child);
instance = classToLoad.newInstance();
} catch (Exception ex) {
ex.printStackTrace();
}
return instance;
}
That loads the Process class correctly, the problem happens in the testPlugins mehod, once it tries to cast to Processor (ClassCastException, can't cast Process to Processor):
public void testPlugins(){
Processor plugin = (Processor) loadJars("C:\\Users\\...\\Desktop\\processor.jar",
"C:\\Users\\...\\Desktop\\plugin.jar",
"test.process.Process");
processor.loadData("dada");
}
Still need to read a lot about classloading but I guess the problem is that it doesn't recognize the Processor loaded from C:\Users\...\Desktop\processor.jar as the same as the Processor loaded from the webapp context or it "forgets" Process extends Processor.
I was in a hurry so I didn't have time to research, to solve the problem I invoked the methods using reflection:
public void modifiedTestPlugins(){
Object plugin = loadJar("C:\\Users\\...\\Desktop\\processor.jar",
"C:\\Users\\...\\Desktop\\plugin.jar",
"test.process.Process");
try {
Method processData = findMethod(obj.getClass(), "processData");
//here I invoke the processData method, it prints: PROCESS DATAAAAAAAAAAAA
loadData.invoke(processData, new Object[]{});
} catch (Exception e) {
e.printStackTrace();
}
}
private static Method findMethod(Class clazz, String methodName) throws Exception {
Method[] methods = clazz.getMethods();
for (int i = 0; i < methods.length; i++) {
if (methods[i].getName().equals(methodName))
return methods[i];
}
return null;
}
Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 2 years ago.
Improve this question
Objective:
Run a class
Change a second class
Save and Compile second class
Without stopping and starting first class the changes to second class should be visible in the console
Problem:
Currently the changes do not reflect after saving and compiling.
I think Joachim's code fragment works perfectly fine (not tested):
public class Autosaver implements Runnable {
public static void main(String args[]) {
Autosaver instance = new Autosaver();
Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(instance, 0, 5, TimeUnit.SECONDS);
}
#Override
public void run() {
try {
Class<? extends Test> Test_class = reloadClass(Test.class.getProtectionDomain().getCodeSource().getLocation(), Test.class.getName());
new Autorunner(Test_class, new File("Test.txt")).run();
} catch (Exception e) {
e.printStackTrace();
}
}
private static <X> Class<X> reloadClass(URL classLocation, String className) throws ClassNotFoundException, IOException {
URLClassLoader loader = new URLClassLoader(new URL[] { classLocation }, String.class.getClassLoader());
#SuppressWarnings({"unchecked"})
Class<X> result = (Class<X>) loader.loadClass(className);
loader.close();
return result;
}
}
If you want to reload a changed classfile, you don't have to ask the classloader which has already loaded the pre-version, this will deliver always this already loaded version . Use a new classloader, for example
...
Class<?> reloadClass(String classLocation, String className) throws Exception {
URL url = new File(classLocation).toURI().toURL();
URLClassLoader cl = new URLClassLoader(new URL[] { url }, String.class.getClassLoader());
Class<?> c = cl.loadClass(className);
cl.close();
return c;
}
...
EDIT:
Ok, i tested it with a simplified version of your code. The changes in my is only a little bit cosmetic (copied from Binkan Salaryman). It works.
public class Autorunner extends Thread {
private Class runnable;
private File output;
public Autorunner(Class runnable, File output) {
this.runnable = runnable;
this.output = output;
}
#Override
public void run() {
try {
//This is only to get the location of the classfile
URL url = Test.class.getProtectionDomain().getCodeSource().getLocation();
Class runtimeClass = reloadClass(url,Test.class.getName());
Method method = runtimeClass.getMethod("main", String[].class);
method.invoke(null, (Object) null);
System.out.flush();
} catch (NoSuchMethodException | SecurityException | IOException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | ClassNotFoundException ex) {
}
}
Class<?> reloadClass(URL classLocation, String className) throws ClassNotFoundException, IOException {
URLClassLoader cl = new URLClassLoader(new URL[] { classLocation }, String.class.getClassLoader());
Class<?> c = cl.loadClass(className);
cl.close();
return c;
}
Here is a tested, fully working, demonstrative class hotswap test program.
Before running, you need to create "./Test.jar" and "./tmp/Test.jar" and put in a file "Test.class" (without package in code, without folder in jar) you've compiled with a main method and a System.out.println statement.
If something does not work as expected, be sure to give detailed error descriptions and what you've tried.
Code for "./Autosaver.jar" (name doesn't matter):
public class Program {
private static final File Test_classLocation = new File("./Test.jar");
private static final File alternativeTest_classLocation = new File("./tmp/Test.jar");
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
System.out.println("Test.class location = " + Test_classLocation.getAbsolutePath());
System.out.println("alternative Test.class location = " + alternativeTest_classLocation.getAbsolutePath());
while (true) {
testInvocation();
swapFiles(Test_classLocation, alternativeTest_classLocation);
Thread.sleep(3000L);
testInvocation();
swapFiles(Test_classLocation, alternativeTest_classLocation);
Thread.sleep(3000L);
}
}
private static void testInvocation() throws IOException, ClassNotFoundException {
Class<?> Test_class = reloadClass(Test_classLocation.toURI().toURL(), "Test");
invokeMain(Test_class, new String[0]);
}
private static void swapFiles(File a, File b) throws IOException {
Path aTempPath = new File(b.getAbsolutePath() + ".tmp").toPath();
Files.move(a.toPath(), aTempPath);
Files.move(b.toPath(), a.toPath());
Files.move(aTempPath, b.toPath());
}
private static <X> Class<X> reloadClass(URL classLocation, String className) throws ClassNotFoundException, IOException {
URLClassLoader loader = new URLClassLoader(new URL[]{classLocation}, null);
#SuppressWarnings({"unchecked"})
Class<X> result = (Class<X>) loader.loadClass(className);
loader.close();
return result;
}
private static void invokeMain(Class<?> mainClass, String[] args) {
try {
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
mainMethod.invoke(null, new Object[]{args});
} catch (NoSuchMethodException | IllegalAccessException e) {
throw new Error(e);
} catch (InvocationTargetException e) {
System.err.println("invocation of " + mainClass.getName() + ".main(" + String.join(",", args) + ") threw an exception:");
e.printStackTrace();
}
}
}
Code for "./Test.jar!Test.class":
public class Test {
public static void main(String[] args) {
System.out.println("old " + Test.class);
}
}
Code for "./tmp/Test.jar!Test.class":
public class Test {
public static void main(String[] args) {
System.out.println("new" + Test.class);
}
}
Output:
Test.class location = D:\rd\test\out\artifacts\Autosaver\.\Test.jar
alternative Test.class location = D:\rd\test\out\artifacts\Autosaver\.\tmp\Test.jar
new class Test
old class Test
new class Test
old class Test
new class Test
old class Test
new class Test
old class Test
new class Test
old class Test
new class Test
old class Test
new class Test
old class Test
new class Test
old class Test
new class Test
...
You can download a zip of the demo here.
When trying to implement a sandbox in a plugin-like environment, I came accross https://stackoverflow.com/a/5580239/2057294 which seems to be exactly what I want, however I am unable to get it working, what am I doing wrong?
I have the following setup:
final class ModURLClassLoader extends URLClassLoader {
ModURLClassLoader(final URL[] urls) {
super(urls);
}
ModURLClassLoader(final URL[] urls, final ClassLoader parent) {
super(urls, parent);
}
ModURLClassLoader(final URL[] urls, final ClassLoader parent, final URLStreamHandlerFactory factory) {
super(urls, parent, factory);
}
#Override
protected PermissionCollection getPermissions(final CodeSource codesource) {
PermissionCollection permissionCollection = super.getPermissions(codesource);
//give no permissions to the codesource
return permissionCollection;
}
#Override
protected Class<?> loadClass(final String name, final boolean resolve) throws ClassNotFoundException {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
int i = name.lastIndexOf('.');
if (i != -1) {
sm.checkPackageAccess(name.substring(0, i));
}
}
return super.loadClass(name, resolve);
}
}
The idea here is that code loaded through the ModURLClassLoader may have no permissions at all. I do want to be able to add extra permissions statically in case I block some permissions which a mod would generally want to have.
Then I load up my classes via the following:
public class JavaMod extends LoadableMod {
private ECSMod ecsMod;
private ModURLClassLoader modUrlClassLoader;
JavaMod(final Path modDirectory) throws ModNotLoadableException {
super(modDirectory);
}
#Override
protected void load0() throws ModNotLoadableException {
try {
Properties properties = ModLoaderHelper.getConfiguration(modDirectory);
String jarName = properties.getProperty("jar");
String entryPoint = properties.getProperty("entryPoint");
Path jarPath = modDirectory.resolve(jarName);
try {
modUrlClassLoader = AccessController.doPrivileged((PrivilegedExceptionAction<ModURLClassLoader>)() -> new ModURLClassLoader(new URL[] { jarPath.toUri().toURL() }, getClass().getClassLoader()));
} catch (PrivilegedActionException ex) {
throw new ModNotLoadableException(ex);
}
Class<?> clazz = Class.forName(entryPoint, false, modUrlClassLoader);
if (!ECSMod.class.isAssignableFrom(clazz)) {
throw new ModNotLoadableException(clazz + " does not implement ECSMod");
}
this.ecsMod = (ECSMod)clazz.newInstance();
} catch (Exception ex) {
throw new ModNotLoadableException(ex);
}
}
#Override
protected void unload0() {
try {
modUrlClassLoader.close();
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
#Override
protected ECSGame createGame0() {
ECSGame ecsGame = new ECSGame();
ecsMod.setupGame(ecsGame);
return ecsGame;
}
}
Which loads an untrusted JAR that has a class that implements ECSMod, I have confirmed that both the clazz and ecsMod belong to an instance of ModURLClassLoader.
Then the malicious code:
public final class UntrustedEvilMod implements ECSMod {
#Override
public void setupGame(final ECSGame game) {
System.exit(0);
}
}
Where ECSMod and ECSGame are classes that belong to the plugin (also called mod) API and reside in the original classloader.
I'm running this without a security manager, as I believe that this would be unnecessary in this context? As the ModURLClassLoader defines the security.
Why is this not working, meaning that it still causes the program to exit, whereas I would've wanted an AccessControlException?
Despite warnings to drop my present course of action, I currently see no better way to solve my problem. I must generate Java code at runtime, then compile it, load it and reference it.
Problem is that the generated code imports code that has already been loaded by the system class loader (I suppose) - that is, code present in one of the jars on my classpath.
(I run inside a Tomcat 6 web container over Java 6.) You may ask yourselves why that is a problem - well I sure don't know - but fact is that I get compilation errors:
/W:/.../parser/v0.5/AssignELParser.java:6:
package com.xxx.yyy.zzz.configuration
does not exist
Following some examples off the internet I have defined the following classes:
class MemoryClassLoader extends ChainedAction {
private static final Logger LOG = Logger.getLogger(MemoryClassLoader.class);
private LoaderImpl impl;
private class LoaderImpl extends ClassLoader {
// The compiler tool
private final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
// Compiler options
private final Iterable<String> options = Arrays.asList("-verbose");
// DiagnosticCollector, for collecting compilation problems
private final DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<JavaFileObject>();
// Our FileManager
private final MemoryFileManager manager = new MemoryFileManager(this.compiler);
public LoaderImpl(File sourceDirectory) {
List<Source> list = new ArrayList<Source>();
File[] files = sourceDirectory.listFiles(new FilenameFilter() {
#Override
public boolean accept(File dir, String name) {
return name.endsWith(Kind.SOURCE.extension);
}
});
for (File file : files) {
list.add(new Source(file));
}
CompilationTask task = compiler.getTask(null, manager, diagnostics, options, null, list);
Boolean compilationSuccessful = task.call();
LOG.info("Compilation has " + ((compilationSuccessful) ? "concluded successfully" : "failed"));
// report on all errors to screen
for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) {
LOG.warn(diagnostic.getMessage(null));
}
}
#Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
synchronized (this.manager) {
Output output = manager.map.remove(name);
if (output != null) {
byte[] array = output.toByteArray();
return defineClass(name, array, 0, array.length);
}
}
return super.findClass(name);
}
}
#Override
protected void run() {
impl = new LoaderImpl(new File(/* Some directory path */));
}
}
class MemoryFileManager extends ForwardingJavaFileManager<JavaFileManager> {
final Map<String, Output> map = new HashMap<String, Output>();
MemoryFileManager(JavaCompiler compiler) {
super(compiler.getStandardFileManager(null, null, null));
}
#Override
public Output getJavaFileForOutput(Location location, String name, Kind kind, FileObject source) {
Output output = new Output(name, kind);
map.put(name, output);
return output;
}
}
class Output extends SimpleJavaFileObject {
private final ByteArrayOutputStream baos = new ByteArrayOutputStream();
Output(String name, Kind kind) {
super(URI.create("memo:///" + name.replace('.', '/') + kind.extension), kind);
}
byte[] toByteArray() {
return this.baos.toByteArray();
}
#Override
public ByteArrayOutputStream openOutputStream() {
return this.baos;
}
}
class Source extends SimpleJavaFileObject {
public Source(File file) {
super(file.toURI(), Kind.SOURCE);
}
#Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
StringBuilder sb = new StringBuilder("");
try {
File file = new File(uri);
FileReader fr = new FileReader(file);
BufferedReader br = new BufferedReader(fr);
sb = new StringBuilder((int) file.length());
String line = "";
while ((line = br.readLine()) != null) {
sb.append(line);
sb.append("\n");
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return sb.toString();
}
}
It seems that the inner class LoaderImpl by extending the ClassLoader class and by not calling an explicit super constructor should reference as its parent class loader the system class loader.
If it does so then why do I get the "runtime" compilation error - above? Why does it not find the code for the imported class?
Not sure if it can help, but have you tried to specify classpath explicitly?
getClassPath()
{
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
URL[] urls = ((URLClassLoader) classLoader).getURLs();
StringBuilder buf = new StringBuilder(1000);
buf.append(".");
String separator = System.getProperty("path.separator");
for (URL url : urls) {
buf.append(separator).append(url.getFile());
}
}
classPath = buf.toString();
and then
options.add("-classpath");
options.add(getClassPath());
I also can't see where do you pass LoaderImpl instance to the compiler. Shouldn't it be done explicitly?