1

I am trying to load plugins from a certain jar file using the java ServiceLoader with an UrlClassLoader, but I just cannot seem to get it to find my plugin classes. Building both modules works, but whenever I run the code below, I get a java.util.NoSuchElementException, even though the file path is correct and I do not see where I messed up

I have to modules in my IntelliJ project, Test (The application loading the plugin and also providing the ServiceProvider) and PluginTest (The plugin to be loaded).

This is a screenshot of the structure of the Test module:

Test Module

And here is a screenshot of the structure of the PluginTest module:

PluginTest Module

Here is the ServiceProvider PluginProvider:

package net.lbflabs;

public abstract class PluginProvider {

    public abstract String getSecretMessage(int message);
}

This is the plugin that extends this class:

package net.lbflabs.plugins;

import net.lbflabs.PluginProvider;

public class PluginClass extends PluginProvider {
    @Override
    public String getSecretMessage(int message) {
        return "Here is my secret.";
    }
}

Here is the main class that loads the plugin:

package net.lbflabs;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ServiceLoader;

public class Main {

    private static String path = "C:/Users/jacke/IdeaProjects/Tests/out/artifacts/PluginTest_jar/PluginTest.jar";

    public static void main(String[] args) throws MalformedURLException {
        System.out.println("Initializing... \nPreparing to load JAR from Path " + path + "...");
        File file = new File(path);
        System.out.println(file.exists()); //Prints true, so file does exist
        URLClassLoader c = new URLClassLoader((new URL[]{file.getAbsoluteFile().toURI().toURL()}));
        ServiceLoader<PluginProvider> loader = ServiceLoader.load(PluginProvider.class, c);
        PluginProvider p = loader.iterator().next(); // Throws the java.util.NoSuchElementException
        System.out.println("Secret message in plugin: " + p.getSecretMessage(1));
    }
}

Here is the output when running the Tests module:

"C:\Program Files\Java\jdk-14.0.1\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2020.3\lib\idea_rt.jar=50330:C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2020.3\bin" -Dfile.encoding=UTF-8 -classpath C:\Users\jacke\IdeaProjects\Tests\out\production\Tests net.lbflabs.Main
Initializing... 
Preparing to load JAR from Path C:/Users/jacke/IdeaProjects/Tests/out/artifacts/PluginTest_jar/PluginTest.jar...
true
Exception in thread "main" java.util.NoSuchElementException
    at java.base/java.util.ServiceLoader$2.next(ServiceLoader.java:1310)
    at java.base/java.util.ServiceLoader$2.next(ServiceLoader.java:1298)
    at java.base/java.util.ServiceLoader$3.next(ServiceLoader.java:1396)
    at net.lbflabs.Main.main(Main.java:19)

Process finished with exit code 1

The service file in the PluginTest module I believe is correctly named (net.lbflabs.PluginProvider) and contains the valid class name (net.lbflabs.plugins.PluginClass). If it was incorrect, IntelliJ would probably find the issue since when I introduce a typo I get warnings.

If someone needs it, I can provide the whole project as .rar (please tell me how you want me to share it, e.g. I create a onedrive link)

PS: I have already asked this question here, but since I was working on a bigger project and hesitant to share all the code in it (and since the only answer I got did not work, probably due to not enough info from me), I wanted to repost the issue with a minimum workable example that throws the same exception, I hope that is fine.

Jakob Tinhofer
  • 313
  • 3
  • 16
  • The classloader doesn't contain the parent class only the implementing class, so it cannot find the parent class. So I suspect due to incomplete class-hierarchy it cannot load that class. So you should use the constructor which takes a parent class loader and set it to the current one. So that a proper class hierarchy can be determined. – M. Deinum Dec 21 '20 at 09:48
  • Would it not throw a different exception then? I had my classloader thow exceptions like NoClassDefFoundError on similar occations. You might still be right tho. How would I fix the issue? Could you post your fix as answer so I can potentially accept it? – Jakob Tinhofer Dec 21 '20 at 09:52
  • Wouldn't be the first that one exception is hidden by another and looking at the `LazyClassPathLookupIterator` (an internal class) it actually catches the `ClassNotFoundException` and simply returns `null`. Leading to the exception you get. – M. Deinum Dec 21 '20 at 09:57

1 Answers1

1

One thing I notice is that you construct a classloader which only contains 1 class. With this it won't be able to create a proper class hierarchy and most likely fail. Instead pass the current Classloader as the parent to your URLClassLoader.

ClassLoader parent = PluginProvider.class.getClassLoader();
URL[] urls = new URL[] { file.getAbsoluteFile().toURI().toURL()};
URLClassLoader c = new URLClassLoader(urls, parent);

Now a proper hierarchy should be able to be constructed.

Another thing is in your project structure your META-INF directory isn't under the src directory containing the sources for the project. Which means Intellij will leave them out of the jar. So you are basically adding a jar without the proper files. It contains only the class and not META-INF/services/net.lbflabs.PluginProvider.

Ellen Spertus
  • 6,576
  • 9
  • 50
  • 101
M. Deinum
  • 115,695
  • 22
  • 220
  • 224
  • Thanks. I did add the PluginProvider class loader to the urlclassloader, yet it still somehow does not work – Jakob Tinhofer Dec 21 '20 at 09:59
  • But do you still get the same exception or a different one? Or try `ClassLoader.getSystemClassLoader` instead. – M. Deinum Dec 21 '20 at 10:00
  • Weird thing, I cannot reproduce this. The exact sample, with either the system or the classloader from PluginProvider, does get the expected result. – M. Deinum Dec 21 '20 at 10:15
  • Weird. I tried with the system class loader, still the same exception. Maybe it is a IntelliJ noob mistake by me. I could share the project, even though I know this would mean quite a bit more effort for you – Jakob Tinhofer Dec 21 '20 at 10:18
  • 1
    The only difference is I created a maven project instead of a plain Java project. Does the jar actually contain the META-INF directory with the services dir and file? The `META-INF` needs to be in `src` else it won't be exported to the jar. Your jar doesn't contain the file and hence not the plugin. – M. Deinum Dec 21 '20 at 10:29
  • Oh ok. So I could use maven and there would be no real difference in the project structure? – Jakob Tinhofer Dec 21 '20 at 10:54
  • Converting the project to maven fixed all issues, thank you. Do you want to edit your answer with the solve in the comments so I can accept it? – Jakob Tinhofer Dec 21 '20 at 11:29
  • 1
    Well I suspect the real issue is that `META-INF` in your original post wasn't under `src` which means leaving out the whole structure and service loader file. When moving to maven you probably put the `META-INF` in `src/main/resources` instead. – M. Deinum Dec 21 '20 at 12:50
  • I'd like to accept an answer, but your answer hasn't solved the issue, the comment has. Could you edit it? – Jakob Tinhofer Dec 21 '20 at 12:54
  • Was just editing whilst you were commenting :). – M. Deinum Dec 21 '20 at 12:56
  • Oh ok sorry :D Thanks again for your help – Jakob Tinhofer Dec 21 '20 at 12:59