插件化架構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
  1. 打包插件:
mvn clean package
  1. 部署運行:
  • 將插件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:物聯網設備管理平台

適用場景 :需要支持多種設備協議的物聯網平台

實現方式 :

  • 主程序:設備管理框架、數據存儲
  • 插件:各種設備協議解析器
  • 動態加載:根據設備類型加載相應協議插件

優勢 :

  • 新設備協議可以快速接入
  • 協議升級不影響現有設備
  • 協議模塊可以獨立優化