Unlocking Spring Boot’s ‘Black Magic’: A Practical Guide to Plugin‑Based Development

This article explains how Spring Boot’s plugin mechanisms—such as Java SPI, custom configuration files, and Spring Factories—enable module decoupling, extensibility, and easy third‑party integration, and walks through complete code examples and a real‑world SMS‑sending case study.

Java Companion
Java Companion
Java Companion
Unlocking Spring Boot’s ‘Black Magic’: A Practical Guide to Plugin‑Based Development

1. Why Use Plugins?

Plugins provide a higher degree of module decoupling, improve system extensibility, and allow third‑party services to be integrated with minimal intrusion. In a typical scenario, different SMS providers may fail under certain conditions; a plugin can be swapped in dynamically without changing core business code.

2. Common Plugin Implementation Ideas

Java Service Provider Interface (SPI) (via ServiceLoader)

Configuration‑driven reflection (custom config files + Class.forName)

Spring Boot’s spring.factories mechanism

Java Agent (instrumentation)

Spring built‑in extension points

Third‑party plugin libraries (e.g., spring-plugin-core)

Spring AOP

3. Java SPI Example

Define a simple plugin interface:

public interface MessagePlugin {
    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";
    }
}

Register implementations in resources/META-INF/services/com.example.MessagePlugin (one fully‑qualified class name per line). Load them with:

ServiceLoader<MessagePlugin> loader = ServiceLoader.load(MessagePlugin.class);
for (MessagePlugin plugin : loader) {
    plugin.sendMsg(new HashMap());
}
SPI workflow diagram
SPI workflow diagram

4. Custom Configuration + Reflection

Instead of the fixed META-INF/services file, a YAML/Properties file can list implementation class names. The following utility loads JARs from a designated lib directory, creates instances via reflection, and invokes sendMsg:

public class ServiceLoaderUtils {
    public static void loadJarsFromAppFolder() throws Exception {
        String path = "E:\\code-self\\bitzpp\\lib";
        File dir = new File(path);
        for (File f : dir.listFiles()) {
            loadJarFile(f);
        }
    }
    private static void loadJarFile(File jar) throws Exception {
        URL url = jar.toURI().toURL();
        URLClassLoader cl = (URLClassLoader) ClassLoader.getSystemClassLoader();
        // reflection to invoke a method in the loaded JAR
    }
}

A test controller calls the utility and returns the result:

@RestController
public class SendMsgController {
    @GetMapping("/sendMsgV2")
    public String index() throws Exception {
        return ServiceLoaderUtils.doExecuteMethod();
    }
}

5. Spring Boot SPI via spring.factories

Spring Boot adds its own SPI layer. Files placed under resources/META-INF/spring.factories map an interface to one or more implementation class names. The core loader SpringFactoriesLoader provides two methods: loadFactories(Class, ClassLoader) – returns instantiated objects. loadFactoryNames(Class, ClassLoader) – returns class name strings.

Example interface and two implementations:

public interface SmsPlugin {
    void sendMessage(String message);
}

public class BizSmsImpl implements SmsPlugin {
    @Override
    public void sendMessage(String message) {
        System.out.println("this is BizSmsImpl sendMessage..." + message);
    }
}

public class SystemSmsImpl implements SmsPlugin {
    @Override
    public void sendMessage(String message) {
        System.out.println("this is SystemSmsImpl sendMessage..." + message);
    }
}

Register them:

com.example.SmsPlugin=\
com.example.impl.SystemSmsImpl,\
com.example.impl.BizSmsImpl

Controller uses the loader:

@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";
}
Spring Factories demo
Spring Factories demo

6. End‑to‑End Case Study: SMS Sending with Multiple Modules

The project is split into three Maven modules:

biz‑pp – defines MessagePlugin and publishes the JAR.

bitpt – Aliyun implementation ( BitptImpl).

miz‑pt – Tencent implementation ( MizptImpl).

Each implementation provides a sendMsg method that logs the user ID and type, then returns a success string.

public class BitptImpl implements MessagePlugin {
    @Override
    public String sendMsg(Map msgMap) {
        Object userId = msgMap.get("userId");
        Object type = msgMap.get("_type");
        System.out.println(" ==== userId :" + userId + ",type :" + type);
        System.out.println("aliyun send message success");
        return "aliyun send message success";
    }
}

Each module also includes a SPI registration file (e.g., resources/com.congge.spi.BitptImpl containing the fully‑qualified class name).

The consuming service ( biz‑pp) loads plugins via ServiceLoader, selects the target based on a configuration property ( msg.type=aliyun or tencent), and falls back to a default implementation when no plugin is found:

public class PluginFactory {
    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 Spring Boot application declares dependencies on the two implementation JARs and injects the selected plugin in SmsService:

@Service
public class SmsService {
    @Value("${msg.type}")
    private String msgType;
    public String sendMsg(String msg) {
        MessagePlugin plugin = PluginFactory.getTargetPlugin(msgType);
        Map<String, Object> params = new HashMap<>();
        return (plugin != null) ? plugin.sendMsg(params) : "default success";
    }
}

Running the application and calling http://localhost:8087/sendMsg?msg=hello prints the chosen implementation’s log and returns the corresponding success message, demonstrating how plugin configuration drives runtime behavior.

SMS endpoint output
SMS endpoint output

7. Takeaway

Plugin mechanisms—whether Java SPI, Spring’s spring.factories, or custom reflection‑based loaders—are now pervasive across frameworks and middleware. Mastering these patterns enables developers to build loosely‑coupled, extensible systems and to swap implementations at runtime without code changes, a skill increasingly essential for modern backend architecture.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

plugin architectureSpring BootJava SPISpring Factories
Java Companion
Written by

Java Companion

A highly professional Java public account

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.