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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

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.