Making a Dynamic Plugin System

You have a project. You need plugins for that project. Plugin, by this definition, is a module loaded which adds functionality to your program.

Lucky, for you, making a plugin system is easy. There are only a few steps you have to follow.

  1. Make the loader
  2. Make the plugin environment
  3. Make the abstract plugin environment
  4. Make the plugin
  5. Load the plugin

Making a plugin is a pretty lengthy job, but that is only because the amounts of clicks and file exploring involved.

Okay let’s get into the meat.

  1. Make the loader

The theory of this plugin system goes: you include a jar, which finds a plugin class using a config file. A new instance, utilizing this config file, of this class is then generated and its functionality is used accordingly. Utilizing a configuration file will be necessary. It allows up to point to the Plugin class in the cluster, that is the jar. It will also provide additional information, such as plugin name, version, and other metadata.

final File plugin = new File(location);
final JarFile jf = new JarFile(plugin);
final HashMap<String, String> settings = new HashMap<>();

So the first step in loading is to get the plugin jar file to load. ‘location’ points to your jar file. ‘config’ refers to the name of the file, since it will be in the root of said jar. If it isn’t in the root, it needs to be pathed accordingly. You create a JarFile from that file, as we are going to get a resource from the jar. This requires that we treat the file as a jar file and get Entries out of the file. We will be getting only the config file, as loading the Plugin class takes a different route. You can alternatively skip the JarFile step in a modded version, given that the config file is outside the jar. It is much neater to include the config file into the jar file, but it is not necessary.

final BufferedReader br = new BufferedReader(new InputStreamReader(jf.getInputStream(jf.getJarEntry(config))));
String in;
while((in = br.readLine()) != null) {
	if(in.isEmpty() || in.startsWith("#"))
		continue;
	final String[] split = in.split(": ", 2);
	settings.put(split[0], split[1]);
}
br.close();
jf.close();

The next step is to open the config file and read it in; using a basic BufferedReader this is possible. The reason I use BufferedReader is I wanted to read the config file per line, it being a text file of course. You will need to access the jar file by using jarfile.getInputStream() which will open a stream to a zip entry, which jf.getJarEntry’s return is an extension of ZipEntry named JarEntry. Then you just load that into a reader for the buffered reader to use.

To read, you just read by line. I chose to delimit keys from values by a ": ". Just to make sure its safe, I put a 2. That way entries like “Example: whatever: is this” is a valid key-pair. The key is “Example” and the value is “whatever: is this”.

final Class<?> clazz = Class.forName(settings.get("Main"), true, new URLClassLoader(new URL[]{plugin.toURI().toURL()}));
final Plugin instance = (Plugin) clazz.newInstance();
instance.setConfig(settings);
return instance;

The final step to loading is simple. We will get a class. Class, by nature, is generic and needs a type reference. We can set this to ?. Using Class.forName, you can gain access to a specific resource. You must pass true, because we need the initialized state, but we also need to use the URLClassLoader. UrlClassLoader has a required parameter of URL[] that calls for the classes to be loaded. We only care about one. So you need to cast the File to a URL, after from a URI. I wouldn’t recommend wrapping the location variable by new URL, as File will determine pathing better than you might. This way works well. Do not use File.toURL() because thats deprecated :^).
The first parameter comes from the settings that we loaded. ‘Main’ points to the plugin class, which is the combinations of packages to the plugin class delimited not by “/” but by a period. For example, com.whatever.plugins.Plugin would be an acceptable path to the plugin. Not com.whatever.plugins.Plugin.class, for example. Finally, newInstance() the class and return it. I added, to the plugin class, the config. Just note that for #2.

  1. Make the plugin environment

So your plugin needs to have some mass to it. What we want in our plugin is very subjective so I will go over two basic things. Storing the settings and an entry function. You can add fields and functions. The class Plugin is abstract. In #1, I added the setConfig() which you can see working below. I do NOT have a constructor here. The reason is newInstance() doesn’t allow passing args. Simples.

public abstract class Plugin {

	private HashMap<String, String> config;
	
	public void setConfig(HashMap<String, String> config) {
		this.config = config;
	}
	
	public abstract void initialize();
	
	public String getSetting(String setting) {
		return config.get(setting);
	}
	
	public void setSetting(String setting, String value) {
		this.config.put(setting, value);
	}
	
}

This will be extended by the user. So there is no problem here. This is a template class. It allows you to detail what minimum the user needs to do. The program itself will be totally blind to any fields that the user creates inside the extended class of this, just as normal. So when you load a plugin, you can’t do PluginLoader.loadPlugin(place, config.cfg).someFieldNotDefinedInPluginClass, but you can do .initialize(). We require the user to @Override initialize and do stuff in it, so anything that that utilizes, such as the fields the user creates when they extend it.

  1. Make the abstract plugin environment

Naturally, you will have things inside the Jar that the plugin user will need to use, such as new Vec3() or even jframe.setTitle(“Whatever”). Create a new Project. Put all classes the user will use inside the new project. Make all the bodies empty, except for things such as enums. This will make it so the user doesn’t utilize the code or anything, because its meant to be a template. Include this Plugin class. Just copy it over without change. Make sure all packages are exact. Jar it.

  1. Make the plugin

Making the plugin is simple. Take this jar that you created and put it into a new project. Configure your build path. Create a class, which extends Plugin. This should and will force you to overwrite the initialize abstract void function. Put something simple, like Syso(“Testing plugin 123”);. Don’t use the default package please. Thats gross. Jar it, and then all you need to do is load it and utilize it, but not before making the config.cfg file to determine your main class’ location. Here is an example file.

[quote]Name: Test Plugin Example
Main: test.plugin.Example
[/quote]
Inside of settings you will have two things. Name, which points to Test Plugin Example. And Main, which points to test.plugin.Example. Let’s connect this back to…

final Class<?> clazz = Class.forName(settings.get("Main"), true, new URLClassLoader(new URL[]{plugin.toURI().toURL()}));

settings.get(“Main”) points to what I was talking about.

  1. Load the plugin

So now let’s load it in and utilize it.

final Plugin plugin = PluginLoader.loadPlugin("res/plugins/PluginTest.jar", "config_file.cfg"); //called my config file config_file.cfg, which is in my jar
plugin.initialize(); //this does its thing, which I set to Syso("Testing plugin 123");

And thats all there is to it!

I will edit this post later to include a video of this.