Master Java SPI: Build a Pluggable Authentication System with Maven Multi‑Module

This guide explains Java's Service Provider Interface (SPI), contrasts it with traditional APIs, and walks through creating a multi‑module Maven project that uses SPI, a custom class loader, and external JAR loading to implement a flexible authentication plugin in a Spring Boot application.

Architect
Architect
Architect
Master Java SPI: Build a Pluggable Authentication System with Maven Multi‑Module

Java SPI Overview

Java SPI (Service Provider Interface) is a built‑in service discovery mechanism. An interface defined by a framework can have multiple implementations; at runtime the framework scans META-INF/services for configuration files that list concrete classes and loads the appropriate implementation automatically.

SPI vs API

Definition : APIs are written by developers for callers; SPIs are defined by frameworks for third‑party implementations.

Invocation : API methods are called directly. SPI implementations are referenced in a file under META-INF/services and loaded by the framework.

Flexibility : API implementations are fixed at compile time, while SPI implementations can be swapped at runtime.

Dependency : Applications depend on the API library; SPI providers depend on the framework that loads them.

Purpose : APIs expose functionality; SPIs enable plug‑in architectures for dynamic extension.

Project Structure

sa-auth               (parent project)
├─ sa-auth-bus       (business logic)
├─ sa-auth-plugin   (defines SPI interface)
└─ sa-auth-plugin-ldap (simulated third‑party implementation)

Define the SPI Interface

package com.vijay.csauthplugin.service;

/** Plugin SPI interface */
public interface AuthPluginService {
    /** Login authentication */
    boolean login(String userName, String password);
    /** Name used to locate the implementation */
    String getAuthServiceName();
}

Implement a Plugin (LDAP Example)

package com.vijay.csauthplugin.ldap;
import com.vijay.csauthplugin.service.AuthPluginService;

public class LdapProviderImpl implements AuthPluginService {
    @Override
    public boolean login(String userName, String password) {
        return "vijay".equals(userName) && "123456".equals(password);
    }
    @Override
    public String getAuthServiceName() {
        return "LdapProvider";
    }
}

In src/main/resources/META-INF/services create a file named com.vijay.csauthplugin.service.AuthPluginService containing the line com.vijay.csauthplugin.ldap.LdapProviderImpl.

Default Plugin (Bus Module)

package com.vijay.bus.plugin;
import com.vijay.csauthplugin.service.AuthPluginService;

public class DefaultProviderImpl implements AuthPluginService {
    @Override
    public boolean login(String userName, String password) {
        return "vijay".equals(userName) && "123456".equals(password);
    }
    @Override
    public String getAuthServiceName() {
        return "DefaultProvider";
    }
}

Also create

META-INF/services/com.vijay.csauthplugin.service.AuthPluginService

with the line com.vijay.bus.plugin.DefaultProviderImpl.

Custom Class Loader

package com.vijay.bus.plugin;
import java.net.URL;
import java.net.URLClassLoader;

public class PluginClassLoader extends URLClassLoader {
    public PluginClassLoader(URL[] urls) { super(urls); }
    public void addzURL(URL url) { super.addURL(url); }
}

External JAR Loader

package com.vijay.bus.plugin;
import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

public class ExternalJarLoader {
    public static void loadExternalJars(String externalDirPath) {
        File dir = new File(externalDirPath);
        if (!dir.exists() || !dir.isDirectory()) {
            throw new IllegalArgumentException("Invalid directory path");
        }
        List<URL> urls = new ArrayList<>();
        File[] listFiles = dir.listFiles();
        if (Objects.nonNull(listFiles) && listFiles.length > 0) {
            ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
            try {
                for (File file : listFiles) {
                    if (file.getName().endsWith(".jar")) {
                        urls.add(file.toURI().toURL());
                    }
                }
                PluginClassLoader customClassLoader = new PluginClassLoader(urls.toArray(new URL[0]));
                Thread.currentThread().setContextClassLoader(customClassLoader);
            } catch (Exception e) {
                e.printStackTrace();
                Thread.currentThread().setContextClassLoader(contextClassLoader);
            }
        }
    }
}

Spring Boot Application (Bus Module)

package com.vijay.bus;
import com.vijay.bus.plugin.ExternalJarLoader;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class CsAuthBusApplication {
    public static void main(String[] args) {
        String jarPath = "/Users/vijay/Downloads/build/plugin"; // external plugin directory
        ExternalJarLoader.loadExternalJars(jarPath);
        SpringApplication.run(CsAuthBusApplication.class, args);
    }
}

Plugin Provider

package com.vijay.bus.plugin;
import com.vijay.csauthplugin.service.AuthPluginService;
import java.util.ServiceLoader;

public class PluginProvider {
    public static AuthPluginService getAuthPluginService() {
        ServiceLoader<AuthPluginService> loader = ServiceLoader.load(AuthPluginService.class);
        AuthPluginService plugin = null;
        for (AuthPluginService service : loader) {
            if (service instanceof DefaultProviderImpl) {
                plugin = service; // fallback to default
            } else {
                return service; // external plugin takes precedence
            }
        }
        return plugin;
    }
}

Spring Configuration

package com.vijay.bus.conf;
import com.vijay.bus.plugin.PluginProvider;
import com.vijay.csauthplugin.service.AuthPluginService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class PluginConfig {
    @Bean
    public AuthPluginService authPluginService() {
        return PluginProvider.getAuthPluginService();
    }
}

Test Controller

package com.vijay.bus.controller;
import com.vijay.csauthplugin.service.AuthPluginService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.HashMap;

@RestController
public class TestController {
    @Resource
    private AuthPluginService authPluginService;
    @GetMapping("test")
    public Object test() {
        return new HashMap<String, Object>() {{
            put("name", authPluginService.getAuthServiceName());
            put("login", authPluginService.login("vijay", "123456"));
        }};
    }
}

Testing the Setup

Run the Spring Boot application and request /test. Initially the response shows the default implementation ( DefaultProvider). Build the sa-auth-plugin-ldap module into a JAR, copy it into the external plugin directory, and restart the application. The response will now show LdapProvider and the same login result, demonstrating that the external plugin has been loaded without modifying the core business code.

project structure diagram
project structure diagram
SPI interface definition
SPI interface definition
custom class loader
custom class loader
final project diagram
final project diagram
JavapluginmavenSpring BootclassloaderSPI
Architect
Written by

Architect

Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.