插件化架構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 主程序啓動流程
- SpringMainBootstrap.launch() - 啓動spring-brick框架
- 掃描插件路徑 - 根據配置掃描插件目錄
- 加載插件 - 為每個插件創建獨立的類加載器
- 初始化插件 - 調用插件的main方法
- 集成到主程序 - 將插件Bean集成到主程序Spring容器
2.3.2 插件啓動流程
- 繼承SpringPluginBootstrap - 插件入口類
- 獨立Spring容器 - 每個插件有獨立的Spring容器
- Bean註冊 - 插件Bean通過擴展點機制註冊到主程序
3、為什麼要用spring-brick?
3.1 解決的主要問題
- 動態功能更新:在不重啓主程序的情況下,動態地給主程序添加、減少、更新功能。
- 避免服務拆分過度:對於中小型功能場景,當不想要引入額外的微服務所帶來的複雜時,就可以使用插件。大型複雜場景,建議使用微服務。
- 插件化架構:同微服務思想一樣,以插件化架構思想,幫助系統實現高內聚、低耦合、可擴展的特點
- 開發效率提升:插件可以並行開發,獨立測試,大大提高了團隊協作效率。
3.2 適用場景
- 功能模塊化需求強的系統:如內容管理系統、企業管理平台等,需要根據不同客户需求動態加載不同功能模塊。
- 業務快速迭代的場景:市場需求變化快,需要頻繁更新功能而不影響系統整體穩定性。
- 多租户系統:不同租户可能需要不同的功能模塊,通過插件機制可以按需加載。
- 工具型應用:需要提供豐富的擴展功能,如開發工具、監控平台等。
3.3 與微服務的對比
|
特性
|
spring-brick插件化
|
微服務
|
|
部署單元
|
單個應用+多個插件
|
多個獨立服務
|
|
開發門檻
|
較低,類似Spring Boot開發
|
較高,需要考慮服務拆分、通信等
|
|
性能
|
高,進程內調用,共享JVM
|
較低,網絡開銷
|
|
適用規模
|
中小型功能模塊
|
大型複雜業務模塊
|
|
啓動速度
|
較快
|
較慢
|
|
事務處理
|
簡單,遵守單體ACID事務
|
較複雜,分佈式事務
|
|
通信方式
|
進程內方法調用
|
網絡通信(RPC/HTTP)
|
選擇spring-brick當 :
- 業務模塊耦合度較高
- 需要強一致性事務
- 性能要求高,延遲敏感
- 團隊規模較小,技術棧統一
- 需要快速迭代和熱部署
選擇傳統微服務當 :
- 業務模塊完全獨立
- 團隊技術棧多樣
- 需要不同語言開發
- 系統規模非常大
- 需要獨立的伸縮能力
4、如何使用spring-brick?
spring-brick的使用步驟分為三步:組件開發、組件集成、應用運行。下面我們將詳細介紹每個步驟。
4.1 組件開發
4.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);
}
}
- 加入配置插件
在application.yml中添加配置:
plugin:
runMode: dev
mainPackage: com.gitee.starblues.example
pluginPath:
#- D://project//plugins(替換為自己環境下)
- ~\plugins-basic #插件目錄或插件上級目錄
- 打包主程序
使用 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>
- 創建插件入口類,繼承SpringPluginBootstrap
@SpringBootApplication
public class ExamplePlugin extends SpringPluginBootstrap {
public static void main(String[] args) {
new ExamplePlugin().run(args);
}
}
- 實現主程序定義的擴展點接口
@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 打包與部署
- 打包主程序:
mvn clean package
- 打包插件:
mvn clean package
- 部署運行:
- 將插件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、插件設計原則:每個插件應該只負責一個明確的業務功能,避免功能過於複雜。
- 接口設計:擴展點接口設計要穩定,避免頻繁變更影響插件兼容性。
- 版本管理:插件和主程序的版本要做好管理,確保兼容性。
- 文檔完善:為擴展點提供詳細的文檔,方便插件開發者使用。
5.2 常見使用場景:
場景1:模塊化業務系統
適用場景 :大型企業ERP、CRM、OA系統等需要模塊化擴展的系統
實現方式 :
- 主程序:核心框架、用户管理、權限控制
- 插件1:銷售管理模塊
- 插件2:採購管理模塊
- 插件3:庫存管理模塊
- 插件4:財務管理模塊
優勢 :
- 各業務模塊獨立開發、測試、部署
- 可以根據客户需求選擇安裝特定模塊
- 模塊升級不影響其他功能
場景2:多租户SaaS平台
適用場景 :為不同客户提供定製化功能的SaaS平台
實現方式 :
- 主程序:基礎平台、租户管理
- 插件:為不同客户定製的功能模塊
- 動態加載:根據租户配置加載相應插件
優勢 :
- 不同客户可以使用不同的功能組合
- 新功能可以作為插件快速上線
- 客户定製功能隔離,互不影響
場景3:功能熱部署
適用場景 :需要7x24小時運行但又要頻繁更新功能的系統
實現方式 :
- 主程序:穩定運行的核心服務
- 插件:需要頻繁更新的業務功能
- 熱部署:不停機更新插件
優勢 :
- 避免系統重啓帶來的服務中斷
- 快速響應業務變化
- 降低部署風險
場景4:金融行業風控系統
適用場景 :需要靈活配置風控規則的系統
實現方式 :
- 主程序:基礎風控框架、數據採集
- 插件:各種風控規則引擎
- 動態組合:根據業務場景加載不同規則組合
優勢 : - 風控規則可以快速迭代
- 不同業務線可以使用不同的風控策略
- 規則更新不影響系統穩定性
場景5:物聯網設備管理平台
適用場景 :需要支持多種設備協議的物聯網平台
實現方式 :
- 主程序:設備管理框架、數據存儲
- 插件:各種設備協議解析器
- 動態加載:根據設備類型加載相應協議插件
優勢 :
- 新設備協議可以快速接入
- 協議升級不影響現有設備
- 協議模塊可以獨立優化