Understanding Java SPI and Implementing a Plugin Architecture
This article explains Java's Service Provider Interface (SPI) mechanism, compares SPI with traditional APIs, and provides a step‑by‑step guide—including Maven project setup, interface definition, implementation classes, custom class loader, and Spring Boot integration—to build a dynamic plugin system for backend applications.
What is Java SPI
Java SPI (Service Provider Interface) is a service discovery and loading mechanism that allows developers to define multiple implementations for an interface and dynamically discover and load them at runtime.
The core of the SPI mechanism is that service providers place implementation classes under META-INF/services and specify them in configuration files. When a service is needed, the Java runtime automatically scans these directories, finds the appropriate implementation classes, and loads them, enabling dynamic service discovery.
Main Uses of Java SPI
Extension points : Service providers can add extension points to frameworks or libraries without modifying business code.
Dynamic replacement : Components can be inserted or swapped at runtime, encouraging loosely‑coupled design.
Third‑party extensions : Allows third‑party libraries to replace core components, enriching the Java ecosystem and giving developers great flexibility.
SPI is widely used in many frameworks and libraries such as servlet container initialization, type conversion, logging, and other scenarios. By using SPI, Java applications can integrate third‑party services without changing business code, improving extensibility and maintainability.
SPI vs. API
The main differences between SPI and API lie in definition method, invocation style, flexibility, dependency relationship, and purpose.
Definition method : APIs are written and exposed by developers, while SPIs are defined by framework or library providers for third‑party implementation.
Invocation style : APIs are called directly; SPIs are selected via configuration files and loaded automatically by the framework.
Flexibility : APIs require implementation classes to be fixed at compile time; SPIs allow runtime replacement based on configuration.
Dependency relationship : Applications depend on the API library; frameworks depend on the third‑party implementation of the SPI.
Purpose : APIs describe how to interact with a component, whereas SPIs provide a plug‑in architecture that enables dynamic discovery and loading of implementations.
In summary, an API is a contract for interaction, while an SPI is a mechanism for dynamically discovering and loading implementations of that contract.
Implementation Process
Directory structure:
sa-auth parent project
-- sa-auth-bus business module
-- sa-auth-plugin module defining SPI interfaces
-- sa-auth-plugin-ldap module simulating a third‑party implementation1. Create a Maven parent project named sa-auth with the following pom.xml :
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.16.RELEASE</version>
</parent>
<groupId>com.vijay</groupId>
<artifactId>cs-auth</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>cs-auth</name>
<packaging>pom</packaging>
<modules>
<module>cs-auth-plugin</module>
<module>cs-auth-bus</module>
<module>cs-auth-plugin-ldap</module>
</modules>
... (dependencyManagement omitted for brevity) ...
</project>2. Create the sa-auth-plugin module and define the SPI interface:
package com.vijay.csauthplugin.service;
/**
* Plugin SPI interface
*/
public interface AuthPluginService {
/**
* Login authentication
* @param userName username
* @param password password
* @return authentication result
*/
boolean login(String userName, String password);
/**
* Name of the AuthPluginService for convenient lookup
* @return service name
*/
String getAuthServiceName();
}3. Implement the SPI in cs-auth-plugin-ldap :
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";
}
}4. In each module, create the META-INF/services directory and a file named with the fully‑qualified SPI interface name ( com.vijay.csauthplugin.service.AuthPluginService ) containing the fully‑qualified implementation class name (e.g., com.vijay.csauthplugin.ldap.LdapProviderImpl ).
5. Create a default implementation in cs-auth-plugin-bus :
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";
}
}6. Implement a custom class loader to load external JARs:
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); }
}7. Create a utility to load all JAR files from a directory:
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
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);
}
}
}
}8. Add the loader to the Spring Boot entry point:
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";
ExternalJarLoader.loadExternalJars(jarPath);
SpringApplication.run(CsAuthBusApplication.class, args);
}
}9. Provide a plugin provider that selects the external implementation if available, otherwise falls back to the default:
package com.vijay.bus.plugin;
import com.vijay.csauthplugin.service.AuthPluginService;
import java.util.ServiceLoader;
public class PluginProvider {
public static AuthPluginService getAuthPluginService() {
ServiceLoader
defaultLoad = ServiceLoader.load(AuthPluginService.class);
AuthPluginService plugin = null;
for (AuthPluginService authPluginService : defaultLoad) {
if (authPluginService instanceof DefaultProviderImpl) {
plugin = authPluginService;
} else {
return authPluginService;
}
}
return plugin;
}
}10. Register the selected plugin as a Spring bean:
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();
}
}11. Create a REST controller to test the plugin:
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() {{
put("name", authPluginService.getAuthServiceName());
put("login", authPluginService.login("vijay", "123456"));
}};
}
}When the application runs, the /test endpoint returns the default implementation. After placing the cs-auth-plugin-ldap JAR into the external plugin directory and restarting, the endpoint returns the LDAP implementation, demonstrating dynamic plugin loading via Java SPI.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
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.