0

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() {}
}
Unihedron
  • 10,902
  • 13
  • 62
  • 72
Kuliu
  • 171
  • 2
  • 12
  • 1
    possible duplicate of [NoClassDefFoundError ClassLoader](http://stackoverflow.com/questions/31282240/noclassdeffounderror-classloader) – Unihedron Jul 09 '15 at 05:35

1 Answers1

2

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

Cobusve
  • 1,572
  • 10
  • 23
  • Sadly this hasnt fixed my problem. I still have the same error. It hasnt hurt anything so ill leave the change. – Kuliu Jul 08 '15 at 05:04
  • Can you set a breakpoint on that line and note the value of obj.getClass().getName() and also the same on the .getClass().getClassLoader(). Inspecting these should give you a clue where the classes are being loaded from. Another common problem may be that you have more than one .jar in the classpath which defines the class. You should remove the Deft-Core.jar from your URLClassLoader, and add it as a dependency to your application. This way anything in Deft-Core.jar will be picked up by the system classloader for the application, but the plugin scripts will be loaded by the URL classloader – Cobusve Jul 08 '15 at 05:29
  • Either way, by inspecting in the debugger the class name and classloader for both sides of your cast you should be able to see where the difference is – Cobusve Jul 08 '15 at 05:30
  • See ive failed to mention its not an application. Its a plugin for a **Minecraft Spigot** server. The server it self will run my jar as a plugin. So its not possible for me too add a breakpoint. – Kuliu Jul 08 '15 at 05:52
  • Can you print it to the console with System.out.println() ? The key is that DeftScript you are assigning to is not the same class as DeftScript your script is inheriting from. If you can print out where each is loaded from that will help you a lot. – Cobusve Jul 08 '15 at 07:21
  • Yes i can print to the console. What do you want me the print out? – Kuliu Jul 08 '15 at 07:26
  • Try printing these : 1) DeftScript.class.getProtectionDomain().getCodeSource().getLocation() and 2) Compass.class.getSuperclass().getProtectionDomain().getCodeSource().getLocation() - these two MUST be the same for your cast to work. – Cobusve Jul 08 '15 at 07:27
  • If the files are identical the classLoaders must be different. – Cobusve Jul 08 '15 at 07:28
  • Now i see the problem – Kuliu Jul 08 '15 at 07:30
  • '[03:29:45 INFO]: file:/C:/Users/Kuliu/Desktop/Deft-Server/plugins/Deft-Core.jar [03:29:45 INFO]: file:/C:/Users/Kuliu/Desktop/Deft-Server/./plugins/Deft-Core/' – Kuliu Jul 08 '15 at 07:30
  • Erm its hard too see but they are in fact different. – Kuliu Jul 08 '15 at 07:31
  • So my question for you would be how would I change the location of `obj.getClass().getProtectionDomain().getCodeSource().getLocation()` – Kuliu Jul 08 '15 at 07:36
  • Depends on what you need and how you interact with the rest of the system. If your plugin will always function alone you can do a "parent last classloader" Quick google gives one example http://jeewanthad.blogspot.com/2014/02/how-to-solve-java-classpath-hell-with.html not sure if it is good. – Cobusve Jul 12 '15 at 22:20