Search code examples
javaclassloaderjavacompiler

ClassCastException when compiling, loading and using inherited class at runtime


I am trying to compile, load and use an inherited class at runtime. Here is the structure of my Java/Maven project. Note there are no .java files under com.mycompany.model.inherited. This is where generated class files will be placed.

src/main/java
  com.mycompany.model.base
    BaseClass.java

src/test/java
  com.mycompany.model.inherited

BaseClass.java

package com.mycompany.model.base;

public abstract class BaseClass {
    public abstract void doWork();
}

At runtime I write the contents of an inherited class to src/test/java/com/mycompany/inherited/SubClass.java

package com.mycompany.model.inherited;

import com.mycompany.model.BaseClass;

public class SubClass extends BaseClass {

    @Override
    public void doWork() {
        System.out.println("SubClass is doing work.");
    }
}

And then I execute this code to compile SubClass.java, load it, create an object instance. All of that works fine. But when I try to cast it to a BaseClass I'm getting a ClassCastException at runtime. Here's the full code.

// prepare the class file contents and write it to disk.
String subClassContents =
    "package com.mycompany.model.inherited;\n" +
    "\n" +
    "import com.mycompany.model.base.BaseClass;\n" +
    "\n" +
    "public class SubClass extends BaseClass {\n" +
    "\n" +
    "    @Override\n" +
    "    public void doWork() {\n" +
    "        System.out.println(\"SubClass is doing work.\");\n" +
    "    }\n" +
    "}\n";
File file = new File(System.getProperty("user.dir") + "\\src\\test\\java\\com\\mycompany\\model\\inherited\\SubClass.java");
FileWriter writer = new FileWriter(file);
writer.write(subClassContents);
writer.close();

// compile the class using the current classpath.
File[] files = new File[]{file};
List<String> options = new ArrayList<>();
options.addAll(Arrays.asList("-classpath", System.getProperty("java.class.path")));
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager filemanager = compiler.getStandardFileManager(null, null, null);
Iterable fileObjects = filemanager.getJavaFileObjects(files);
JavaCompiler.CompilationTask task = compiler.getTask(null, null, null, options, null, fileObjects);
task.call();

// load the class using the current classpath.
String[] paths = System.getProperty("java.class.path").split(";");
List<URL> urls = new ArrayList<>();
urls.add(new File("src/test/java").toURI().toURL());
for (String p : paths) {
    urls.add(new File(p).toURI().toURL());
}
URLClassLoader classLoader = URLClassLoader.newInstance(urls.stream().toArray(URL[]::new));
Class<?> cls = Class.forName("com.mycompany.model.inherited.SubClass", true, classLoader);

// use the class.
BaseClass base = (BaseClass) cls.newInstance();
base.doWork();
java.lang.ClassCastException: com.mycompany.model.inherited.SubClass cannot be cast to com.mycompany.model.base.BaseClass

If I include src/test/java/com/mycompany/model/inherited/SubClass.java at compile time it works fine. I.e:

BaseClass base = (BaseClass) SubClass.class.getClassLoader().loadClass("com.mycompany.model.inherited.SubClass").newInstance();
base.doWork();

Hoping someone can point to the issue and help me solve it. Thanks!


Solution

  • The cast failed because the base class of SubClass is actually considered a different class from the "BaseClass" that you wrote in your source code. They are considered different because they are loaded by different class loaders. The base class of SubClass is loaded by the URLClassLoader you created, whereas the BaseClass you wrote in the source code is presumably loaded by the system class loader.

    To fix this, you can set the parent class loader of the URLClassLoader to the same class loader as the one that loaded BaseClass, and only give src/test/java as the class path. This way, when the URLClassLoader loads SubClass, it can't find BaseClass in src/test/java and so would use the parent class loader to load it.

    URLClassLoader classLoader = URLClassLoader.newInstance(
        new URL[] { new File("src/test/java").toURI().toURL() }, 
        BaseClass.class.getClassLoader()
    );