前言
在當今快速迭代的軟件開發領域,業務需求的頻繁變更對系統架構的靈活性和可擴展性提出了極高要求。傳統的單體應用架構在面對功能的不斷新增和修改時,往往會陷入代碼臃腫、維護困難、擴展性差的困境。組件化開發,為解決這些問題提供了新的思路,通過實現組件的動態插拔,讓系統能夠更敏捷地響應業務變化。?
1. 背景
在大部分業務場景,微服務拆分不是一個好的選擇。服務拆分帶來有幾方面的挑戰:1)成本增加。微服務單獨部署需要更多資源,包括服務、負載、網絡等;2)應用復雜度增加。微服務需要引入rpc框架,跨服務事務保障,服務鏈路延長耗時處理等;3)應用維護成本增加。微服務調用鏈路延長,在排查問題時,如果沒有日志鏈路工具,整個鏈路追蹤下來是非常耗時的,bug修復維護成本大大增加;4)運維成本增加。多服務器夸鏈路調用,運維成本也會隨之增加。
在綜合考慮性能和成本的情況,會收縮服務應用,抽象出功能齊全的單體應用。單體架構將所有功能模塊打包成一個可執行文件進行部署。這種架構在項目初期,由于功能相對簡單,開發和部署都較為便捷。隨著業務復雜度增加,一些問題會隨之浮現:
-
通用基礎能力之外,需要為不同客戶提供定制化功能
-
代碼耦合度越來越高,”屎山“代碼堆積,后來者不敢輕易維護,只能重新寫邏輯,導致不斷增加冗余代碼
-
某些獨立功能模塊,不隨主線迭代發布
-
某些不需要的功能,需要下線處理掉
-
緊急Bug或者功能增加,需要緊急替換服務實現
-
不同團隊負責不同業務模塊的并行開發
為了應對這些挑戰,組件化開發模式應運而生。
2. 組件化開發,實現系統的敏捷響應?
插件模式開發是一種軟件架構設計模式,允許應用程序通過動態加載和卸載插件來擴展或定制功能,而無需修改主程序的源代碼。這種模式將核心功能與擴展功能分離,使系統具備更高的靈活性、可維護性和可擴展性。
(一)降低維護成本?
在單體架構中,一次小的功能修改可能需要對整個項目進行全面測試,以確保不會影響其他功能。組件化開發后,每個組件都是一個獨立的個體。當需要修改某個功能時,只需對對應的組件進行調整和測試,不會影響到其他組件的正常運行,大大降低了維護的復雜度和測試成本。?
(二)加速功能迭代?
傳統開發模式下,新功能的添加需要重新構建和部署整個應用,流程繁瑣且耗時。采用組件化動態插拔開發,開發人員可以將新功能封裝成獨立的組件,在運行時動態地將其插入到系統中,無需停機和重新部署整個應用,極大地縮短了新功能的上線周期,使系統能夠更快速地響應市場需求。?
(三)提高系統穩定性?
由于組件之間的低耦合性,即使某個組件出現故障,也不會導致整個系統崩潰。其他組件仍然可以正常運行,通過快速定位和修復故障組件,能夠有效提高系統的穩定性和可用性。
(四)適配定制化開發
將定制功能抽象到特定的組件中,通過組件化動態插拔組件,可以很好解決客戶定制化需求。
3. 組件動態插拔框架選型
在Spring框架下,可選擇組件動態插拔框架有:
能力維度 | OSGi | PF4J | Spring Brick | Spring Plugin Core | Sermant |
---|---|---|---|---|---|
熱部署方式 | Bundle 動態安裝/卸載 | JAR 插件熱加載 | 插件包動態安裝/卸載 | 配置驅動,需重啟應用 | 字節碼動態增強 |
類隔離性 | ? 強隔離(獨立 ClassLoader) | ?? 需手動配置隔離 | ? 全資源隔離(獨立類加載器) | ? 無隔離 | ? 宿主應用隔離 |
Spring 支持度 | ?? 需 Spring DM 適配 | ? 原生友好 | ? 深度集成(Spring Boot 原生開發) | ? 完全原生 | ?? 部分兼容 |
通信機制 | ? 原生 ServiceTracker | ?? 需自建事件總線 | ? 事件總線 + 主程序路由 | ? Bean 直接調用 | ? 不支持跨插件通信 |
適用場景 | 大型企業應用/IDE | 中小型業務擴展 | 高隔離業務模塊/SaaS 定制 | 輕量級靜態擴展 | 微服務治理/故障測試 |
主要限制 | 配置復雜、啟動慢 | 隔離性弱、無服務發現 | 需手動管理資源生命周期 | 無熱插拔、依賴沖突風險 | 無法新增類/字段 |
結合自身團隊情況以及熱插拔需求,我們選擇基于PF4J作為Springboot應用的插件框架。關于PL4J的相關介紹,可參考官網
PF4J : PF4JPlugin Framework for Javahttps://pf4j.org/其主要突出點包括:
- 通過獨立類加載器做類隔離,支持運行時熱插拔
- 核心包很小,類加載運行所需內存資源小
- 與Spring原生支持性好。
4. 方案實現
4.1 搭建運行框架底座
運行框架底座是spirngboot+JPA+內嵌tomcat的web應用,前端資源直接打包成靜態文件到fat jar中,后端web控制點有權限攔截器和登錄攔截器,數據庫支持MySQL和H2內置數據庫。
父的pom文件定義:
<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/maven-v4_0_0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.test.sf</groupId><artifactId>sf-flow</artifactId><version>1.0-SNAPSHOT</version><packaging>pom</packaging><modules><module>ingress</module><module>app/standardapp</module><module>app/permissionapp</module><module>layer/framework</module><module>layer/datax-spi</module></modules><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.4.1</version><relativePath/> <!-- lookup parent from repository --></parent><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><maven.compiler.source>21</maven.compiler.source><maven.compiler.target>21</maven.compiler.target><jackson.version>2.17.2</jackson.version><protobuf.version>3.25.5</protobuf.version><grpc.version>1.62.2</grpc.version><aws-sdk-java.version>1.12.725</aws-sdk-java.version><springdoc-openapi-ui.version>2.8.4</springdoc-openapi-ui.version><datax-spi-version>1.0-SNAPSHOT</datax-spi-version><hutool.version>5.8.33</hutool.version></properties><dependencyManagement>...</dependencyManagement><profiles><profile><id>dev</id><properties><environment>dev</environment></properties><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-resources-plugin</artifactId><executions><execution><configuration>
<outputDirectory>${basedir}/target/classes</outputDirectory><resources><resource><directory>src/main/resources</directory><filtering>true</filtering><includes><include>**/application-${environment}.properties</include></includes></resource></resources></configuration></execution></executions></plugin></plugins></build></profile>
</profiles></project>
入口ingress的Main函數實現:
@SpringBootApplication(scanBasePackages = { "com.test.sf"
})
@EnableJpaRepositories(basePackages = {"com.test.sf.layer","com.test.sf.a