I'm trying to write a scripting system in Java and I've managed to get my scripts to compile and instantiate but when I try to cast the script into a "DeftScript" it throws a ClassCastError even thought the script itself extends the class "DeftScript"
Error (the important part at least):
java.lang.ClassCastException: scripts.Compass cannot be cast to com.deft.core.scripts.DeftScript
at com.deft.core.scripts.DeftScriptManager.instantiate(DeftScriptManager.java:52) ~[?:?]
The error is caused by this
deftScript = (DeftScript)obj;
Compiling and Instantiating:
public static DeftScript instantiate(String java) {
File file = new File("./plugins/Deft-Core/scripts/" + java);
DeftScript deftScript = null;
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<JavaFileObject>();
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null);
List<String> optionList = new ArrayList<String>();
optionList.addAll(Arrays.asList("-classpath", System.getProperty("java.class.path") + ";./plugins/Deft-Core.jar"));
Iterable<? extends JavaFileObject> compilationUnit = fileManager.getJavaFileObjectsFromFiles(Arrays.asList(file));
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnostics, optionList, null, compilationUnit);
if (task.call()) {
Object obj = null;
try {
String jarFile = "./plugins/Deft-Core.jar";
URLClassLoader classLoader = new URLClassLoader (new URL[] {new File(jarFile).toURI().toURL(), new File("./plugins/Deft-Core/").toURI().toURL()}, Thread.currentThread().getContextClassLoader());
Class<?> loadedClass;
loadedClass = Class.forName("scripts.Compass", false, classLoader);
obj = loadedClass.newInstance();
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | MalformedURLException e) {
e.printStackTrace();
}
deftScript = (DeftScript)obj;
deftScript.onEnable();
} else {
for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) {
System.out.format("Error on line %d in %s%n", diagnostic.getLineNumber(), diagnostic.getSource().toUri());
}
}
return deftScript;
}
Calling the instantiate method:
String script = "Compass.java";
DeftScriptManager.instantiate(script);
DeftScript.java
package com.deft.core.scripts;
public abstract class DeftScript {
public abstract void onEnable();
}
Compass.java
package scripts;
import com.deft.core.scripts.DeftScript;
public class Compass extends DeftScript {
#Override
public void onEnable() {}
}
If your default classloader loads the class DeftScript and the .jar you are loading also contains the class DeftScript, then Java will think that these are two different classes with the same binary name but loaded by different classloaders and you will get that exception since Java sees you trying to mix the two different classes like they are the same thing.
The unique identification of a class in Java consists of the binary class name AND the classloader which was used to load the class.
If you create your URLClassloader like this :
URLClassLoader classLoader =
new URLClassLoader (new URL[] {new File(jarFile).toURI().toURL(),
new File("./plugins/Deft-Core/").toURI().toURL()},
Thread.currentThread().getContextClassLoader());
The second parameter tells java to use the current thread's classloader first to load classes, and only load them from the jar in your URLClassLoader if they are NOT defined in the parent.
Now the classloader will refer to it's parent first and the class DeftScript will only be loaded by the parent classloader even though your .jar file defines the same class (by name).
This was a pretty good article describing the way that this works :
http://www2.sys-con.com/ITSG/virtualcd/java/archives/0808/chaudhri/index.html
This was also helpful
http://www.javaworld.com/article/2077344/core-java/find-a-way-out-of-the-classloader-maze.html?page=1
Related
I am trying to load in text files on the fly and compile them.
File file = new File("Files/"+fileName+".java");
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
compiler.run(null, null, errStream, file.getAbsolutePath());
I then will load the compiled .class files later:
public Class loadStrategyClass(File strategyClassFile) throws IOException
{
FileChannel roChannel = new RandomAccessFile(strategyClassFile, "r").getChannel();
ByteBuffer buffer = roChannel.map(FileChannel.MapMode.READ_ONLY, 0, (int)roChannel.size());
return defineClass(strategyClassFile.getName(), buffer, (ProtectionDomain)null);
}
I am currently running into two issues:
The first is if the .java files I load in contain anonymous classes. It doesn't appear that the JavaCompiler class will compile these.
Exception in thread "main" java.lang.IllegalAccessException: Class Loader.ClassLoader can not access a member of class Files.myname.myclass$1 with modifiers ""
The second:
Is that sometimes I will get errors for NoClassDefFoundError:
Exception in thread "main" java.lang.NoClassDefFoundError: Files/myname/myclass
Despite the fact that other classes will load correctly and the .class file is in that path.
Apparently, your loadStrategyClass is defined within a custom ClassLoader. The problem is that it is not enough to call defineClass once for the class you’re interested in, your class loader must be able to resolve classes on demand, usually by implementing findClass, so the JVM can resolve dependencies, like the inner classes.
You didn’t specify, how you get the strategyClassFile argument for the loadStrategyClass method. Since you ran the compiler without any options, I suppose you simply looked up the file relative to the source file. To resolve other dependencies, the actual root of the class directory needs to be known. It becomes much easier when you define where to store the class files, e.g.
// customize these, if you want, null triggers default behavior
DiagnosticListener<JavaFileObject> diagnosticListener = null;
Locale locale = null;
JavaCompiler c = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fm
= c.getStandardFileManager(diagnosticListener, locale, Charset.defaultCharset());
// define where to store compiled class files - use a temporary directory
Path binaryDirectory = Files.createTempDirectory("compile-test");
fm.setLocation(StandardLocation.CLASS_OUTPUT,
Collections.singleton(binaryDirectory.toFile()));
JavaCompiler.CompilationTask task = c.getTask(null, fm,
diagnosticListener, Collections.emptySet(), Collections.emptySet(),
// to make this a stand-alone example, I use embedded source code
Collections.singleton(new SimpleJavaFileObject(
URI.create("string:///Class1.java"), Kind.SOURCE) {
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return "package test;\npublic class Class1 { public class Inner {} }";
}
}));
if(task.call()) try {
URLClassLoader cl = new URLClassLoader(new URL[]{ binaryDirectory.toUri().toURL() });
Class<?> loadedClass = cl.loadClass("test.Class1");
System.out.println("loaded "+loadedClass);
System.out.println("inner classes: "+Arrays.toString(loadedClass.getClasses()));
} catch(ClassNotFoundException ex) {
ex.printStackTrace();
}
In the example above, we know the root of the class directory, because we have defined it. This allows to simply use the existing URLClassLoader rather than implementing a new type of class loader. Of course, using a custom file manager, we also could use an in-memory storage for rather than a temporary directory.
You may use this API to discover what has been generated, which enables you to use the resulting class without knowing beforehand, which package or inner class declarations exist in the source file you’re going to compile.
public static Class<?> compile(
DiagnosticListener<JavaFileObject> diagnosticListener,
Locale locale, String sourceFile) throws IOException, ClassNotFoundException {
JavaCompiler c = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fm
= c.getStandardFileManager(diagnosticListener, locale, Charset.defaultCharset());
// define where to store compiled class files - use a temporary directory
Path binaryDirectory = Files.createTempDirectory("compile-test");
fm.setLocation(StandardLocation.CLASS_OUTPUT,
Collections.singleton(binaryDirectory.toFile()));
JavaCompiler.CompilationTask task = c.getTask(null, fm,
diagnosticListener, Collections.emptySet(), Collections.emptySet(),
fm.getJavaFileObjects(new File(sourceFile)));
if(task.call()) {
Class<?> clazz = null;
URLClassLoader cl = new URLClassLoader(new URL[]{binaryDirectory.toUri().toURL()});
for(JavaFileObject o: fm.list(
StandardLocation.CLASS_OUTPUT, "", Collections.singleton(Kind.CLASS), true)) {
String s = binaryDirectory.toUri().relativize(o.toUri()).toString();
s = s.substring(0, s.length()-6).replace('/', '.');
clazz = cl.loadClass(s);
while(clazz.getDeclaringClass() != null) clazz = clazz.getDeclaringClass();
if(Modifier.isPublic(clazz.getModifiers())) break;
}
if(clazz != null) return clazz;
throw new ClassNotFoundException(null,
new NoSuchElementException("no top level class generated"));
}
throw new ClassNotFoundException(null,
new NoSuchElementException("compilation failed"));
}
If you use this to dynamically bind plugins or modules, you may extend the search to look for a result class which implements a particular interface or has a certain annotation.
I've got a classloader problem with Java 9.
This code worked with previous Java versions:
private static void addNewURL(URL u) throws IOException {
final Class[] newParameters = new Class[]{URL.class};
URLClassLoader urlClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
Class newClass = URLClassLoader.class;
try {
Method method = newClass.getDeclaredMethod("addNewURL", newParameters );
method.setAccessible(true);
method.invoke(urlClassLoader, new Object[]{u});
} catch (Throwable t) {
throw new IOException("Error, could not add URL to system classloader");
}
}
From this thread I learned that this has to be replaced by something like this:
Class.forName(classpath, true, loader);
loader = URLClassLoader.newInstance(
new URL[]{u},
MyClass.class.getClassLoader()
MyClass is the class I'm trying to implement the Class.forName() method in.
u = file:/C:/Users/SomeUser/Projects/MyTool/plugins/myNodes/myOwn-nodes-1.6.jar
String classpath = URLClassLoader.getSystemResource("plugins/myNodes/myOwn-nodes-1.6.jar").toString();
For some reason - I really can't figure out, why - I get a ClassNotFoundException when running Class.forName(classpath, true, loader);
Does someone know what I'm doing wrong?
From the documentation of the Class.forName(String name, boolean initialize, ClassLoader loader) :-
throws ClassNotFoundException - if the class cannot be located by the specified class loader
Also, note the arguments used for the API includes the name of the class using which the classloader returns the object of the class.
Given the fully qualified name for a class or interface (in the same format returned by getName) this method attempts to locate, load, and link the class or interface.
In your sample code, this can be redressed to something like :
// Constructing a URL form the path to JAR
URL u = new URL("file:/C:/Users/SomeUser/Projects/MyTool/plugins/myNodes/myOwn-nodes-1.6.jar");
// Creating an instance of URLClassloader using the above URL and parent classloader
ClassLoader loader = URLClassLoader.newInstance(new URL[]{u}, MyClass.class.getClassLoader());
// Returns the class object
Class<?> yourMainClass = Class.forName("MainClassOfJar", true, loader);
where MainClassOfJar in the above code shall be replaced by the main class of the JAR myOwn-nodes-1.6.jar.
In my code I want to dynamically load Module class implementations from Jar files.
In my directory I have 3 files: A.jar, B.jar, C.jar
Each jar has one class called Main which extends Module class
A.jar code example:
public class Main extends Module {
private static String name = "A";
public Main() {
super(name);
}
}
(B and C files are the same but with "B" and "C" instead of "A" in the name property).
My Module class code is:
public abstract class Module{
private StringProperty nameProperty;
public Module(String name){
this.nameProperty = new SimpleStringProperty(name);
}
public StringProperty nameProperty(){
return nameProperty;
}
}
This is the code that I use to dynamically load the three classes:
for (File moduleFile : Data.modulesDir.listFiles()) {
try {
URL url = moduleFile.toURI().toURL();
Class[] parameters = new Class[] { URL.class };
URLClassLoader sysLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
Class<URLClassLoader> sysClass = URLClassLoader.class;
Method method = sysClass.getDeclaredMethod("addURL", parameters);
method.setAccessible(true);
method.invoke(sysLoader, new Object[] { url });
Constructor<?> cs = ClassLoader.getSystemClassLoader().loadClass("com.ehzlab.webreaper.module.Main")
.getConstructor();
Module instance = (Module) cs.newInstance();
System.out.println(instance.nameProperty.get());
} catch (Exception e) {
e.printStackTrace();
}
}
I expect this ouput:
A
B
C
but I get this instead:
A
A
A
It seems like that loads the same jar at each file list iteration. But debugging I noted that the URL changes every time.
I also tried inverting the order, for example, placing B.jar before the other jar, and the output is:
B
B
B
Why?
Simply because you are using same classloader each time, which doesn't reload underlying classes:
ClassLoader.getSystemClassLoader()...
In order to get access to specific classes, you have to use appropriate classloader used for loading particular jar file (may be it is sysLoader, not sure, as I didn't check):
Constructor<?> cs = sysLoader.loadClass("com.ehzlab.webreaper.module.Main")
.getConstructor();
Look at this question as well: How should I load Jars dynamically at runtime?
I am trying to use Java Compiler API to compile some java class. That class imports some packages from the jar files which can be loaded by context ClassLoader, let's call him X, which is NOT the system classloader. When I run the compilation, the compiler complains about not recognizing the imports. I have tried to specify the fileManager to pass the classloader, but it does not help.
When compile method is called, it first prints "CLASS LOADED", so the context ClassLoader CAN find the dependency class. However, the compilation itself fails (I get "Compilation FAILED" message) and during the compilation I get errors like this:
/path/to/my/Source.java:3: package my.dependency does not exist
import my.dependency.MyClass;
^
What am I doing wrong? What's the correct way to pass custom classloader to the compilationTask? I can't extract the URLs from the ClassLoader since it's not URLClassLoader.
My methods are here:
public void compile(List<File> filesToCompile) {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager stdFileManager =
compiler.getStandardFileManager(null, null, null);
Iterable<? extends JavaFileObject> fileObjects = stdFileManager
.getJavaFileObjectsFromFiles(filesToCompile);
FileManagerImpl fileManager = new FileManagerImpl(stdFileManager);
CompilationTask task = compiler.getTask(null, fileManager, null, null, null, fileObjects);
Boolean result = task.call();
if (result == true) {
System.out.println("Compilation has succeeded");
} else {
System.out.println("Compilation FAILED");
}
}
private final class FileManagerImpl extends ForwardingJavaFileManager<JavaFileManager> {
public FileManagerImpl(JavaFileManager fileManager) {
super(fileManager);
}
#Override
public ClassLoader getClassLoader(JavaFileManager.Location location) {
ClassLoader def = getContextClassLoader();
try {
def.loadClass("my.dependency.MyClass");
System.out.println("CLASS LOADED");
} catch (ClassNotFoundException ex) {
System.out.println("NOT LOADED");
}
return def;
}
}
The main point is that, while a class loader loads classes, javac will call JavaFileManager#list() to get a listing of all the files in a package.
So to use a custom class loader you need to modify (or extend) it to override JavaFileManager#list(). Hopefully you can reuse some of the logic used for class loading.
You might want to use your own implementations of JavaFileObject to represent class objects. You will then need to override JavaFileManager#inferBinaryName() (else the javac version will crash). Your implementations of JavaFileObject also needs to override (at least) JavaFileObject#openInputStream.
Here are some pointers: http://atamur.blogspot.be/2009/10/using-built-in-javacompiler-with-custom.html
Also, don't make your life harder than it should and extend ForwardingJavaFileManager and SimpleJavaFileObject.
For reference, here is an example implementation:
#Override public Iterable<JavaFileObject> list(Location location,
String packageName, Set<JavaFileObject.Kind> kinds, boolean recurse)
throws IOException
{
Iterable<JavaFileObject> stdResults =
fileManager.list(location, packageName, kinds, recurse);
if (location != StandardLocation.CLASS_PATH
|| !kinds.contains(JavaFileObject.Kind.CLASS))
{
return stdResults;
}
Set<JavaFileObject> additional = pkgObjects.get(packageName);
if (additional == null || additional.isEmpty()) {
return stdResults;
}
List<JavaFileObject> out = new ArrayList<>();
for (JavaFileObject obj : additional) {
out.add(obj);
}
for (JavaFileObject obj : stdResults) {
out.add(obj);
}
return out;
}
Where pkgObjects is a map from package names to JavaFileObject. The way you fill this map depends on how your class loader works.
This question has the answer. You'll have to set a classpath through an options list with the getTask() method (as described in detail in the accepted answer).
For loading the class from different jar file you could try with Reflection API it is easy way ..refer the following link http://download.oracle.com/javase/tutorial/reflect/index.html..
I know I'm missing something basic. I'm trying to dynamically compile a class and then reload it after a change. The following code works. However, when I call it twice (once after changing the .java), the class definition does not update. What am I missing?
File file = new File("/eraseme/Eraseme.java");
File[] files = new File[] {file};
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjects(files);
CompilationTask task = compiler.getTask(null, fileManager, null, null,null,compilationUnits);
boolean madeIt = task.call(); // works
File classFile = new File("/eraseme/Eraseme.class");
URL url = classFile.toURL();
URL[] urls = new URL[] { url };
ClassLoader cl = new URLClassLoader(urls);
Class cls = cl.loadClass("Eraseme");
TestApi test = (TestApi) cls.newInstance();
System.out.println(test.getVersion());
What you're missing is that the urls passed to URLClassLoader should be directories that are then searched. What's happening is that the URLClassLoader that you're constructing isn't actually finding the .class file, but is failing to find the class and is falling back to having the parent classloader load the class.
Specifically, try modifying the second bit of your code to be:
File classFile = new File("/eraseme");
URL url = classFile.toURL();
URL[] urls = new URL[] { url };
ClassLoader cl = new URLClassLoader(urls);
Class cls = cl.loadClass("Eraseme"); // will try to load /eraseme/Eraseme.class
TestApi test = (TestApi) cls.newInstance();
System.out.println(test.getVersion());
Note that if your class is called (with the package) com.mycompany.erasestuff.EraseMe, then the classloader will look in the file /eraseme/com/mycompany/erasestuff/EraseMe.class
Note that when you do this, you don't want the system classloader to be able to load the EraseMe class. The system classloader will take precedence, and your URLClassloader won't load anything. (The unfortunate thing is that when you say to a classloader "load this class", it first checks whether its parent classloader can load the class, then it tries to load it itself. If the system classloader can load this class, you'll never get a different version)