Mastering Java SPI: Build a Pluggable Authentication System with Spring Boot

This guide explains Java's Service Provider Interface (SPI) mechanism, compares it with APIs, and walks through creating a multi‑module Maven project that defines SPI interfaces, implements plugins, loads external JARs with a custom class loader, and integrates the plugins into a Spring Boot application for dynamic authentication.

macrozheng
macrozheng
macrozheng
Mastering Java SPI: Build a Pluggable Authentication System with Spring Boot

Overview of Java SPI

Java SPI (Service Provider Interface) is a runtime discovery mechanism that loads implementations of an interface from configuration files placed under META-INF/services. It enables plug‑in style extensions without changing core code, allowing components to be swapped at runtime.

SPI vs API

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

Invocation : API methods are called directly; SPI implementations are selected via configuration files and loaded automatically.

Flexibility : API implementations are fixed at compile time; SPI implementations can be replaced at runtime.

Dependency : Applications depend on APIs; frameworks depend on SPI implementations.

Purpose : APIs expose functionality; SPIs provide a plug‑in architecture for dynamic extension.

Multi‑module Maven Project

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

1. Parent POM (sa-auth)

<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>
  <parent>org.springframework.boot:spring-boot-starter-parent:2.1.16.RELEASE</parent>
  <groupId>com.vijay</groupId>
  <artifactId>cs-auth</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <modules>
    <module>cs-auth-plugin</module>
    <module>cs-auth-bus</module>
    <module>cs-auth-plugin-ldap</module>
  </modules>
</project>

2. Define SPI Interface (sa-auth-plugin)

package com.vijay.csauthplugin.service;
public interface AuthPluginService {
    /** Login authentication */
    boolean login(String userName, String password);
    /** Identifier used to locate the implementation */
    String getAuthServiceName();
}

3. Default Implementation (sa-auth-bus)

package com.vijay.bus.plugin;
import com.vijay.csauthplugin.service.AuthPluginService;
public class DefaultProviderImpl implements AuthPluginService {
    @Override public boolean login(String u, String p) {
        return "vijay".equals(u) && "123456".equals(p);
    }
    @Override public String getAuthServiceName() {
        return "DefaultProvider";
    }
}

Register the class in

src/main/resources/META-INF/services/com.vijay.csauthplugin.service.AuthPluginService

with the single line:

com.vijay.bus.plugin.DefaultProviderImpl

4. Mock Third‑Party Plugin (sa-auth-plugin-ldap)

package com.vijay.csauthplugin.ldap;
import com.vijay.csauthplugin.service.AuthPluginService;
public class LdapProviderImpl implements AuthPluginService {
    @Override public boolean login(String u, String p) {
        return "vijay".equals(u) && "123456".equals(p);
    }
    @Override public String getAuthServiceName() {
        return "LdapProvider";
    }
}

Place the fully‑qualified class name in the same

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

file so the SPI loader can discover it.

5. 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); }
}

6. Load External JARs at Runtime

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 dirPath) {
        File dir = new File(dirPath);
        if (!dir.isDirectory()) {
            throw new IllegalArgumentException("Invalid directory path");
        }
        List<URL> urls = new ArrayList<>();
        for (File f : Objects.requireNonNull(dir.listFiles())) {
            if (f.getName().endsWith(".jar")) {
                urls.add(f.toURI().toURL());
            }
        }
        PluginClassLoader cl = new PluginClassLoader(urls.toArray(new URL[0]));
        Thread.currentThread().setContextClassLoader(cl);
    }
}

7. Spring Boot Application Integration

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) {
        ExternalJarLoader.loadExternalJars("/path/to/external/plugins");
        SpringApplication.run(CsAuthBusApplication.class, args);
    }
}

8. 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 fallback = null;
        for (AuthPluginService s : loader) {
            if (s instanceof DefaultProviderImpl) {
                fallback = s; // keep default as fallback
            } else {
                return s; // external implementation wins
            }
        }
        return fallback; // may be null if no implementation found
    }
}

9. Spring Bean 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();
    }
}

10. 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;
import java.util.Map;
@RestController
public class TestController {
    @Resource private AuthPluginService authPluginService;
    @GetMapping("test")
    public Map<String,Object> test() {
        Map<String,Object> m = new HashMap<>();
        m.put("name", authPluginService.getAuthServiceName());
        m.put("login", authPluginService.login("vijay","123456"));
        return m;
    }
}

When the cs-auth-plugin-ldap JAR is placed in the external plugins directory and the application restarts, the controller returns the LDAP implementation instead of the default.

Key Points

Define an SPI interface in a dedicated module.

Provide a default implementation in the core module and register it via META-INF/services.

Package third‑party implementations as separate JARs that also contain the service registration file.

Use a custom URLClassLoader to load external JARs at runtime.

Leverage ServiceLoader to discover implementations and prefer external ones.

Expose the selected implementation as a Spring bean for injection.

plugin architectureSpring BootDynamic LoadingCustom ClassLoaderServiceLoaderJava SPIMaven Multi‑module
macrozheng
Written by

macrozheng

Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.

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.