pf4j
pf4j copied to clipboard
How to use Application ClassLoader to load all plugins
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.
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 https://github.com/pf4j/pf4j/issues/93#issuecomment-188526528, maybe the comments from that issue are valuable for you.
@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.
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.
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.
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.
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.
@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 :smile:. Maybe https://github.com/pf4j/pf4j/issues/93#issuecomment-188526528 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.
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.
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 https://github.com/pf4j/pf4j/issues/497#issuecomment-1306094263, 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.
@milgner What is the status of this issue? Did you solve the problem?
@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 https://github.com/hibernate/hibernate-reactive/issues/1526#issuecomment-1625247830 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.