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.
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 implementation1. 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.AuthPluginServicewith the single line:
com.vijay.bus.plugin.DefaultProviderImpl4. 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.AuthPluginServicefile 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.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
