DEV Community

Ginto Philip
Ginto Philip

Posted on

Building a Simple Plugin Architecture in Java Using the Java SPI Mechanism

Software systems often grow over time, and with growth comes the need for flexibility. You might want to add new features, support new formats, or integrate with new systems without modifying the core application each time. This is exactly where a plugin architecture shines.

In this post, we’ll walk through how to build a lightweight plugin architecture using Java’s Service Provider Interface (SPI). We’ll also describe a practical example involving greeting messages in multiple languages.

The main objective of this post is to make people familiarize with the plugin architecture in a simple way.

What Is a Plugin Architecture?

A Plugin Architecture is a software design pattern that allows for the core functionality of an application to be extended, modified, or customized without changing the core application's source code.

New functionality is packaged as small, independent modules called plugins, which the main application discovers and loads at runtime.

  • Core Application (Platform): This is the stable, main part of the software. It defines a set of contracts (interfaces or abstract classes) that external components must adhere to. It provides the mechanism to discover and load extensions.

  • Plugins (Extensions): These are external, self-contained modules that implement the contracts defined by the core application. They provide the actual extension logic.

Why Use a Plugin Architecture?

Using this architecture offers several significant advantages:

  • Extensibility: New features can be added rapidly by simply deploying new plugins, without rebuilding or redeploying the entire core application.

  • Decoupling/Modularity: It separates concerns, making the core platform smaller, more stable, and easier to maintain. Plugins can be developed and updated independently.

  • Third-Party Integration: It enables third parties to build extensions for your platform (e.g., IDE extensions, browser add-ons).

  • Reduced Complexity: Allows teams to work on specific features in isolation, leading to cleaner codebases.

Real-world examples

  • IntelliJ / Eclipse extensions
  • Browser extensions
  • Payment gateways in e-commerce platforms

What Is Java SPI (Service Provider Interface)?

Java’s SPI is a built-in mechanism that enables loose coupling between an API (interface) and its implementations. It allows an application (the core Platform) to load implementations (the Plugins) of an interface or abstract class (the Contract) that are provided by external JAR files at runtime.

It allows applications to discover implementations at runtime using simple configuration files.

At a high level, SPI involves:

  • Service: An interface or abstract class known to the application and implemented by the plugins. In the project, this is the GreetingPlugin interface.

  • Service Provider: A specific implementation of the Service (e.g., SpanishGreeting).

  • Service Loader: A core Java class (java.util.ServiceLoader) that the application uses to discover and load all available Service Providers for a given Service interface.

For the SPI to work:

  • Plugins must contain a special file in the META-INF/services/ directory.

  • The file's name must be the fully qualified name of the Service interface (the Contract).

    • eg: com.gintophilip.core.greeting.contract.GreetingPlugin
  • The content of this file is a list of the fully qualified class names of the Service Provider implementations within that JAR.

    • com.gintophilip.spanishgreeting.SpanishGreeting com.gintophilip.hindigreeting.HindiGreeting

Project Structure and Implementation Overview

The project is divided into three modules,

  • Plugin Platform (Core Application)

  • Plugin Platform Contracts (Interfaces / Contracts)

  • Plugin Implementations (Actual Plugins)

Plugin Platform (Core Application)

This is the main application. Its primary task is to do the following:

  • Manage users (simulated database with hardcoded users)

  • Provide a CLI interface (Login, show user details, quit)

  • Use the ServiceLoader to discover and load the available GreetingPlugin implementations at runtime.

  • Delegate greeting responsibility to the appropriate plugin based on user language

Key Responsibilities:

  • Configuration: Reads the configured plugin folder location (e.g., plugins/).

  • Plugin Discovery and Loading: At startup, it needs to find the plugin JARs in the folder and use ServiceLoader to load all implementations of GreetingPlugin.

    • The core component for using the plugins, like UserGreeter, will utilize ServiceLoader.load(GreetingPlugin.class) to get an iterable of all available plugin instances.
  • Plugin Selection and Execution (The UserGreeter Logic):

    1. When a user logs in, the UserGreeter logic fetches the user's preferred language (currentUser.getPreferredLanguage()).
    2. It iterates through the loaded GreetingPlugin instances.
    3. It finds the plugin where plugin.getLanguage() matches the user's preferred language.
    4. If a match is found, it calls plugin.greet(userName).
    5. If no matching plugin is found (or no preferred language is set), it falls back to the default English greeting implementation provided by the core platform itself.
  • CLI Interface: Provides the basic interaction loop (L for login, D for details, q! to quit).

Plugin Platform Contracts (The Service)

This module defines the public interface that all plugins must implement. It's the Service in the SPI pattern. It must be packaged as a JAR and be a dependency for both the Core Platform and the Plugin Implementations.

Contract Interface: GreetingPlugin

public interface GreetingPlugin {
    void greet(String userName);
    String getLanguage();
}
Enter fullscreen mode Exit fullscreen mode

This contract defines:

  • greet(...) → Logic to greet the user

  • getLanguage() → A unique identifier for the language this plugin supports

Plugin Implementations

This project contains concrete plugin classes in our case, a Spanish and a Hindi greeter. These are the actual, deployable extensions. They depend only on the Contracts module. They do not need to know anything about the Platform module.

Each plugin:

  • Depends on Plugin Platform Contracts

  • Implements GreetingPlugin

  • Has a provider configuration file

In this project we provide two implemntations→ SpanishGreeting and HindiGreeting Both implementations are provided in a single JAR.

The plugin JAR must contain the following file structure and content:

  • Location: resources/META-INF/services/

  • File Name: com.gintophilip.core.greeting.contract.GreetingPlugin (using the fully qualified name of the contract interface).

  • File Content: (Listing all implementations in the JAR)

    com.gintophilip.core.greeting.plugin.SpanishGreeting com.gintophilip.core.greeting.plugin.HindiGreeting

Once packaged as a JAR and dropped into the plugins/ folder, the platform picks them up automatically at startup.

Here is a gist of the code

The contract

public interface GreetingPlugin {
    void greet(String userName);
    String getLanguage();
}
Enter fullscreen mode Exit fullscreen mode

The implementation

public class SpanishGreeting implements GreetingPlugin {

    @Override
    public void greet(String userName) {
        System.out.println("Hola "+ userName+" "+"bienvenido");
    }

    @Override
    public String getLanguage() {
        return "Spanish";
    }
}
Enter fullscreen mode Exit fullscreen mode

Loading the plugin

public void loadPlugins() {
    loadDefaultPlugin();
    File pluginDir = new File(PLUGIN_DIRECTORY);

    if (!pluginDir.exists() || !pluginDir.isDirectory()) {
        System.out.println("[PluginRepository] Plugin directory not found: " + pluginDir.getAbsolutePath());
        return;
    }

    File[] jarFiles = pluginDir.listFiles(new JarFileFilter());
    if (jarFiles == null || jarFiles.length == 0) {
        System.out.println("[PluginRepository] No plugin JARs found in: " + pluginDir.getAbsolutePath());
        return;
    }

    for (File jarFile : jarFiles) {
        loadPluginFromJar(jarFile);
    }

    if (greetingPluginsMap.isEmpty()) {
        System.out.println("[PluginRepository] No valid GreetingPlugins loaded.");
    }
}
private void loadPluginFromJar(File jarFile) {
    try {
        URL jarUrl = jarFile.toURI().toURL();
        try (URLClassLoader classLoader = new URLClassLoader(new URL[]{jarUrl}, this.getClass().getClassLoader())) {
            ServiceLoader<GreetingPlugin> serviceLoader =
                    ServiceLoader.load(GreetingPlugin.class, classLoader);

            for (GreetingPlugin plugin : serviceLoader) {
                addPlugin(plugin);
                System.out.println("[PluginRepository] Loaded plugin: " + plugin.getClass().getName());
            }
        }
    } catch (MalformedURLException e) {
        System.err.println("[PluginRepository] Invalid plugin URL: " + jarFile.getAbsolutePath());
    } catch (Exception e) {
        System.err.println("[PluginRepository] Failed to load plugin from: " + jarFile.getAbsolutePath());
        e.printStackTrace();
    }
}
Enter fullscreen mode Exit fullscreen mode

The code for each module is available in github:

How to run this project:

Clone the three repositories above. Then,

  • Build the Plugin Platform Contracts module.

    After the build completes, a JAR file named PluginPlatformContracts-1.0-SNAPSHOT.jar will appear in the build folder.

  • Add the contracts JAR to both the Plugin Platform and Plugin Implementation projects.

    In each project’s build.gradle, add the following dependency:

    implementation files('libs/PluginPlatformContracts-1.0-SNAPSHOT.jar')
    
  • Ensure that a libs folder exists in each project; create it if necessary.

    • libs folder
  • Build the Plugin Implementation module.

    This will generate a JAR file named SPHNGreeting-1.0-SNAPSHOT.jar in its build folder.

  • Deploy the plugin.

    Copy the generated SPHNGreeting-1.0-SNAPSHOT.jar into the plugins folder of the Plugin Platform.

    The location of the plugin directory is configured in the variable PLUGIN_DIRECTORY in the PluginRepository class. You can change the value here

plugin

  • Run the Plugin Platform application.

    Launch Main.java in the Plugin Platform. Once the application starts and the CLI becomes visible, type L, then enter a username and password for a user. You should see the greeting produced by the plugin.

output

Here is the hard coded user details:

First Name Last Name Username Password Preferred Language
John Doe jdoe password123 NONE
Alice Smith asmith alicePass Hindi
Bob Johnson bjohnson securePass NONE
Emma Brown ebrown hello123 Spanish
Liam Wilson lwilson mypassword NONE

Each user will be greeted in following language as listed in the below table

Username Preferred Language
jdoe English. [NONE is given defaults to English]
asmith Hindi
bjohnson English. [NONE is given defaults to English]
ebrown Spanish
lwilson English. [NONE is given defaults to English]

How Plugins Are Packaged and Deployed

To deploy a plugin:

  1. Build the plugin project into a JAR

  2. Ensure it contains:

    • implementation classes
    • META-INF/services/... provider file
    • The contract dependency
  3. Copy the JAR into the configurable plugin directory

  4. Restart the core application

  5. The new language is now supported with no code changes to the core app

Happy Coding and Exploring Plugins!🎉

Remember, building plugin architectures is not just about adding features, it’s about designing software that can grow gracefully over time. Every new plugin you create is a small step toward mastering modular, flexible, and maintainable Java applications. Keep experimenting, keep learning, and enjoy the journey!

Top comments (0)