前言
在当今快速迭代的软件开发领域,业务需求的频繁变更对系统架构的灵活性和可扩展性提出了极高要求。传统的单体应用架构在面对功能的不断新增和修改时,往往会陷入代码臃肿、维护困难、扩展性差的困境。组件化开发,为解决这些问题提供了新的思路,通过实现组件的动态插拔,让系统能够更敏捷地响应业务变化。
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