3

I currently develop an open-source project where people may add their own .jar to extend the included features. However, I'm stuck with how to load the classes in the jar.

I have an abstract class which has an abstract method onEnable() and some getter that provides some objects to work with the application. The plugin needs the subclass my plugin-class BasePlugin. The jar should be added to /plugins and thus I want all *.jar files in the /plugins folder to be loaded when the application starts.

The problem I'm running to now is that, of all the approaches I found, I need to declare a classpath of the classes in the jar file, which I do not know. Neither do I know the name of the jar file. Thus, I need to scan the /plugins folder for any *.jar file and load the corresponding class inside the jar which implements BasePlugin and invoke the onEnable() method.

Nordic88
  • 129
  • 1
  • 12
  • 1
    I used a configuration file which provided the "entry point" into the plugin, placed within a "known"/"common" location within the Jar. The class loader used to load the Jar could then find and load this file, which would provide the name of the class which is the "entry point" for it – MadProgrammer Apr 02 '19 at 01:27
  • @MadProgrammer so basically one can add the name of the jar e.g. `MyPluginExtension` to the configuration file and my class looks for the a class called like this and then trys to load and invoke it? – Nordic88 Apr 02 '19 at 01:29
  • You could look into [`ServiceLoader`](https://docs.oracle.com/en/java/javase/12/docs/api/java.base/java/util/ServiceLoader.html). – Slaw Apr 02 '19 at 01:31
  • 1
    No, you would create a "plugin" configuration file, which would be stored within the Jar as an embedded resource. All the "plugins" would have a file named the same, in the same location, which could then be read by the class loader. Within this file, it would contain the name of the class that conforms to the "base" class/interface requirements which the class loader would then load – MadProgrammer Apr 02 '19 at 01:36
  • @MadProgrammer I'd still run into the issue, that I don't know the name of the jar to load. Best would be, if there would be an approach to scan the /plugins folder for jars and load them. However, scanning the folder and then accessing the configuration file would be neat. Still, I do not know how to get all jars and look for that file... – Nordic88 Apr 02 '19 at 01:41
  • 1
    @Nordic88 You don't need to. You simply list all the Jars in, let say, the "plugins" directory, use one or more class loaders to load them and then find the configuration Jar – MadProgrammer Apr 02 '19 at 01:43
  • @MadProgrammer Sounds like a good approach, however I have no clue how to properly do it. See my edited post for my current approach. – Nordic88 Apr 02 '19 at 01:56
  • 1
    @Nordic88 The basic idea involves using a `URLClassloader`, for [example](https://stackoverflow.com/questions/28470081/how-to-load-my-jar-to-classpath-at-runtime-through-java-coding/28470194#28470194) and [example](https://stackoverflow.com/questions/12520395/how-to-run-a-jar-file-from-another-jar/12520470#12520470), then you can use `findResource` to find the "named" configuration, [for example](https://stackoverflow.com/questions/14408685/how-to-access-a-resource-configuration-text-file-in-an-external-jar-from-jav/14409210#14409210) (although `findResources` will return multiple matches) – MadProgrammer Apr 02 '19 at 03:16

4 Answers4

3

SpigotMC uses JAR files as plugins as well, inside the jar is a plugin.yaml file that stores extra information about the plugin including the classpath. You don't need to use a YAML file, instead you could use something like JSON or even a plain text file.

The YAML file is inside the jar and can be accessed by using some of the methods explained here. You can then get the classpath property and then load the jar using the methods explained here. Extra information can be stored about the plugin such as the name, version, and dependencies.

minebolt
  • 60
  • 1
  • 6
3

The basic idea is too...

  1. Read all the files in a specific directory
  2. Convert the File reference to a URL for each result
  3. Use a URLClassLoader, seeded with the URL results to load each result
  4. Use URLClassLoader#findResources to find all the match resources with a specific name
  5. Iterate over the matching resources and load each one, which should give, at least, the "entry point" class name
  6. Load the class specified by the "entry point" property

For example...

public List<PluginClass> loadPlugins() throws MalformedURLException, IOException, ClassNotFoundException {
    File plugins[] = new File("./Plugins").listFiles(new FileFilter() {
        @Override
        public boolean accept(File file) {
            return file.getName().endsWith(".jar");
        }
    });
    List<URL> plugInURLs = new ArrayList<>(plugins.length);
    for (File plugin : plugins) {
        plugInURLs.add(plugin.toURI().toURL());
    }
    URLClassLoader loader = new URLClassLoader(plugInURLs.toArray(new URL[0]));
    Enumeration<URL> resources = loader.findResources("/META-INFO/Plugin.properties");
    List<PluginClass> classes = new ArrayList<>(plugInURLs.size());
    while (resources.hasMoreElements()) {
        URL resource = resources.nextElement();
        Properties properties = new Properties();
        try (InputStream is = resource.openStream()) {
            properties.load(is);
            String className = properties.getProperty("enrty-point");
            PluginClass pluginClass = loader.loadClass(className);
            classes.add(pluginClass);
        }
    }
    return classes
}

nb: I've not run this, but this is the "basic

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
1

Java already has a class for this: ServiceLoader.

The ServiceLoader class was introduced with Java 6, but the “SPI jar” concept is actually as old as Java 1.3. The idea is that each jar contains a short text file that describes its implementations of a particular service provider interface.

For instance, if a .jar file contains two subclasses of BasePlugin named FooPlugin and BarPlugin, the .jar file would also contain the following entry:

META-INF/services/com.example.BasePlugin

And that jar entry would be a text file, containing the following lines:

com.myplugins.FooPlugin
com.myplugins.BarPlugin

Your project would scan for the plugins by creating a ClassLoader that reads from the plugins directory:

Collection<URL> urlList = new ArrayList<>();

Path pluginsDir = Paths.get(
    System.getProperty("user.home"), "plugins");

try (DirectoryStream<Path> jars =
    Files.newDirectoryStream(pluginsDir, "*.jar")) {

    for (Path jar : jars) {
        urlList.add(jar.toUri().toURL());
    }
}

URL[] urls = urlList.toArray(new URL[0]);
ClassLoader pluginClassLoader = new URLClassLoader(urls,
    BasePlugin.class.getClassLoader());

ServiceLoader<BasePlugin> plugins =
    ServiceLoader.load(BasePlugin.class, pluginClassLoader);

for (BasePlugin plugin : plugins) {
    plugin.onEnable();
    // etc.
}

An additional advantage of using ServiceLoader is that your code will be capable of working with modules, a more complete form of code encapsulation introduced with Java 9 which offers increased security.

VGR
  • 40,506
  • 4
  • 48
  • 63
0

There is an example here it may be helpful. Also, you should take a look at OSGi.

Mohammad Ahmad
  • 192
  • 2
  • 12