Mastering Java Plugin Architecture: From SPI to Spring Factories
This article explains the concept and advantages of plugin-based development, outlines common implementation strategies in Java, demonstrates practical SPI and custom configuration approaches, and shows how Spring Boot’s spring.factories mechanism can be used to build extensible backend systems.
1. Introduction
Plugin-based development is widely used in many programming languages and frameworks such as Jenkins, Rancher, IDEs like IntelliJ IDEA and VS Code. Plugins improve extensibility, scalability, and overall system value.
1.1 Benefits of Using Plugins
1.1.1 Module Decoupling
Plugins provide a higher degree of decoupling and flexibility compared with traditional design patterns, allowing dynamic replacement of components such as SMS providers.
1.1.2 Improved Extensibility and Openness
Frameworks like Spring expose many extension points through plugins, enabling easy integration with other middleware and enriching the ecosystem.
1.1.3 Easy Third‑Party Integration
Third‑party systems can implement custom plugins with minimal intrusion, supporting hot‑loading via configuration.
1.2 Common Implementation Ideas
Typical Java approaches include:
SPI mechanism
Convention‑based configuration with reflection
Spring Boot Factories
Java agents
Built‑in Spring extension points
Third‑party plugin libraries (e.g., spring‑plugin‑core)
Spring AOP
2. Common Java Plugin Solutions
2.1 ServiceLoader (Java SPI)
ServiceLoader implements the SPI pattern. Define an interface and implementations, then list the implementation class names in META-INF/services/<interface>. At runtime ServiceLoader loads them.
2.1.1 Java SPI Overview
SPI (Service Provider Interface) enables dynamic discovery of implementations, e.g., JDBC drivers.
Illustration of SPI mechanism:
2.1.2 Simple SPI Example
Interface definition:
public interface MessagePlugin {
public String sendMsg(Map msgMap);
}Two implementations:
public class AliyunMsg implements MessagePlugin {
@Override
public String sendMsg(Map msgMap) {
System.out.println("aliyun sendMsg");
return "aliyun sendMsg";
}
}
public class TencentMsg implements MessagePlugin {
@Override
public String sendMsg(Map msgMap) {
System.out.println("tencent sendMsg");
return "tencent sendMsg";
}
}Service configuration file (in resources/META-INF/services/com.example.MessagePlugin) lists the fully qualified class names.
Loading and using ServiceLoader:
public static void main(String[] args) {
ServiceLoader<MessagePlugin> loader = ServiceLoader.load(MessagePlugin.class);
for (MessagePlugin plugin : loader) {
plugin.sendMsg(new HashMap());
}
}2.2 Custom Configuration + Convention
Instead of ServiceLoader files, define a custom YAML/Properties configuration that lists implementation classes, then load them via reflection.
server:
port: 8081
impl:
name: com.congge.plugins.spi.MessagePlugin
clazz:
- com.congge.plugins.impl.TencentMsg
- com.congge.plugins.impl.AliyunMsgConfiguration‑loading class:
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("impl")
@ToString
public class ClassImpl {
@Getter @Setter String name;
@Getter @Setter String[] clazz;
}Controller that instantiates plugins based on the configuration:
@RestController
public class SendMsgController {
@Autowired ClassImpl classImpl;
@GetMapping("/sendMsg")
public String sendMsg() throws Exception {
for (int i = 0; i < classImpl.getClazz().length; i++) {
Class pluginClass = Class.forName(classImpl.getClazz()[i]);
MessagePlugin plugin = (MessagePlugin) pluginClass.newInstance();
plugin.sendMsg(new HashMap());
}
return "success";
}
}2.3 Loading Plugins from External JARs
Read JAR files from a designated directory, create a URLClassLoader, and instantiate classes listed in the custom configuration.
public static void loadJarsFromAppFolder() throws Exception {
String path = "E:\\code-self\\bitzpp\\lib";
File f = new File(path);
if (f.isDirectory()) {
for (File subf : f.listFiles()) {
if (subf.isFile()) loadJarFile(subf);
}
} else {
loadJarFile(f);
}
}
public static void loadJarFile(File path) throws Exception {
URL url = path.toURI().toURL();
URLClassLoader classLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
// Example of invoking a method from the loaded JAR
Method method = URLClassLoader.class.getMethod("sendMsg", Map.class);
method.setAccessible(true);
method.invoke(classLoader, url);
}3. Plugin Mechanism in Spring Boot
Spring Boot uses spring.factories (located in META-INF) to declare implementations of interfaces. SpringFactoriesLoader reads this file and creates instances.
public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
try {
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
List<String> result = new ArrayList<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
String factoryClassNames = properties.getProperty(factoryClassName);
result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
}
return result;
} catch (IOException ex) {
throw new IllegalArgumentException("Unable to load [" + factoryClass.getName() + "] factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}Define a service interface, two implementations, and list them in META-INF/spring.factories:
com.congge.plugin.spi.SmsPlugin=\
com.congge.plugin.impl.SystemSmsImpl,\
com.congge.plugin.impl.BizSmsImplLoad implementations at runtime:
@GetMapping("/sendMsgV3")
public String sendMsgV3(String msg) throws Exception {
List<SmsPlugin> smsServices = SpringFactoriesLoader.loadFactories(SmsPlugin.class, null);
for (SmsPlugin sms : smsServices) {
sms.sendMessage(msg);
}
return "success";
}4. End‑to‑End Plugin Case Study
A realistic scenario with three micro‑services: a core module defining MessagePlugin, and two implementations (Aliyun and Tencent). The core module loads plugins via ServiceLoader or custom configuration, selects the appropriate implementation based on a configuration parameter, and falls back to a default implementation when none is found.
Key code snippets include the plugin interface, ServiceLoader‑based factory, custom configuration loader, and Spring Boot controller that delegates to the selected plugin.
5. Conclusion
Plugin mechanisms are pervasive across languages, frameworks, and tools. Mastering them is essential for building flexible, extensible backend systems and for architectural design.
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.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
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.
