Plug‑and‑Play Spring Boot: Building a Plugin Architecture
This article explains how to implement a plug‑and‑play development model in Spring Boot using Java SPI, custom configuration, and Spring Factories, providing step‑by‑step code examples, a multi‑module microservice case study, and practical guidance for creating extensible, decoupled applications.
Introduction
The plug‑in development model is widely used in many languages and frameworks (e.g., Jenkins, Rancher, IDEs) to improve system extensibility and flexibility. In Java, plugins enable higher‑level module decoupling, easier third‑party integration, and hot‑loading of functionality.
Benefits of Plugins
Module decoupling : Plugins allow dynamic replacement of implementations, such as switching SMS providers at runtime.
Enhanced extensibility : Spring’s rich ecosystem is built on numerous extension points that can be leveraged via plugins.
Third‑party integration : External systems can implement a predefined plug‑in interface with minimal intrusion, even supporting configuration‑driven hot loading.
Common Java Plugin Implementations
Typical approaches include:
SPI mechanism (ServiceLoader)
Custom configuration files combined with reflection
Spring Boot Factories mechanism
Java agent (instrumentation)
Spring built‑in extension points
Third‑party plugin libraries (e.g., spring‑plugin‑core)
Spring AOP
1. ServiceLoader (SPI) Approach
Java’s Service Provider Interface (SPI) enables runtime discovery of implementations. The article defines a MessagePlugin interface and two implementations ( AliyunMsg and TencentMsg).
public interface MessagePlugin {
String sendMsg(Map msgMap);
} 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";
}
}A resource file under META-INF/services/com.congge.plugins.spi.MessagePlugin lists the fully‑qualified class names. Loading code:
public static void main(String[] args) {
ServiceLoader<MessagePlugin> loader = ServiceLoader.load(MessagePlugin.class);
for (MessagePlugin plugin : loader) {
plugin.sendMsg(new HashMap());
}
}Running this prints the messages from both implementations, demonstrating dynamic discovery.
2. Custom Configuration + Reflection
To avoid proliferating META-INF files, a custom configuration file can specify implementation class names. The article shows a YAML snippet:
server:
port: 8081
impl:
name: com.congge.plugins.spi.MessagePlugin
clazz:
- com.congge.plugins.impl.TencentMsg
- com.congge.plugins.impl.AliyunMsgA POJO ( ClassImpl) holds the configuration, and a controller loads the classes via Class.forName and invokes sendMsg on each instance.
@RestController
public class SendMsgController {
@Autowired
ClassImpl classImpl;
@GetMapping("/sendMsg")
public String sendMsg() throws Exception {
for (String clazzName : classImpl.getClazz()) {
Class pluginClass = Class.forName(clazzName);
MessagePlugin plugin = (MessagePlugin) pluginClass.newInstance();
plugin.sendMsg(new HashMap());
}
return "success";
}
}3. Loading JARs from a Directory
When implementations are packaged as separate JARs, the article provides a utility ( ServiceLoaderUtils) that scans a configured directory, loads each JAR with URLClassLoader, and invokes a known method via reflection.
public static void loadJarsFromAppFolder() throws Exception {
String path = "E:\\code-self\\bitzpp\\lib";
File dir = new File(path);
if (dir.isDirectory()) {
for (File f : dir.listFiles()) {
if (f.isFile()) loadJarFile(f);
}
} else {
loadJarFile(dir);
}
}
private static void loadJarFile(File file) throws Exception {
URL url = file.toURI().toURL();
URLClassLoader cl = (URLClassLoader) ClassLoader.getSystemClassLoader();
Method m = URLClassLoader.class.getMethod("sendMsg", Map.class);
m.setAccessible(true);
m.invoke(cl, url);
}4. Spring Boot spring.factories Mechanism
Spring Boot extends the SPI idea with spring.factories. The class SpringFactoriesLoader reads META-INF/spring.factories from all JARs on the classpath and returns instances of the configured types.
public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
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 props = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
String names = props.getProperty(factoryClassName);
result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(names)));
}
return result;
}Example: an SmsPlugin interface with two implementations ( BizSmsImpl and SystemSmsImpl) is declared in spring.factories:
com.congge.plugin.spi.SmsPlugin=\
com.congge.plugin.impl.SystemSmsImpl,\
com.congge.plugin.impl.BizSmsImplA controller loads all implementations via SpringFactoriesLoader.loadFactories and invokes sendMessage on each.
@GetMapping("/sendMsgV3")
public String sendMsgV3(String msg) throws Exception {
List<SmsPlugin> plugins = SpringFactoriesLoader.loadFactories(SmsPlugin.class, null);
for (SmsPlugin p : plugins) {
p.sendMessage(msg);
}
return "success";
}5. End‑to‑End Case Study
The article assembles a realistic microservice scenario:
biz‑pp : defines MessagePlugin and is packaged as a JAR.
bitpt and miz‑pt : depend on biz‑pp and provide Alibaba and Tencent SMS implementations respectively.
biz‑pp includes a PluginFactory that selects a plugin based on a configuration property ( msg.type).
public static MessagePlugin getTargetPlugin(String type) {
ServiceLoader<MessagePlugin> loader = ServiceLoader.load(MessagePlugin.class);
for (MessagePlugin p : loader) {
if (type.equals("aliyun") && p instanceof BitptImpl) return p;
if (type.equals("tencent") && p instanceof MizptImpl) return p;
}
return null;
}The SmsService autowires the selected plugin (or falls back to a default implementation) and a REST controller exposes /sendMsg. Maven dependencies include the two implementation JARs, the core biz‑pp JAR, and Lombok.
<dependency>
<groupId>com.congge</groupId>
<artifactId>biz-pt</artifactId>
<version>1.0‑SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.congge</groupId>
<artifactId>miz-pt</artifactId>
<version>1.0‑SNAPSHOT</version>
</dependency>After starting the application, calling http://localhost:8087/sendMsg?msg=hello triggers the selected plugin, prints the SMS sending log, and returns success.
Conclusion
By leveraging Java SPI, custom configuration, and Spring Boot’s spring.factories, developers can build a plug‑and‑play architecture that cleanly separates core logic from optional extensions, supports hot‑loading, and simplifies third‑party integration in backend systems.
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 XiaoFu
xiaofucode.com – a programmer learning guide driven by the pursuit of profit
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.
