插件化架构spring-brick,从入门到奔跑

前言

在当今快速迭代的软件开发环境中,系统的灵活性和可扩展性越来越受到重视。传统的单体应用架构在面对频繁功能更新时,往往需要整体重新部署,这不仅影响服务稳定性,还降低了开发效率。本文将向您介绍一款国内开源的插件化开发框架——spring-brick,它为Spring Boot应用提供了优雅的插件化解决方案,让我们的系统像搭积木一样灵活可扩展。

1、spring-brick是什么?

spring-brick是一个基于Spring Boot的插件开发框架它允许将功能模块作为插件动态加载到主应用程序中,为Spring Boot应用开发功能插件。其核心思想是"组件即砖,组合即应用"。插件和Spring Boot应用的关系,就类似VS Code中的插件之于VS Code一样。

使用spring-brick开发插件,好比开发一个小型的Spring Boot应用。本质上spring-brick也是一种组织代码的方式,将相同功能或业务的代码内聚在一起做成一个插件。插件的生命周期是独立的,支持独立开发、测试、版本管理。

值得一提的是,spring-brick并非Spring官方的项目,而是国内的一个程序员开发的,在Gitee上开源。

2、spring-brick的运行原理

2.1 架构设计

spring-brick采用分层架构设计,主要包含以下核心组件:

插件容器
├─ 插件1 (Plugin1)  // 可扩展的插件实例
│  ├─ 独立类加载器  // 插件隔离的核心组件
│  │  └─ 插件业务逻辑  // 插件自身的功能实现

主程序 (Main App)
├─ Spring Boot 容器
│  ├─ spring-brick 核心
│  │  └─ 插件管理器  // 核心组件,负责管理插件

主程序通过插件管理器与插件容器进行对接,实现了主程序与插件的解耦。

2.2 核心特性

2.2.1 类加载器隔离

spring-brick的一个核心特性就是类加载器隔离,即主程序和每个插件都有一个自己的独立的类加载器,每个类加载器只加载自己模块的代码互不影响,这样可以避免各插件之间的类冲突。

由于插件的pom中通常会依赖主程序,所以插件可以直接访问主程序中的类,但主程序不能直接使用插件中的类。这种设计保证了插件的独立性和安全性。

2.2.2 类的动态加载

spring-brick采用双亲委派机制的变种来加载类,即优先使用主程序加载的类,没有再使用当前插件中加载的类。这种机制确保了类的正确加载顺序,同时避免了类重复加载的问题。

2.2.3 通信机制

主程序中调用插件,可以使用spring-brick的扩展点机制或HTTP接口调用,其他通信方式如消息队列、三方存储等也可以灵活使用。

2.3 启动流程

2.3.1 主程序启动流程
  1. SpringMainBootstrap.launch() - 启动spring-brick框架
  2. 扫描插件路径 - 根据配置扫描插件目录
  3. 加载插件 - 为每个插件创建独立的类加载器
  4. 初始化插件 - 调用插件的main方法
  5. 集成到主程序 - 将插件Bean集成到主程序Spring容器
2.3.2 插件启动流程
  1. 继承SpringPluginBootstrap - 插件入口类
  2. 独立Spring容器 - 每个插件有独立的Spring容器
  3. Bean注册 - 插件Bean通过扩展点机制注册到主程序

3、为什么要用spring-brick?

3.1 解决的主要问题

  1. 动态功能更新:在不重启主程序的情况下,动态地给主程序添加、减少、更新功能。

  2. 避免服务拆分过度:对于中小型功能场景,当不想要引入额外的微服务所带来的复杂时,就可以使用插件。大型复杂场景,建议使用微服务。

  3. 插件化架构:同微服务思想一样,以插件化架构思想,帮助系统实现高内聚、低耦合、可扩展的特点

  4. 开发效率提升:插件可以并行开发,独立测试,大大提高了团队协作效率。

3.2 适用场景

  1. 功能模块化需求强的系统:如内容管理系统、企业管理平台等,需要根据不同客户需求动态加载不同功能模块。

  2. 业务快速迭代的场景:市场需求变化快,需要频繁更新功能而不影响系统整体稳定性。

  3. 多租户系统:不同租户可能需要不同的功能模块,通过插件机制可以按需加载。

  4. 工具型应用:需要提供丰富的扩展功能,如开发工具、监控平台等。

3.3 与微服务的对比

特性 spring-brick插件化 微服务
部署单元 单个应用+多个插件 多个独立服务
开发门槛 较低,类似Spring Boot开发 较高,需要考虑服务拆分、通信等
性能 高,进程内调用,共享JVM 较低,网络开销
适用规模 中小型功能模块 大型复杂业务模块
启动速度 较快 较慢
事务处理 简单,遵守单体ACID事务 较复杂,分布式事务
通信方式 进程内方法调用 网络通信(RPC/HTTP)

选择spring-brick当 :

  • 业务模块耦合度较高
  • 需要强一致性事务
  • 性能要求高,延迟敏感
  • 团队规模较小,技术栈统一
  • 需要快速迭代和热部署

选择传统微服务当 :

  • 业务模块完全独立
  • 团队技术栈多样
  • 需要不同语言开发
  • 系统规模非常大
  • 需要独立的伸缩能力

4、如何使用spring-brick?

spring-brick的使用步骤分为三步:组件开发、组件集成、应用运行。下面我们将详细介绍每个步骤。

4.1 组件开发

4.1.1 项目结构设置
  1. 按照官网推荐的开发目录结构创建项目(其实只要插件的路径能被正确扫描到,分开的两个项目也行)
spring-brick-demo/
├── example-main/       # 主程序模块
│   ├── pom.xml
│   └── src/main/
├── plugins-basic/      # 基础插件模块
│   ├── pom.xml
│   └── src/main/
└── pom.xml            # 父项目pom
4.1.2 主程序开发

1、在主程序pom.xml中添加spring-brick依赖:

<dependency>
  <groupId>com.gitee.starblues</groupId>
  <artifactId>spring-brick</artifactId>
  <version>3.1.0</version>
</dependency>

2、改造主程序springBoot入口类,继承SpringBootstrap

@SpringBootApplication
public class Application implements SpringBootstrap {

    public static void main(String[] args) {
        SpringMainBootstrap.launch(Application.class, args);
    }

    @Override
    public void run(String[] args) throws Exception {
        SpringApplication.run(Application.class, args);
    }
}
  1. 加入配置插件

在application.yml中添加配置:

plugin:
  runMode: dev
  mainPackage: com.gitee.starblues.example
  pluginPath:
     #- D://project//plugins(替换为自己环境下)
     - ~\plugins-basic  #插件目录或插件上级目录
  1. 打包主程序

使用 mvn clean install 命令进行打包

4.1.3 插件开发

1、在插件的pom中加入依赖和定制的maven打包插件:

<!-- spring-boot-starter依赖 -->
<!--建议将spring-boot-starter依赖放到第一个位置, 以防止出现依赖冲突导致无法启动插件-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter</artifactId>
  <version>${和主程序一致的springboot版本}</version>
</dependency>
<!--插件不包含spring-boot-starter-web依赖,如果在主程序的pom.xml中已经定义了这个依赖,需要在插件的pom.xml中排除这个依赖。-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<!-- spring-brick-bootstrap依赖 -->
<dependency>
  <groupId>com.gitee.starblues</groupId>
  <artifactId>spring-brick-bootstrap</artifactId>
  <version>3.1.0</version>
</dependency>

<!-- 主程序依赖:将主程序以 provided 方式依赖到插件中,确保只编译不打包 -->
<dependency>
  <groupId>主程序的 groupId</groupId>
  <artifactId>主程序的 artifactId</artifactId>
  <version>主程序 version</version>
  <scope>provided</scope>
</dependency>



<build>
  <plugins>
    <plugin>
      <groupId>com.gitee.starblues</groupId>
      <artifactId>spring-brick-maven-packager</artifactId>
      <version>3.1.0</version>
      <configuration>
        <!--当前打包模式为: 开发模式-->
        <mode>dev</mode>
        <!--插件信息定义-->
        <pluginInfo>
          <!--插件id-->
          <id>plugin-example</id>
          <!--插件入口类, 定义说明见: 定义插件入口类-->
          <bootstrapClass>com.gitee.starblues.example.ExamplePlugin</bootstrapClass>
          <!--插件版本号-->
          <version>1.0.0</version>
        </pluginInfo>
      </configuration>
      <executions>
        <execution>
          <goals>
            <goal>repackage</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>
  1. 创建插件入口类,继承SpringPluginBootstrap
@SpringBootApplication
public class ExamplePlugin extends SpringPluginBootstrap {
    public static void main(String[] args) {
        new ExamplePlugin().run(args);
    }
}
  1. 实现主程序定义的扩展点接口
@Component
public class ExamplePluginExtension implements PluginExtensionPoint {
    @Override
    public String getName() {
        return "example-plugin";
    }
    
    @Override
    public String doSomething(String param) {
        return "Plugin processed: " + param;
    }
}

4、插件 测试controller

@RestController
@RequestMapping("/example")
public class ExampleController {
    @GetMapping
    public String hello(){
        return "hello";
    }
}

5、使用maven命令编译插件项目

mvn clean compile

6、启动主程序验证
启动主程序后,主程序中的插件管理器会根据配置的插件路径,到指定的位置去加载插件的类。看到如下信息说明插件加载成功:

c.g.s.i.operator.DefaultPluginOperator   : 插件加载环境: dev
c.g.s.core.PluginLauncherManager         : 插件[plugin-example@1.0.0-SNAPSHOT]加载成功
c.g.s.b.p.web.PluginControllerProcessor  : 插件[plugin-example]注册接口: {GET [/plugins/module1/example]}
c.g.s.core.PluginLauncherManager         : 插件[plugin-example@1.0.0-SNAPSHOT]启动成功
c.g.s.i.operator.DefaultPluginOperator   : 插件初始化完成

7、访问接口
访问接口 http://127.0.0.1:8080/plugins/plugins-basic/example ,查看插件访问结果

4.1.4、关于主程序和插件的开发
  • 把主程序和插件当成正常springBoot应用开发即可。由于类加载器隔离的原因,他俩是独立的
  • 主程序的配置在主程序和插件之间是共享的,在插件中获取主程序的配置 通过spring-brick 提供的API 获取,参看:https://www.yuque.com/starblues/spring-brick-3.0.0/bhr0wo ;获取自身配置时还是正常获取。
  • 插件尽量做到职责单一、功能内聚,不要太复杂
4.1.5、主程序和插件之间的通信

4.2 组件集成

1、 插件调用主程序

由于插件依赖了主程序,所以插件中可以正常使用访问和注入主程序中的bean

2、 主程序调用插件

原则上来说,主程序不应该过多的与插件交互,不然就耦合太严重。
如果想调用插件的话,可以使用spring-brick提供的扩展点机制 或者 http接口调用。

以下是扩展点机制示例:
可以根据不同的bus 参数调用不同的插件实现。

@RestController
@RequestMapping("/main/extract")
public class ExtracTestController {

    @Autowired
    private ExtractFactory extractFactory;

    /**
     * 获取指定的扩展插件实现
     * @param bus 业务标识
     * @return 返回用户列表
     */
    @GetMapping("/getExtractByCoordinate2")
    public List<UserDTO> getExtractByCoordinate2(@RequestParam("bus") String bus){
        UserInterface userInterface = extractFactory.getExtractByCoordinate(ExtractCoordinate.build(bus, null, null));
        List<UserDTO> userList = userInterface.getUserList();
        
        return userList;
    }
}

// 主程序中定义的扩展接口,让自身或不同的插件去实现
public interface UserInterface {

    List<UserDTO> getUserList();
}

// 插件中的实现
@Extract(bus = "basicUserInterfaceImpl")
public class UserInterfaceImpl implements UserInterface {

    @Autowired
    private UserMapper userMapper;
    
    @Override
    public List<UserDTO> getUserList() {
        List<User> userList = userMapper.selectList(null);

        return JSON.parseArray(JSON.toJSONString(userList), UserDTO.class);
    }
}

4.3 应用运行

4.3.1 打包与部署
  1. 打包主程序:

    mvn clean package
    
  2. 打包插件:

    mvn clean package
    
  3. 部署运行:

    • 将插件jar包复制到配置的插件目录
    • 启动主程序:java -jar example-main.jar
4.3.2 动态管理插件

spring-brick提供了插件的动态加载、卸载、更新等功能:(只适用于prod环境)

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

    @Autowired
    private PluginOperator pluginOperator;

    /**
     * 上传并安装、启动插件。注意: 该操作只适用于生产环境
     * @param multipartFile 上传文件 multipartFile
     * @return 操作结果
     */
    @PostMapping("/upload")
    public String upload(@RequestParam("jarFile") MultipartFile multipartFile){
        try {
            UploadParam uploadParam = UploadParam.byMultipartFile(multipartFile)
                    .setBackOldPlugin(true)
                    .setStartPlugin(true)
                    .setUnpackPlugin(false);
            PluginInfo pluginInfo = pluginOperator.uploadPlugin(uploadParam);
            if(pluginInfo != null){
                return "install success";
            } else {
                return "install failure";
            }
        } catch (Exception e) {
            e.printStackTrace();
            return "install failure : " + e.getMessage();
        }
    }

    /**
     * 获取插件信息
     * @return 返回插件信息
     */
    @GetMapping("/infoList")
    public List<PluginInfo> getPluginInfo(){
        return pluginOperator.getPluginInfo();
    }

    /**
     * 根据插件路径安装插件。该插件jar必须在服务器上存在。注意: 该操作只适用于生产环境
     * @param path 插件路径名称
     * @return 操作结果
     */
    @PostMapping("/installByPath")
    public String install(@RequestParam("path") String path,
                          @RequestParam(value = "unpackPlugin", defaultValue = "false", required = false) Boolean unpackPlugin){
        try {
            PluginInfo pluginInfo = pluginOperator.install(Paths.get(path), unpackPlugin);
            if(pluginInfo != null){
                return "installByPath success";
            } else {
                return "installByPath failure";
            }
        } catch (Exception e) {
            e.printStackTrace();
            return "installByPath failure : " + e.getMessage();
        }
    }

    /**
     * 根据插件id停止插件
     * @param id 插件id
     * @return 返回操作结果
     */
    @PostMapping("/stop/{id}")
    public String stop(@PathVariable("id") String id){
        try {
            if(pluginOperator.stop(id)){
                return "plugin '" + id +"' stop success";
            } else {
                return "plugin '" + id +"' stop failure";
            }
        } catch (Exception e) {
            e.printStackTrace();
            return "plugin '" + id +"' stop failure. " + e.getMessage();
        }
    }

    /**
     * 根据插件id启动插件
     * @param id 插件id
     * @return 返回操作结果
     */
    @PostMapping("/start/{id}")
    public String start(@PathVariable("id") String id){
        try {
            if(pluginOperator.start(id)){
                return "plugin '" + id +"' start success";
            } else {
                return "plugin '" + id +"' start failure";
            }
        } catch (Exception e) {
            e.printStackTrace();
            return "plugin '" + id +"' start failure. " + e.getMessage();
        }
    }


    /**
     * 根据插件id卸载插件
     * @param id 插件id
     * @return 返回操作结果
     */
    @PostMapping("/uninstall/{id}")
    public String uninstall(@PathVariable("id") String id){
        try {
            pluginOperator.uninstall(id, true, true);
            return "plugin '" + id +"' uninstall success";
        } catch (Exception e) {
            e.printStackTrace();
            return "plugin '" + id +"' uninstall failure. " + e.getMessage();
        }
    }

}

5、企业应用建议

5.1 开发最佳实践

1、插件设计原则:每个插件应该只负责一个明确的业务功能,避免功能过于复杂。

  1. 接口设计:扩展点接口设计要稳定,避免频繁变更影响插件兼容性。

  2. 版本管理:插件和主程序的版本要做好管理,确保兼容性。

  3. 文档完善:为扩展点提供详细的文档,方便插件开发者使用。

5.2 常见使用场景:

场景1:模块化业务系统

适用场景 :大型企业ERP、CRM、OA系统等需要模块化扩展的系统
实现方式 :

  • 主程序:核心框架、用户管理、权限控制
  • 插件1:销售管理模块
  • 插件2:采购管理模块
  • 插件3:库存管理模块
  • 插件4:财务管理模块

优势 :

  • 各业务模块独立开发、测试、部署
  • 可以根据客户需求选择安装特定模块
  • 模块升级不影响其他功能
场景2:多租户SaaS平台

适用场景 :为不同客户提供定制化功能的SaaS平台
实现方式 :

  • 主程序:基础平台、租户管理
  • 插件:为不同客户定制的功能模块
  • 动态加载:根据租户配置加载相应插件

优势 :

  • 不同客户可以使用不同的功能组合
  • 新功能可以作为插件快速上线
  • 客户定制功能隔离,互不影响
场景3:功能热部署

适用场景 :需要7x24小时运行但又要频繁更新功能的系统
实现方式 :

  • 主程序:稳定运行的核心服务
  • 插件:需要频繁更新的业务功能
  • 热部署:不停机更新插件

优势 :

  • 避免系统重启带来的服务中断
  • 快速响应业务变化
  • 降低部署风险
场景4:金融行业风控系统

适用场景 :需要灵活配置风控规则的系统

实现方式 :

  • 主程序:基础风控框架、数据采集

  • 插件:各种风控规则引擎

  • 动态组合:根据业务场景加载不同规则组合
    优势 :

  • 风控规则可以快速迭代

  • 不同业务线可以使用不同的风控策略

  • 规则更新不影响系统稳定性

场景5:物联网设备管理平台

适用场景 :需要支持多种设备协议的物联网平台

实现方式 :

  • 主程序:设备管理框架、数据存储
  • 插件:各种设备协议解析器
  • 动态加载:根据设备类型加载相应协议插件

优势 :

  • 新设备协议可以快速接入
  • 协议升级不影响现有设备
  • 协议模块可以独立优化

参考资料:
官方文档:https://www.yuque.com/starblues/spring-brick-3.0.0/ypurcw

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐