Backend Development 20 min read

Java Plugin Development: SPI, Spring Factories, and Custom Extension Mechanisms

This article explains the concept of plugin‑based development in Java, outlines its advantages such as decoupling and extensibility, and provides detailed implementation guides using ServiceLoader, custom configuration files, and Spring Boot's spring.factories mechanism with complete code examples.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Java Plugin Development: SPI, Spring Factories, and Custom Extension Mechanisms

Plugin‑oriented development is widely used in many programming languages and frameworks (e.g., Jenkins, Rancher, IDEs) to improve system extensibility, modularity, and third‑party integration. The article first discusses why plugins are beneficial, highlighting module decoupling, enhanced extensibility, and easy third‑party access.

For Java, several common plugin implementation ideas are presented:

SPI mechanism (Service Provider Interface)

Convention‑based configuration with reflection

Spring Boot's spring.factories extension point

Dynamic loading of external JARs via custom class loaders

1. ServiceLoader (SPI) example

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";
    }
}

// resources/META-INF/services/com.example.MessagePlugin
com.example.AliyunMsg
com.example.TencentMsg

public class Main {
    public static void main(String[] args) {
        ServiceLoader
loader = ServiceLoader.load(MessagePlugin.class);
        for (MessagePlugin plugin : loader) {
            plugin.sendMsg(new HashMap<>());
        }
    }
}

This code demonstrates loading multiple implementations at runtime and invoking them without compile‑time dependencies.

2. Custom configuration‑driven approach

# application.yml (or custom yaml)
server:
  port: 8081
impl:
  name: com.congge.plugins.spi.MessagePlugin
  clazz:
    - com.congge.plugins.impl.TencentMsg
    - com.congge.plugins.impl.AliyunMsg

// Configuration POJO
@Getter @Setter @ConfigurationProperties("impl")
public class ClassImpl {
    private String name;
    private String[] clazz;
}

@RestController
public class SendMsgController {
    @Autowired
    private ClassImpl classImpl;

    @GetMapping("/sendMsg")
    public String sendMsg() throws Exception {
        for (String className : classImpl.getClazz()) {
            Class
cls = Class.forName(className);
            MessagePlugin plugin = (MessagePlugin) cls.getDeclaredConstructor().newInstance();
            plugin.sendMsg(new HashMap<>());
        }
        return "success";
    }
}

This method reads a custom YAML file to determine which plugin classes to load, offering runtime flexibility.

3. Loading external JARs dynamically

public class ServiceLoaderUtils {
    public static void loadJarsFromAppFolder() throws Exception {
        String path = "E:/code-self/bitzpp/lib";
        File dir = new File(path);
        for (File file : dir.listFiles()) {
            if (file.isFile()) loadJarFile(file);
        }
    }

    public static void loadJarFile(File jar) throws Exception {
        URL url = jar.toURI().toURL();
        URLClassLoader cl = (URLClassLoader) ClassLoader.getSystemClassLoader();
        Method method = URLClassLoader.class.getMethod("addURL", URL.class);
        method.invoke(cl, url);
    }
}

// Example usage
String result = ServiceLoaderUtils.invokeMethod("hello");

The utility loads JAR files from a specified directory and uses reflection to invoke methods inside the loaded classes.

4. Spring Boot SPI via spring.factories

// resources/META-INF/spring.factories
com.congge.plugin.spi.SmsPlugin=\
com.congge.plugin.impl.SystemSmsImpl,\
com.congge.plugin.impl.BizSmsImpl

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

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

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

@RestController
public class SmsController {
    @GetMapping("/sendMsgV3")
    public String sendMsgV3(String msg) {
        List
plugins = SpringFactoriesLoader.loadFactories(SmsPlugin.class, null);
        for (SmsPlugin p : plugins) {
            p.sendMessage(msg);
        }
        return "success";
    }
}

SpringBoot automatically discovers implementations listed in spring.factories , allowing seamless plugin integration.

5. Full case study

The article walks through a realistic scenario where three micro‑services share a common plugin interface for SMS sending. It shows how to define the interface, package implementations as separate JARs, configure them via SPI or Spring factories, and select a concrete plugin at runtime based on configuration parameters.

Finally, the author emphasizes that plugin mechanisms are pervasive across languages and frameworks, and mastering them is valuable for both development and architectural design.

JavamicroservicesPlugin ArchitectureSpring Bootdependency injectionSPIServiceLoader
Code Ape Tech Column
Written by

Code Ape Tech Column

Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn

0 followers
Reader feedback

How this landed with the community

login 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.