SpringBoot3 动态扩展实战:不重启服务,轻松插拔业务模块

0 阅读7分钟

做后端开发久了,总会遇到这类痛点:业务要新增功能、老模块要迭代优化,改完代码就得重启服务,线上环境停服运维直接影响用户体验;想做模块化解耦、灵活扩展,又不想引入复杂度高、运维成本大的微服务架构,轻量化插件化方案就成了刚需。

这篇文章全程基于 SpringBoot3.2.5 + PF4J 3. 15 .0 实战落地,主打无侵入、热插拔的插件化热加载能力,不用重启服务、不用改动核心业务代码,只需把编译好的Jar包丢进指定目录,就能快速扩展业务功能,代码可直接复用、部署即用。

一、先理清标准项目结构

采用父工程+API模块+主应用+独立插件的分层模式,彻底解耦核心业务与扩展业务,插件和主应用互不侵入,打包部署更省心,目录结构如下:

# 根项目:plugin-parent(Maven父工程,统一版本管理)
├── pom.xml  # 全局依赖版本管控
├── plugin-api  # 扩展点API模块(主应用+插件共用契约)
│   ├── src/main/java
│   │   └── com/demo/api
│   │       └── MessageHandler.java  # 核心扩展点接口
│   └── pom.xml
├── plugin-boot  # SpringBoot3主应用(启动入口+插件集成)
│   ├── src/main/java
│   │   └── com/demo/boot
│   │       ├── PluginDemoApplication.java  # 启动类
│   │       ├── config
│   │       │   └── PluginConfig.java  # 插件管理器配置
│   │       └── controller
│   │           └── PluginController.java  # 插件调用接口
│   ├── src/main/resources
│   │   ├── application.yml  # 主配置文件
│   │   └── plugins  # 开发阶段插件存放目录
│   └── pom.xml
└── plugin-sms  # 独立业务插件(可复制多个,实现不同功能)
    ├── src/main/java
    │   └── com/demo/plugin
    │       ├── SmsPlugin.java  # 插件生命周期管理类
    │       └── SmsMessageHandler.java  # 扩展点实现类
    ├── src/main/resources
    │   └── plugin.properties  # 插件元数据配置(resources根目录,无嵌套)
    └── pom.xml

二、各模块完整pom.xml

所有配置针对 SpringBoot3,解决依赖冲突、日志冲突、打包适配问题,无需额外修改,重点优化插件模块打包规则,确保配置文件正常打入Jar包。

1. 父工程 plugin-parent pom.xml

统一管理所有模块版本,锁定 SpringBoot、PF4J 依赖,避免子模块版本混乱:

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <!-- 父工程坐标 -->
    <groupId>com.demo</groupId>
    <artifactId>plugin-parent</artifactId>
    <version>1.0.0</version>
    <packaging>pom</packaging>
    <name>plugin-parent</name>
    <description>SpringBoot3插件化父工程,统一版本管理</description>

    <!-- 子模块声明 -->
    <modules>
        <module>plugin-api</module>
        <module>plugin-boot</module>
        <module>plugin-sms</module>
    </modules>

    <!-- 统一版本属性 -->
    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <spring.boot.version>3.2.5</spring.boot.version>
        <pf4j.version>3.15.0</pf4j.version>
        <pf4j.spring.version>0.10.0</pf4j.spring.version>
    </properties>

    <!-- 依赖版本管控,子模块按需引入 -->
    <dependencyManagement>
        <dependencies>
            <!-- SpringBoot3 依赖管理 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring.boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!-- PF4J 核心框架 -->
            <dependency>
                <groupId>org.pf4j</groupId>
                <artifactId>pf4j</artifactId>
                <version>${pf4j.version}</version>
            </dependency>
            <!-- PF4J-Spring 集成包 -->
            <dependency>
                <groupId>org.pf4j</groupId>
                <artifactId>pf4j-spring</artifactId>
                <version>${pf4j.spring.version}</version>
            </dependency>
            <!-- 自定义扩展点API模块 -->
            <dependency>
                <groupId>com.demo</groupId>
                <artifactId>plugin-api</artifactId>
                <version>${project.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

2. API模块 plugin-api 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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>com.demo</groupId>
        <artifactId>plugin-parent</artifactId>
        <version>1.0.0</version>
    </parent>

    <modelVersion>4.0.0</modelVersion>
    <artifactId>plugin-api</artifactId>
    <name>plugin-api</name>
    <description>插件扩展点API模块,定义契约接口</description>
    <packaging>jar</packaging>

    <dependencies>
        <!-- 仅引入PF4J扩展点标记,作用域为provided -->
        <dependency>
            <groupId>org.pf4j</groupId>
            <artifactId>pf4j</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <!-- 编译配置 -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.11.0</version>
                <configuration>
                    <source>17</source>
                    <target>17</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

3. 主应用模块 plugin-boot pom.xml

SpringBoot 启动模块,集成插件管理器,提供 Web 调用接口,打包为可执行 Jar:

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>com.demo</groupId>
        <artifactId>plugin-parent</artifactId>
        <version>1.0.0</version>
    </parent>

    <modelVersion>4.0.0</modelVersion>
    <artifactId>plugin-boot</artifactId>
    <name>plugin-boot</name>
    <description>SpringBoot3主应用,插件化核心集成模块</description>
    <packaging>jar</packaging>

    <dependencies>
        <!-- SpringBoot3 Web 核心依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
            <version>4.5.0</version>
        </dependency>

        <!-- 引入扩展点API -->
        <dependency>
            <groupId>com.demo</groupId>
            <artifactId>plugin-api</artifactId>
        </dependency>
        <!-- PF4J 核心依赖 -->
        <dependency>
            <groupId>org.pf4j</groupId>
            <artifactId>pf4j</artifactId>
        </dependency>
        <!-- PF4J-Spring 集成,排除日志冲突 -->
        <dependency>
            <groupId>org.pf4j</groupId>
            <artifactId>pf4j-spring</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.logging.log4j</groupId>
                    <artifactId>log4j-api</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- 测试依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <!-- SpringBoot 打包插件 -->
    <build>
        <finalName>plugin-boot</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring.boot.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

4. 插件模块 plugin-sms pom.xml(修复报错核心)

独立业务插件,严禁引入 SpringBoot 全家桶,仅依赖 API 和 PF4J,新增资源拷贝配置,确保 plugin.properties 打入Jar根目录,彻底解决 manifest 找不到问题:

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>com.demo</groupId>
        <artifactId>plugin-parent</artifactId>
        <version>1.0.0</version>
    </parent>

    <modelVersion>4.0.0</modelVersion>
    <artifactId>plugin-sms</artifactId>
    <name>plugin-sms</name>
    <description>短信通知插件模块,实现消息扩展点</description>
    <packaging>jar</packaging>

    <dependencies>
        <!-- 扩展点API,provided避免类冲突 -->
        <dependency>
            <groupId>com.demo</groupId>
            <artifactId>plugin-api</artifactId>
            <scope>provided</scope>
        </dependency>
        <!-- PF4J核心依赖,provided避免重复引入 -->
        <dependency>
            <groupId>org.pf4j</groupId>
            <artifactId>pf4j</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <!-- 插件打包配置 -->
    <build>
        <finalName>sms-message-plugin</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.11.0</version>
                <configuration>
                    <source>17</source>
                    <target>17</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <version>3.3.1</version>
                <configuration>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

三、主应用基础配置

1. application.yml 配置

仅需指定插件存放路径:

server:
  port: 8080

spring:
  application:
    name: plugin-demo

plugin:
  path: classpath:/plugins/

手动创建 plugins 文件夹,后续插件 Jar 包直接放入该目录,路径错误会导致PF4J无法扫描插件。

2. 插件管理器配置

核心配置类,初始化插件管理器,绑定插件路径,交由Spring容器管理:

import org.pf4j.PluginManager;
import org.pf4j.spring.SpringPluginManager;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import java.nio.file.Path;
import java.nio.file.Paths;

@Configuration
public class PluginConfig {

    @Value("${plugin.path}")
    private String pluginPath;

    @Bean
    public PluginManager pluginManager() throws Exception {
        if (pluginPath.startsWith("classpath:")) {
            PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
            Resource[] resources = resolver.getResources(pluginPath + "/*.jar");

            if (resources.length > 0) {
                return new SpringPluginManager(resources[0].getFile().getParentFile().toPath());
            }
            throw new RuntimeException("No plugins found in classpath: " + pluginPath);
        } else {
            Path path = Paths.get(pluginPath);
            return new SpringPluginManager(path);
        }
    }
}

四、定义扩展点契约(核心规范)

插件化的核心是契约先行,主应用和插件通过接口约定功能,避免硬编码耦合,所有插件必须遵循统一规范。

扩展点接口(plugin-api模块)

必须继承 ExtensionPoint,这是PF4J识别扩展点的核心标记,无此标记无法加载:

import org.pf4j.ExtensionPoint;

/**
 * 消息处理扩展点,所有业务插件必须实现该接口
 */
public interface MessageHandler extends ExtensionPoint {

    /**
     * 插件唯一标识,用于区分不同插件
     */
    String getPluginId();

    /**
     * 业务处理方法
     * @param content 业务入参
     * @return 处理结果
     */
    String handleMessage(String content);
}

五、插件开发实战

1. 实现扩展点(插件模块)

@Extension 注解标记实现类,PF4J启动时会自动扫描加载该扩展实现:

import org.pf4j.Extension;
import com.demo.api.MessageHandler;

/**
 * 短信通知插件实现,遵循消息扩展点契约
 */
@Extension
public class SmsMessageHandler implements MessageHandler {

    @Override
    public String getPluginId() {
        // 与plugin.properties中plugin.id保持一致
        return "sms-plugin";
    }

    @Override
    public String handleMessage(String content) {
        // 真实业务可接入第三方短信SDK、参数校验、日志埋点、异常捕获
        return String.format("【短信插件】处理消息:%s,发送成功", content);
    }
}

2. 插件元数据配置

在插件 src/main/resources 根目录 下创建 plugin.properties,PF4J靠该文件识别插件信息,必填项不可缺失:


# 插件生命周期全限定类名(必填,与自定义Plugin类路径一致)
plugin.class=com.demo.plugin.SmsPlugin
# 插件唯一ID(必填,不可重复)
plugin.id=sms-plugin
# 插件版本号(必填)
plugin.version=0.0.1
plugin.requires=1.0.0
# 插件依赖(无依赖留空,必填)
plugin.dependencies=
# 插件描述(必填)
plugin.description=My example plugin
# 插件开发者/团队(选填)
plugin.provider=maluxinghe
plugin.license=Apache License 2.0

3. 插件生命周期类

用于插件启动、停止时的资源初始化与释放,管控插件生命周期,属于必填配置:

import org.pf4j.Plugin;
import org.pf4j.PluginWrapper;

public class SmsPlugin extends Plugin {

    public SmsPlugin(PluginWrapper wrapper) {
        super(wrapper);
    }

    // 插件启动时执行:初始化连接、加载配置、预热资源
    @Override
    public void start() {
        System.out.println("=== 短信插件启动成功 ===");
    }

    // 插件停止时执行:关闭连接、释放资源、清理缓存
    @Override
    public void stop() {
        System.out.println("=== 短信插件已停止,资源释放完毕 ===");
    }
}

插件开发完成后,执行 mvn clean package 打包,将target目录下生成的 Jar 包,复制到主应用的 plugins 目录。

六、主应用调用插件

通过插件管理器动态获取插件实现,无需硬编码,支持批量调用和指定插件ID调用,适配不同业务场景:

import com.demo.api.MessageHandler;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.pf4j.PluginManager;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/plugin")
public class PluginController {

    @Resource
    private PluginManager pluginManager;

    /**
     * 调用所有插件处理消息
     */
    @GetMapping("/invoke")
    public List<String> invokeAllPlugin(@RequestParam("content") String content) {
        // 动态获取所有扩展点实现
        List<MessageHandler> handlers = pluginManager.getExtensions(MessageHandler.class);
        return handlers.stream()
                .map(handler -> handler.handleMessage(content))
                .collect(Collectors.toList());
    }

    /**
     * 指定插件调用
     */
    @GetMapping("/invoke/single")
    public String invokeSinglePlugin(@RequestParam("content") String content, @RequestParam("pluginId") String pluginId) {
        List<MessageHandler> handlers = pluginManager.getExtensions(MessageHandler.class);
        return handlers.stream()
                .filter(handler -> pluginId.equals(handler.getPluginId()))
                .findFirst()
                .map(handler -> handler.handleMessage(content))
                .orElse("插件不存在或未加载");
    }

    /**
     * 加载所有插件
     */
    @GetMapping("/load")
    public String loadPlugins() {
        pluginManager.loadPlugins();
        return "插件加载成功,共加载 " + pluginManager.getPlugins().size() + " 个插件";
    }

    /**
     * 启动所有已加载的插件
     */
    @GetMapping("/start")
    public String startPlugins() {
        pluginManager.startPlugins();
        return "插件启动成功,已启动 " + pluginManager.getStartedPlugins().size() + " 个插件";
    }

    /**
     * 停止所有插件
     */
    @GetMapping("/stop")
    public String stopPlugins() {
        pluginManager.stopPlugins();
        return "插件已停止";
    }

    /**
     * 获取所有已加载的插件信息
     */
    @GetMapping("/list")
    public List<String> listPlugins() {
        return pluginManager.getPlugins().stream()
                .map(plugin -> plugin.getDescriptor().getPluginId())
                .collect(Collectors.toList());
    }

    /**
     * 获取已启动的插件信息
     */
    @GetMapping("/list/started")
    public List<String> listStartedPlugins() {
        return pluginManager.getStartedPlugins().stream()
                .map(plugin -> plugin.getDescriptor().getPluginId())
                .collect(Collectors.toList());
    }


}

七、测试与热加载技巧

1. 启动测试

启动 SpringBoot 主应用,控制台打印插件启动日志,代表加载成功:

1.png

调用以下接口验证功能:

  • 批量调用:GET /plugin/invoke?content=测试动态插件

2.png

  • 指定调用:GET /plugin/invoke/single?content=测试动态插件&pluginId=sms-plugin

3.png

2. 插件热加载(不重启服务)

新增/更新/卸载插件时,无需重启主应用,执行以下步骤完成热插拔:

  1. 将新插件Jar包放入plugins目录,或删除旧Jar包

  2. 调用插件管理器方法完成加载/卸载

// 加载目录内新增的插件
pluginManager.loadPlugins();
// 启动所有未启动的插件
pluginManager.startPlugins();
// 卸载指定插件(按需使用,先停止再卸载)
// pluginManager.unloadPlugin("插件ID");

八、落地总结

这套SpringBoot3插件化方案轻量化、无额外中间件依赖,特别适合中小型项目、后台管理系统、定制化业务模块、灰度发布场景,既能实现模块解耦,又能规避微服务的部署复杂度、运维成本。

核心优势:热插拔不重启、模块独立部署、核心代码零侵入、扩展灵活,中小型项目直接复用这套代码即可落地;大型分布式项目可结合配置中心、插件权限管控、监控告警,进一步完善插件管理体系。

只要严格遵循文件路径、打包配置、契约规范这三大核心规则,就能彻底规避各类报错,实现稳定的插件化动态扩展。