Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to use Application ClassLoader to load all plugins #497

Closed
karthic891 opened this issue Jun 9, 2022 · 13 comments
Closed

How to use Application ClassLoader to load all plugins #497

karthic891 opened this issue Jun 9, 2022 · 13 comments
Labels

Comments

@karthic891
Copy link

karthic891 commented Jun 9, 2022

Hi @decebals ,

I'm building a pf4j plugin with javassist and msgpack libraries. I'm building a fat jar out of the plugin. This fat jar contains javassist and msgpack library classes. Looks like there's some issue with class loading, probably during annotation processing (some classes are annotated with @message annotation which gets processed at runtime during serialization/deserialiation of our pojos)

I get the following error:

Caused by: javassist.CannotCompileException: by java.lang.ClassFormatError: org/msgpack/template/Template
        at javassist.util.proxy.DefineClassHelper.toClass(DefineClassHelper.java:274)
        at javassist.ClassPool.toClass(ClassPool.java:1232)
        at javassist.CtClass.toClass(CtClass.java:1384)
        at org.msgpack.template.builder.BuildContext.createClass(BuildContext.java:154)
        at org.msgpack.template.builder.BuildContext.build(BuildContext.java:68)
        ... 51 more
Caused by: java.lang.ClassFormatError: org/msgpack/template/Template
        at javassist.util.proxy.DefineClassHelper$Java7.defineClass(DefineClassHelper.java:182)
        at javassist.util.proxy.DefineClassHelper.toClass(DefineClassHelper.java:263)
        ... 55 more

When I move the javassist, msgpack and our pojo libraries out of the fat jar, place them in the classpath of the app (-cp) and when I call the app, I don't get the above error. When I run in this way, the library classes mentioned above are loaded by the Application ClassLoader (not the plugin/dependency class loader). I verified this through the TRACE logs.

[2022-06-09 15:33:21,696] TRACE Received request to load class 'my.pojo.Car' (org.pf4j.PluginClassLoader)
[2022-06-09 15:33:21,696] TRACE Couldn't find class 'my.pojo.Car' in PLUGIN classpath (org.pf4j.PluginClassLoader)
[2022-06-09 15:33:21,696] TRACE Search in dependencies for class 'my.pojo.Car' (org.pf4j.PluginClassLoader)
[2022-06-09 15:33:21,696] TRACE Couldn't find class 'my.pojo.Car in DEPENDENCIES classpath (org.pf4j.PluginClassLoader)
[2022-06-09 15:33:21,698] TRACE Found class 'my.pojo.Car' in APPLICATION classpath (org.pf4j.PluginClassLoader)

I'd like to try loading all the plugins with the Application ClassLoader so that I can build a fat jar for each plugin and not have separate dependencies in the classpath belonging to each of the plugins. I see that you have mentioned:

PF4J uses by default a separate class loader for each plugin but this doesn’t mean that you cannot use the same class loader (probably the application class loader) for all plugins. If your application requires this use case then what you must to do is to return the same class loader from PluginLoader.loadPlugin:

public interface PluginLoader {

boolean isApplicable(Path pluginPath);

ClassLoader loadPlugin(Path pluginPath, PluginDescriptor pluginDescriptor);

}
If you use DefaultPluginManager you can choose to override DefaultPluginManager.createPluginLoader and/or DefaultPluginLoader.createClassLoader.

This doesn't seem to be clear enough on how to pass host application's ClassLoader. Could you please provide a sample on how to define a DefaultPluginManager that loads plugins from a filesystem path and uses application's ClassLoader?

Here's the sample I use that didn't work (it doesn't use Application's class loader, hence fails in my case):

       Path path = Paths.get("/some/path");
       final PluginManager pluginManager = new DefaultPluginManager(path) {
           @Override
           protected PluginLoader createPluginLoader() {
               return new DefaultPluginLoader(this) {

                   //Tried with only this method, didn't work
                   @Override
                   public ClassLoader loadPlugin(Path pluginPath, PluginDescriptor pluginDescriptor) {
                       // return super.loadPlugin(pluginPath, pluginDescriptor);
                       return this.getClass().getClassLoader();
                   }

                   //Tried with only this method, didn't work
                   @Override
                   protected PluginClassLoader createPluginClassLoader(Path pluginPath, PluginDescriptor pluginDescriptor) {
                       PluginClassLoader pluginClassLoader = new PluginClassLoader(this.pluginManager, pluginDescriptor, this.getClass().getClassLoader());
                       pluginClassLoader.addFile(pluginPath.toFile());
                       return pluginClassLoader;
                   }
               };
           };
       };

Appreciate your library and the documentation. Thank you.

@decebals
Copy link
Member

The part with the same ClassLoader for both application and plugins is the ultimate solution (I don't recommend a such solution).
In such situation you need for your application a custom ClassLoader that allows you to load jars dynamically. You can see an example in https://github.com/decebals/gogo.
Back to initial problem, first of all, it's important to know why javassist and msgpack libraries don't work in your case. Probably you have a possibility to pass a ClassLoader in that libraries.
Please take a look on #93 (comment), maybe the comments from that issue are valuable for you.

@decebals
Copy link
Member

@karthic891 Any news on this topic? Can I help you with anything else related to this topic? If your response is no, I want to close this issue.

@pdharmendra
Copy link

Hi @decebals
I also have a similar requirement where I want my plugins to be loaded by the Application class loader, as I do not want different plugins to load the common libraries again and again maybe to reduce performance overhead.
I see it can also be implemented by having the correct plugin dependencies but would like to explore about the other option as well.

@decebals
Copy link
Member

decebals commented Nov 7, 2022

I played a little bit with this idea and my conclusion is that with the current state of the pf4j, it's not easy to achieve this goal.
Started with Java 9, the AppClassLoader is not an implementation on URLClassLoader, and the things get complicated.
On the other side, the effort is not justified, it's difficult to have plugins that uses the single version of a shared library, and finally you will have conflicts if the ecosystem of the application grows. If you are a one man show on project, maybe with discipline you can use one class loader for application and plugins.

@decebals
Copy link
Member

decebals commented Nov 7, 2022

However, if you want to try, the idea is to set the SystemClassLoader with a custom implementation using:

-Djava.system.class.loader=org.pf4j.util.UrlClassLoader

where UrlClassLoader looks like:

public class UrlClassLoader extends URLClassLoader {

    static {
        registerAsParallelCapable();
    }

    /*
     * Required when this classloader is used as the system classloader.
     */
    public UrlClassLoader(ClassLoader parent) {
        super(new URL[0], parent);
    }

    @Override
    public void addURL(URL url) {
        super.addURL(url);
    }

    public void addFile(File file) throws IOException {
        addURL(file.getCanonicalFile().toURI().toURL());
    }

    /*
     *  Required for Java Agents when this classloader is used as the system classloader.
     */
    @SuppressWarnings("unused")
    private void appendToClassPathForInstrumentation(String jarPath) throws IOException {
        addURL(Paths.get(jarPath).toRealPath().toUri().toURL());
    }

}

The PluginClassLoader will extends this custom class loader.

After this, you need to set this custom class loader in PluginLoader implementation:

@Override
protected UrlClassLoader createPluginClassLoader(Path pluginPath, PluginDescriptor pluginDescriptor) {
    return (UrlClassLoader) ClassLoader.getSystemClassLoader();
}

That's about all I had to say on this subject. If you get something notable, create a PR and we will discuss it.

@milgner
Copy link
Contributor

milgner commented Mar 2, 2023

I came across this issue from a different context: using Hibernate with JPA annotations in my plugins fails since getDeclaredFields cannot execute reflection with fields of types from other plugins. See https://hibernate.atlassian.net/jira/software/c/projects/HCANN/issues/HCANN-128 for details.

I wonder whether using the SystemClassLoader approach would be viable for that? Another idea would be to have a PluginClassLoader which cascades to classloaders of directly required plugins if it doesn't find the class on its own - but I don't know what implications or effort this would have either.

@decebals
Copy link
Member

decebals commented Mar 2, 2023

@milgner With your use case, we are between two worlds, the Hibernate guys don't know about PF4J and I don't know (too much) about Hibernate 😄.
Maybe #93 (comment) can help here.
I see that someone is assigned on https://hibernate.atlassian.net/jira/software/c/projects/HCANN/issues/HCANN-128, let's see what conclusion he reaches, maybe with some changes in Hibernate the problem can be solved. If we can do something in PF4J we will do it, but at the moment I don't know what we could do.

@milgner
Copy link
Contributor

milgner commented Mar 2, 2023

Well, I certainly wouldn't want to suggest making Hibernate-specific changes to PF4J. The heart of the matter is Class::getDeclaredFields which croaks when invoked on a class from plugin A with one field having a type from (required) plugin B.

In Hibernate, I hope that it could be solved by getting it to use the class loading service which knows about all plugins and their classes but when I read about the SystemClassLoader here I was wondering whether this would be a viable approach, too.

@decebals
Copy link
Member

decebals commented Mar 2, 2023

In Hibernate, I hope that it could be solved by getting it to use the class loading service which knows about all plugins and their classes

I think so too

but when I read about the SystemClassLoader here I was wondering whether this would be a viable approach, too

As I mentioned somehow in #497 (comment), for me this approach with one ClassLoader for application and plugins seems a step backwards. It comes as a solution to a problem but your application loses the power provided by PF4J. Maybe in this situation you don't need PF4J and a custom solution based on a dynamic class loader and ServiceLoader is good enough. Maybe in the future someone will fully implement the solution based on SystemClassLoader described by me an a previous comment, to have a reference.

On the other hand, I think that other developers also use the PF4J with Hibernate, and I was curious to see how they do it.
Such of discussions (also related to Spring ecosystem) are very welcome on https://github.com/pf4j/pf4j/discussions.

@decebals
Copy link
Member

@milgner
What is the status of this issue? Did you solve the problem?

@milgner
Copy link
Contributor

milgner commented Jul 25, 2023

@milgner What is the status of this issue? Did you solve the problem?

I solved it, but only for the specific use case of Hibernate. After collecting all the plugin classloaders and passing them in to Hibernate as base classloaders, all entity classes from my plugins can be loaded.

See hibernate/hibernate-reactive#1526 (comment) for a code snippet.

The original issue remains unaffected by this, though. Sorry for jumping on this but when I originally found this issue, it seemed somewhat applicable to my use case.

@decebals
Copy link
Member

@karthic891 Can we close it? Do you need more help?

@karthic891
Copy link
Author

@decebals Yes, we can close this. Thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants