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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
