Concise notes for the impatient learner

Uncategorized

Java Applications with Plugins – Basics

Objective

Create a basic Java application that is extensible through plugin classes loaded at runtime.

Eclipse 2018-12 was used for the projects in this post. The full source code can be downloaded here.

Application Project

Define an interface for the plugin. In this case all the plugin does is to provide a message as a string.

public interface Plugin {
    String getMessage();
}

Easy so far. Now for the more complex part: creating a custom ClassLoader. We need to create a subclass of ClassLoader and override the loadClass method.

public class PluginLoader extends ClassLoader {
	
    // Given the name of a class, it returns a Class object. 
    @Override
    public Class<?> loadClass(String name) {
        // Is the class already loaded?
        Class<?> pluginClass = findLoadedClass(name);

        // Is it a system class?
        if (pluginClass == null) {
            try { pluginClass = findSystemClass(name); }
            catch (Exception ex) {}
        }

        // If none of the above, read from the corresponding .class file.
        if (pluginClass == null) {
            File file = new File(pluginDirectory + "/" + name + ".class");

            try {
                byte[] fileContent = Files.readAllBytes(file.toPath());
                pluginClass = defineClass(name, fileContent, 0, fileContent.length);
                resolveClass(pluginClass);
            } catch (Exception e) {}		
        }

        return pluginClass;
    }
}

Following the comments in the code, the loadClass method does the following.

  • Check whether the class is already loaded.
  • Check whether it’s a system class.
  • If none of the above, load the class from file.

If we don’t check with findLoadedClass and findSystemClass, the program errors out reporting it cannot find the parent class Plugin. This is because loadClass is called recursively. When it is called for the parent class Plugin, it’d try to extract if from the file and it will fail (even though the class is already loaded, as it’s not loaded dynamically).

The full source code includes methods to specify the directory containing the plugins and get a list of the plugins available in that directory.

The main method then simply gets a list of available plugins, loads them, and displays the messages provided.

public class Main {
    public static void main(String[] args) {
		
        // Get a list of plugin names.
        PluginLoader loader = new PluginLoader();
        loader.setPluginDirectory("plugins");
        String[] pluginNames = loader.listPlugins();
		
        // For each plugin, get the class, instantiate it, and print the message.
        for(String pluginName: pluginNames) {
            Class<?> pluginClass = loader.loadClass(pluginName);

            if(pluginClass != null) {
                try {
                    Plugin plugin = (Plugin)pluginClass.newInstance();
                    System.out.println(plugin.getMessage());
                } catch (InstantiationException | IllegalAccessException e) {
                    System.out.println("Error loading plugin " + pluginName);
                }
            }
        }
    }
}

Plugin Development Project

Create a new project. Copy the Plugin file to this project. Create two plugin classes implementing the interface.

public class ByePlugin implements Plugin {
    @Override
    public String getMessage() {
        return "Bye";
    }
}
public class HelloPlugin implements Plugin {
    @Override
    public String getMessage() {
        return "Hello";
    }
}

Compile the files to get .class files.
Move the .class files generated for the two plugins into the “plugins” folder in the application project.

When you run the application, the two plugins are loaded and the “Bye” and “Hello” messages are displayed.

Conclusions

The “SDK” to share with users that want to develop plugins is just the file defining the Plugin interface.

The code shown here is a very bare-bones framework. There are several issues left to address

  • Making sure the loaded class actually implements the plugin interface. You can get a list of interfaces implemented by the loaded class through reflection. You can also confirm that the expected methods are present and working correctly before using the plugin in the rest of your application.
  • Preventing malicious plugin code from compromising the application/system. Loading a plugin and executing its methods is equivalent to running arbitrary code. Are you sure you can trust the author of the plugin? Java provides a way to limit what the loaded classes can do. This part will be covered in a separate post.

Links

Adding Plugins to a Java Application
Excellent explanation and example. This article was the most useful in making my example work.

Class Loaders in Java

The basics of Java class loaders

Leave a Reply