ServiceLoader.next causing a NoClassDefFoundError - java

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.

Related

Using SystemLoader / SpringFactoryLoader to load external Jar in Spring-Project

first: I'm really new to spring-boot and maven. So I still don't get how everything plugs together.
What I'm trying to achieve is some kind of plugin-feature for my application. From my research it seems the best way to do this is using ServiceLoader or the spring-boot implmentation of the SpringFactoriesLoader.
According to several instructions from the web I put two projects together
James (the main application) GitHub
TemperatureSensor (the plugin) GitHub
The JamesApplication provides an interfaces which is supposed to be implemented (de.maxrakete.james.device.domain.DeviceInterface).
The TemperatureSensor implements said class and exposes this in several ways.
For the ServiceLoader in in the file META-INF\services\de.maxrakete.james.device.domain.DeviceInterface with this content
de.maxrakete.james.plugin.TemperatureSensor.TemperatureSensor
For the SpringFactoriesLoader in the file META-INF\spring.factories with this content
de.maxrakete.james.device.domain.DeviceInterface=de.maxrakete.james.plugin.TemperatureSensor.TemperatureSensor
According to this page I tried two different implementations (see in the onApplicationEvent-function) in the MainApplication:
#SpringBootApplication
public class JamesApplication implements ApplicationListener<ApplicationReadyEvent> {
public static void main(String[] args) {
SpringApplication.run(JamesApplication.class, args);
ClassLoader cl = ClassLoader.getSystemClassLoader();
URL[] urls = ((URLClassLoader)cl).getURLs();
for(URL url: urls){
System.out.println("Classpath file: " + url.getFile());
}
}
#Override
public void onApplicationEvent(ApplicationReadyEvent event) {
ServiceLoader<DeviceInterface> loader = ServiceLoader.load(DeviceInterface.class);
loader.iterator();
List<DeviceInterface> foos = SpringFactoriesLoader.loadFactories(DeviceInterface.class, null);
}
}
I'm trying both ways to load the jar, but nothing is happening (I'm supposed to get some log-messages from the plugin) but this is not happening.
The way I'm running the application is like this:
java -cp "./plugins/TemperatureSensor-0.0.1-SNAPSHOT.jar" -jar james.war
As you see I'm trying to add the jar in the subfolder to the classpath, but in the ouput of the main-function (where I try to print all the files in the classpath) I only get Classpath file: /home/max/folder/james.war
Conclusion
So, there are three possible error-sources
Wrong cli command to add classpath files
Wrong declaration of interfaces in the META-INF folder
Wrong implementation of the Loader
Maybe I'm compiling the sources the wrong way?
Wrong configuration of the pom.xml
I really have no idea what the problem might be. I tried to provide you with as much information as possible and all the steps of my research. I hope someone finds some helpful clues, which I might have overlooked.
Thanks veryone!

Gradle Custom Plugins: Read Compiled Java Class

In a custom plugin (or task) I would like to read all compiled classes (preferrably those that have changed from last compilation) with a classloader so that I'll be able to use reflection on them.
Is that possible?
1) It would be great to have a cook right after a Java class was compiled so that I could read it, but I found no way to do this.
2) I'm thinking of something like this ...
compileJava.doLast {
ClassLoader parent = getClass().getClassLoader();
GroovyClassLoader loader = new GroovyClassLoader(parent);
// retrieve all class files
// for each class file, loader.parseClass(classFile)
}
In a gradle script getClass().getClassloader() will get you the classloader of the gradle script. This will NOT contain the compiled classes or compile/runtime jars. I think you want to do something similar to:
Collection<URL> urls = sourceSets.main.runtimeClasspath.files.collect { it.toURI().toURL() }
Classloader parent = new URLClassLoader(urls.toArray());
If you want to only act on the classes that have changed you are best to do do that in an incremental task

Load a class from a jar having dependency on another jar

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...
}
}

Spring webapp loading plugins: ClassNotFoundException

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.

How to reflect classes from JAR on windows

Assuming the following loader definition (the example is written in scala but it is almost the same as Java, thus you should easily understand the problem):
jar = new File("path/to/myjar.jar")
url = jar.toURI.toURL
urls = Array[URL](url) // just a single-element array containing the url
loader = new URLClassLoader(urls)
The JAR myjar.jar contains org/pack/Simple.class. The Simple has standard package org.pack and was compiled and packed into the JAR via JavaCompiler. The JAR is correct (executable etc.)
On linux, loader.loadClass("org.pack.Simple") returns the correct Class object.
On windows, it throws a ClassNotFoundException. Of course, I use \ instead of / as a file separator.
What do I do wrong? Or does simply windows suck? I installed OracleJDK8 and added its bin/ folder to the path.
EDIT1:
If I unpack JAR into some directory and let the ClassLoader to read from URL pointing at that directory, everything works. How is this possible?
I struggled long with this problem yesterday, but after a long Googling session I stumbled on the solution.
The URL object you specify must be created as follows:
URLClassLoader.addURL(new File(pathParam).toURI().toURL());
or in the constructor as follows:
new URLClassLoader(new URL[]{new File(pathParam).toURI().toURL()},this.getClass().getClassLoader());
Note that the constructor has the parent class-loader as a parameter.
for the loadClass() method. You need to ensure the path to the class is in binary format (as you are doing already).
For some reason, even if I create a URL object using the URL(String) constructor it won't work.
If you go and look at the java.lang.ClassLoader super-class, you will notice that it throws an exception by default. This is why the classloader is important.
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}

Categories

Resources