I am writing a Spring web application, which loads plugin files at runtime. These plugins are classes that implement IPlugin. However, when loading the plugin at runtime I get a ClassNotFoundException(IPlugin cannot be found).
The interface IPlugin is located in a package in my web app. In order to build a plugin I exported the interface to a jar file and included it in the plugin's build path.
In my web app, the plugin is loaded using a URLClassLoader:
URL fileUrl = jar.toURI().toURL();
String jarUrl = "jar: " + fileUrl + "!/";
URL[] urls = new URL[] {new URL(jarUrl)};
URLClassLoader loader = new URLClassLoader(urls);
IPlugin plugin = (IPlugin) Class.forName(clazz, true, loader).newInstance();
How do I make the interface available at runtime?
Edit:
It does work if I load the jar containing IPlugin.class together with the plugin. But is that really necessary?
I don't think you can use IPlugin without importing on the top
Simply bring the IPlugin.java to your code with the same package name, nothing wrong in this
You can directly use full file path to load a JAR via URLClassLoader
Try the following code
URL[] classLoaderUrls = new URL[]{new URL("file:///<your directory path>/<your filename>.jar")};
URLClassLoader urlClassLoader = new URLClassLoader(classLoaderUrls);
Class<?> beanClass = urlClassLoader.loadClass("com.my.MyImplementation");
Constructor<?> constructor = beanClass.getConstructor();
Object beanObj = constructor.newInstance();
Try to type cast, if you have IPlugin.java in your import on top this will work
IPlugin plugin = (IPlugin)beanObj;
Otherwise you need to use Reflection
Method method = beanClass.getMethod("myMethod");
method.invoke(beanObj);
I found the solution, I had to set the parent class loader like this: new URLClassLoader(URLs, getClass().getClassLoader()). It is then sufficient to have the IPlugin interface present in the webapp, no additional jar is needed in WEB-INF/lib.
Related
I have an annotation-processor java app that is looking into the target folder of the external maven project for the class files. This app uses URLClassLoader. The target folder is the absolute path app parameter. I have a simple junit test which is loading classes and processing the annotations successfully. Now I wrote the maven plugin that is just a wrapper for annotation-processor app. When running this maven plugin, annotation-processor loads the classes but it does not see any annotations in those. How can I access class/interface annotations from the maven plugin?
maven-plugin-plugin:3.6.4
java 11.0.15
private static Class<?> getClass(String path, String className, String packageName) {
File file = new File(path);
try {
URL[] urls = new URL[]{file.toURI().toURL()};
ClassLoader cl = new URLClassLoader(urls);
return cl.loadClass(packageName + "." + className));
} catch (MalformedURLException | ClassNotFoundException e) {
log.warn("Cannot find class: " + packageName + "." + className, e);
}
return null;
}
Example call:
Class clazz = getClass("/home/user/java/my-project/target/classes", "MyClass", "com.my.example.package");
assert clazz.getAnnotations() > 0;
There are two possible reasons:
your annotations do not have proper #Retention
JVM silently drops information about annotations while loading the class - that may happen if classloader does know nothing about annotation classes - that is very likely your case because you are instantiating new classloader without specifying parent
Basically our code allows people to pass in a custom implementation of our IRepository interface if they want to define their own repository for the files we generate. I keep getting the following error:
ClassCastException: S3RepositoryPlugin.S3Repository cannot be cast to package_name.IRepository
This is how I define the S3Repository:
public class S3Repository implements IRepository
And this is how im trying to insatiate it:
URL[] urls = { new URL("jar:file:" + assembly +"!/") };
URLClassLoader cl = URLClassLoader.newInstance(urls);
Class classToLoad = Class.forName(className, true, cl);
IRepository externalRepo = (IRepository) classToLoad.newInstance();
Where assembly is the full path to the jar, and className is "S3RepositoryPlugin.S3Repository".
Any idea as to why this is happening? From the stuff Im seeing online it seems casting to interfaces works differently than casting to classes but im not sure if the code im using is necessarily affected by that.
NOTE:
Using Tomcat9 and java8
I had to add the following code to the URLClassLoader to make it work:
URLClassLoader cl = URLClassLoader.newInstance(urls, getClass.getClassLoader());
My project structure is the following (very simplified of course):
So under lib-ext i download on a daily basis from a Jenkins server 2 jar files 'jar1 and jar2' to be checked by my program, i need one file from 'jar1' lets call it: "Class2Bloaded".
The issue is that this file implements an interface that is to be found in 'jar2', lets call this 'Dependency'
What i would like to do is, from my class under src "ClassThatLoads.java", load 'Class2Bloaded.class' and tell the class loader to look into 'jar2' to search for the implementing interface "Dependency.class"
My code so far (omitting exceptions handling):
//Create the URL pointing to Jar1
private URL getJarUrl(JarFile jarFile)
{
return new File(jarFile.getName()).toURI().toURL();
}
URL jar1Url = getJarUrl(jar1);
ClassLoader jar1classLoader = new URLClassLoader(new URL[] { jar1Url });
Class<?> Class2Bloaded = Class.forName(fullClassName, false, jar1classLoader );
So the problem happens within the Class.forName invocation, because the class i want to load implements an interface that is in jar 2.
Exception in thread "main" java.lang.NoClassDefFoundError: com/packagewithinJar2/Dependency
So eventually i have prepared another class loader that points to 'jar2', and i have even got the actual Interface i need:
URL jar2Url = getJarUrl(jar2);
ClassLoader jar2classLoader = new URLClassLoader(new URL[] { jar2Url });
Class<?> Interface2Bloaded = Class.forName(fullClassName, false, jar2classLoader );
Where 'fullClassName' in the second case is the fully qualified name of the interface from which 'Class2Bloaded' depends on.
Is just that i cant find anything in the javadocs of ClassLoader that allows me to 'inject' an additional class loader for the dependencies.
I hope my explanation is clear.
The first thing to do would be to add jar2 to the list of jars your URLClassLoader reads:
ClassLoader jarclassLoader = new URLClassLoader(new URL[] { jar1Url, jar2Url });
BUT the normal thing to do would be to add jar1 and jar2 on your classpath from the beginning.
To do so you would use the -cp parameter of the java executable.
for example, if you compile your classes into the bin directory:
java -cp libext/jar1.jar:libext/jar2.jar:bin ClassThatLoads
That way, you could use the classes seamless in your own java source and get rid of the cumbersome loading part :
public class ClassThatLoads {
public static void main(String[] args) {
Class2Bloaded stuff = new Class2Bloaded();
//use stuff from here...
}
}
So I have a classloader loading a class like so:
ClassLoader cl = new URLClassLoader(urls);
Class cls = cl.loadClass("modules.Test");
Method method = cls.getDeclaredMethod("getModule", noparams);
Class<?> type = method.getReturnType();
if(type.newInstance() instanceof Module){
System.out.println("Accessed field with type: Module");
}
The class Module is in another jar at runtime. And the Test.class was generated within that main jar then i unarchived it, so the dependency would be there.
How can I access other dependencies from the external .class file I have loaded?
The exception:
java.lang.NoClassDefFoundError: com/xxxxxxx/xxxx/objects/Module
Caused by: java.lang.ClassNotFoundException: com.xxxxxxx.xxxx.objects.Module
I think that the problem is happening because your modules.Test class depends the Modules class, but your custom class loader can't find that class.
I think that is because you have instantiated the custom classloader incorrectly. You wrote:
ClassLoader cl = new URLClassLoader(urls);
That creates a classloader whose parent classloader is the default system classloader. But the error implies that the default classloader is not the one that knows about Modules. Try this instead:
Classloader cl = new URLClassLoader(
urls, this.getClass().getClassLoader());
This should at least give you a classloader that knows about Modules.
Note: adding the URL for the JAR containing Modules to the urls array is a non-solution. You are liable to end up loading the Modules class twice, and that is liable to lead to other problems. (The instanceof won't work, for example.)
I'm asking because I'm totally not sure I've done the right thing. I'm using Eclipse for a web project. Let's call it WebProject (duh) in the package com.web.project.
I want WebProject to load JAR plugins at runtime, so I thought I could take advantage of java.util.ServiceLoader. So I created an interface com.web.project.WebProjectPlugin in the WebProject project with all the methods the plugins must implement.
Then I created the project PluginProject, adding WebProbject/build/classes in its Build path as a class folder:
package com.web.project.plugin;
import com.web.project.WebProjectPlugin;
public class TestPlugin implements WebProjectPlugin {
// Implementation of the interface methods...
}
Then I created a META-INF/services folder in the plugin project, put the text file com.web.project.WebProjectPlugin inside, containing the sole line "com.web.project.plugin.TestPlugin".
I exported the JAR file checking out the added build/classes folder and put it somewhere in the hard drive. When WebProject starts up, it does the following:
File[] jlist = pluginsDir.listFiles(new FileFilter() {
public boolean accept(File file) {
return file.getPath().toLowerCase().endsWith(".jar");
}
});
URL[] urls = new URL[jlist.length];
for (int i = 0; i < jlist.length; i++)
urls[i] = jlist[i].toURI().toURL();
URLClassLoader ucl = new URLClassLoader(urls);
ServiceLoader<WebProjectPlugin> srvl =
ServiceLoader.load(WebProjectPlugin.class, ucl);
Iterator<WebProjectPlugin> iter = srvl.iterator();
while (iter.hasNext()) {
WebProjectPlugin plugin = iter.next();
plugins.add(plugin);
}
pluginsDir is a File object pointing to the directory the JAR file is in. At first it seems that srvl does its job, since iter isn't empty, but then it throws the dreaded NoClassDefFoundError when it reaches iter.next().
I've already managed to create a plugin manager project to test ServiceLoader, and it runs just fine, but it's a plain console Java application, not a web project. So, what am I doing wrong here?
I'm a little puzzled: how can it not find the class definition for com.web.project.WebProjectPlugin, since it's in the same project that is running? Has that something to do with the URLClassLoader object I'm using?
This is the stack trace.
Try assigning the parent classloader to your URLClassLoader
URLClassLoader loader = new URLClassLoader(urls, Thread.currentThread().getContextClassLoader());
A WebProject expects a certain hierarchy of class loaders, so it might be that your classes are not visible to each other if the parent/child hieararchy is not set properly.