简体   繁体   中英

Java ClassLoaders - Cast class to an interface

I'm trying to develop a java application in which users can add functionality (through plugins) which must implement a common interface: PluginFunction. Then, the custom.class files must be in a specified directory (in this case, ./plugins) and are loaded by my custom classloader.

While tesing from the IDE everything works fine, but when I export the app to a jar file the following exception is thrown:

java.lang.ClassCastException: class operaciones.Suma cannot be cast to class operaciones.PluginOperacion (operaciones.Suma is in unnamed module of loader logica.PluginClassLoader @daf4db4; operaciones.PluginOperacion is in unnamed module of loader java.net.URLClassLoader @6576fe71)
        at logica.OperacionesManager.loadOperaciones(OperacionesManager.java:75)
        at gui.commands.RefreshCommand.execute(RefreshCommand.java:28)
        at gui.GUI_CalcSimple.<init>(GUI_CalcSimple.java:124)
        at gui.GUI_CalcSimple$1.run(GUI_CalcSimple.java:60)

I've been doing some research and I found out the problem occurs because the interface is loaded by a different classloader than my custom classloader, but I don't know how to fix this.

Thanks

NB: Because java represents all type kinds (classes, interfaces, enums, etc) java.lang.Class I'll use the term 'class' for the rest of this answer, but that encompasses interfaces and enums and such as well.

Usually, we java devs say that a type (class, interface, enum, etc) is only ever loaded once by the system (as in, there is just one instance of java.lang.Class ).

This is true.

However, 'a class', that definition needs some fudging: A class is defined not just by its fully qualified name ( operaciones.Suma ). It's actually defined by the combination of its name and its loader.

Now, in the normal situation, you have only one relevant loader present in a given VM: it loaded the class that had your main method and will load everything that executing this method ends up needing, and it looks at the classpath to do its job.

Important: Compatibility of instances

If you somehow end up with 2 separate loaded classes both named, say, java.lang.Integer , by using 2 class loaders that each individually loaded that class, then these types are completely incompatible. You get crazy errors, like "Instance of type java.lang.Integer cannot be assigned to variable of type java.lang.Integer".

Important: The notion of boundary types

In your module system, there are 3 worlds. There's the stuff you do, internally, in your main app. The plugin has no business even knowing about any of this. It's stuff marked private and the like.

In the plugin system, that too has private components.

But, there's a third world: What's in between the two. Presumably, you are going to end up having a string generated in your main app code, and you hand it over to the plugin. That means java.lang.String is a boundary type. The plugin has operaciones.PluginOperacion as a thing it needs (it implements it, after all,). but so does your main code, That, too. is a boundary type.

Crucial point: All boundary types must be loaded by one class loader (both for the plugin code and the main app), because otherwise you can't use them as boundary type.

Thus, the explanation of what you are observing is simply: The plugin ended up loading a boundary type in its own loader, instead of in loader of your main class, and that's what you need to fix.

Important: "What is the loader?"

What does "the loader that loaded this class" mean? It is simple: ClassLoaders, in the very end, call the native defineClass method on themselves, passing a byte array of bytecode. THAT makes the instance you call the defineClass method on "the loader". Whenever that class ends up needing another class to do its job, it will immediately ask its class loader to do so. This is true even for something as simple as java.lang.String .

Important: ClassLoaders have parentage

The ClassLoader API is designed to be fairly flexible, but its most basic intended usage is as follows:

  1. ClassLoaders have parent loaders.
  2. To load any resource, the CL will first ask its parents to load it . They call defineClass , making the class it loaded set up such that your parent is the loader, not your custom loader.
  3. Only if the parent(s) can't get it done, will the CL do it itself. You end up calling defineClass , you are the loader now. Any types needed by what you loaded go from #1, and will again result in 'ask parent first, only if it cannot, we load it'.

You don't have to, you can choose not to ask your parents and always load it itself. There are reasons to do so sometimes.

The right design

So, the trick is, to use the parentage system to ensure that the boundary types are only ever loaded once. Possibly by the one classloader you set up for your main app, but that's probably overengineering it: Your main app and ALL the boundary classes should be loaded by the java standard loader (that loaded the class with your main method), and the plugin itself and any types that it defines are loaded by the plugin's loader.

Because of parentage, this works out: The plugin's loader has a parent (the main loader). You ask your custom loader to load the plugin. It asks its parent (main loader) first, but that can't find it because this plugin is not in your classpath. Thus, pluginloader loads it. The very moment pluginloader has done this, pluginloader is immediately asked to then load operaciones.PluginOperacion because the plugin class you are loading extends/implements that.

Following the standard intent of the API, your pluginloader will ask its parent to load it, and... that should succeed , and thus the jlClass instance your pluginloader returns is nevertheless loaded by mainloader. As in, calling getClassLoader() on that will return the same thing YourMainApp.class.getClassLoader() returns.

Great? How?

class PluginLoader extends ClassLoader {
    public PluginLoader(ClassLoader parent) {
        super(parent);
    }

    public Class<?> findClass(String name) {
        String tgt = name.replace(".", "/") + ".class";
        byte[] bytecode = readFullyFromPluginjar(tgt);
        return defineClass(name, bytecode, 0, bytecode.length);
    }
}

That's all you have to do. Simple, once you grok how it works.

But, note that java itself is going to invoke loadClass and not findClass . Fortunately, the impl of loadClass that you inherit will ask parent first, and only if parent fails to find, will it invoke findClass (which will end up running the overridden code above). Thus, if you want to write a classloader that doesn't fit the standard intent of asking parent first, you override loadClass instead. But, the standard intent is usually what you want, so, usually, override findClass, not loadClass.

To use:

class Main {
    public PluginOperaciones loadPlugin(Path jarLocation, String className) {
        PluginLoader loader = new PluginLoader(Main.class.getClassLoader());
        loader.setJarSearchSpace(jarLocation);
        Class<?> pl = loader.loadClass(className); // load, not find!!
        return (PluginOperaciones) pl.getConstructor().newInstance();
    }
}

Good luck with the rest of the project!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM