前言
抛出一个场景,你需要构建出两个项目,一个是webmvc版 一个是webflux版,应用层完全一样,协议层也完全一样,你会如何 进行架构设计,使得 项目结构足够优雅呢?
方案
方案一(极不推荐)
最粗暴的方式,肯定就是分成两个完全不同的项目,copy 一份,修改传输层代码,但这明显,差强人意,0复用。
设计模式,便是绞尽脑汁的去提高项目的各种复用性。
方案二(常见做法)
-- webmvc
---- common
---- transport-webvc
-- webflux
---- common
---- transport-webflux
这种就是最常见的 模块设计 思路,提取公共的部分,封装成 公共模块,结构清晰,拥有很好的隔离性和扩展性,目前架构示意图 如下所示:
有人会讲了,这个架构看起来非常清晰,非常简单啊,和这篇文章的标题 有什么关系吗?要点要来了,往往正常开发中,即使是一个非常简单的业务系统的架构也比这复杂的多,举个例子:
当业务系统从 “简单 demo 级” 升级为 “企业级复杂应用” 时,方案二的模块设计会逐渐暴露出难以忽视的问题。我们以一个真实的企业级业务系统为例,其架构往往不止 “common+transport” 两层,而是会包含领域模型层(domain)、业务服务层(service)、数据访问层(repository)、第三方集成层(integration)、配置层(config) 等多个核心模块,且可能存在跨模块的依赖关系。此时方案二的架构会演变成这样:
-- webmvc-project(webmvc版总项目)
-- common(公共模块:包含基础工具类、通用枚举等)
-- domain(领域模型模块:用户、订单等核心领域对象)
-- service(业务服务模块:订单创建、用户查询等业务逻辑)
-- repository(数据访问模块:MyBatis/JPA接口定义)
-- integration(第三方集成模块:调用支付、短信等外部接口)
-- config(配置模块:数据库、缓存等配置类)
-- transport-webmvc(webmvc传输层:Controller、拦截器等)
-- starter-webmvc(webmvc启动模块:包含SpringBootApplication主类)
-- webflux-project(webflux版总项目)
-- common(与webmvc版完全一致的公共模块)
-- domain(与webmvc版完全一致的领域模型模块)
-- service(与webmvc版完全一致的业务服务模块)
-- repository(与webmvc版完全一致的数据访问模块)
-- integration(与webmvc版完全一致的第三方集成模块)
-- config(与webmvc版大部分一致,但需适配webflux的配置类)
-- transport-webflux(webflux传输层:@RestController、WebFilter等)
-- starter-webflux(webflux启动模块:包含适配webflux的SpringBootApplication主类)
此时你会发现,方案二的分离性反而是冗余负担,核心问题集中在三点:
- 模块重复维护成本高:common、domain、service 等 “通用模块” 在两个总项目中完全一致,但需要分别在 webmvc-project 和 webflux-project 中各维护一份。一旦需要修改领域模型(如给 Order 类增加 status 字段),必须同时在两个项目的 domain 模块中修改,否则会出现版本不一致导致的线上问题。
- 依赖管理复杂:若后续新增 “权限模块(security)”,需要同时在两个总项目中引入该模块依赖,且需分别适配 webmvc 和 webflux 的权限拦截逻辑(如 webmvc 用 HandlerInterceptor,webflux 用 WebFilter)。随着模块数量增加,两个项目的 pom.xml 依赖关系会逐渐失控,容易出现依赖冲突或版本不兼容问题。
- 构建效率低下:每次发布版本时,需要分别构建 webmvc-project 和 webflux-project 两个独立项目。若每个项目包含 10 + 模块,且单元测试总时长超过 30 分钟,那么两次构建会占用大量 CI/CD 流水线时间,严重影响发布效率。
方案三:Conditional + Maven 动态构建方案(推荐)
核心思路是:将 “webmvc 版” 和 “webflux 版” 合并为一个总项目,通过 Maven 的 “环境激活(profile)” 动态选择依赖,再通过 Spring 的 “条件注解(@Conditional)” 动态加载 Bean,实现 “一套代码,两套构建产物”。
1. 项目结构设计:“通用模块 + 动态适配模块” 分层
合并后的项目结构如下,核心是将 “通用逻辑” 与 “webmvc/webflux 差异逻辑” 彻底分离:
2. 第一步:Maven Profile 动态控制依赖
在总项目的 pom.xml 中定义两个 Profile:webmvc和webflux,通过激活不同 Profile,动态引入对应的适配模块和依赖。
通过命令行激活不同 Profile 进行构建:
- 构建 webmvc 版:mvn clean package -Pwebmvc
- 构建 webflux 版:mvn clean package -Pwebflux
此时 Maven 会根据激活的 Profile,自动引入对应的适配模块和依赖,避免了 “重复引入通用模块” 的问题。
3. 第二步:Spring Conditional 动态加载 Bean
在 “差异逻辑”(如配置类、传输层组件)中,通过 Spring 的条件注解(如@ConditionalOnClass、@ConditionalOnProperty),只加载当前环境下需要的 Bean,避免 Bean 冲突。
示例 1:动态加载配置类
在config模块中,分别定义 webmvc 和 webflux 的配置类,通过@ConditionalOnClass判断当前环境是否存在对应的依赖类,从而决定是否加载该配置。
示例 2:动态加载传输层组件
在adapter-core模块中定义通用接口,然后在adapter-webmvc和adapter-webflux模块中分别实现,再通过@Conditional确保只加载当前环境的实现类。
在业务代码中,只需注入RequestInterceptor接口,Spring 会自动根据当前环境选择对应的实现类:
4. 方案三的核心优势
相比方案一和方案二,Conditional + Maven 动态构建方案完美解决了 “复用” 和 “灵活性” 的平衡问题:
- 零冗余维护:common、domain 等通用模块只需维护一份,修改时无需在多个项目中同步,降低人为失误风险。
- 依赖清晰可控:通过 Maven Profile 统一管理环境依赖,避免依赖冲突,新增模块时只需在总项目中引入一次。
- 构建效率提升:一套代码支持两种构建产物,CI/CD 流水线可并行构建两个版本,且无需重复构建通用模块,缩短构建时间。
- 扩展性强:若后续新增 “grpc 传输层”,只需新增adapter-grpc模块和对应的 Maven Profile,无需修改现有通用逻辑,符合 “开闭原则”。
总结:动态构建的适用场景与最佳实践
适用场景
- 同一业务系统需要适配多种技术栈(如 webmvc/webflux、MySQL/PostgreSQL)。
- 系统需要区分 “开发 / 测试 / 生产” 环境的差异配置(如数据源、缓存策略)。
- 需提供 “轻量版 / 完整版” 两种部署包,差异仅在于部分模块是否引入。
具体例子:
SpringAI的mcp模块,spring-ai-autoconfigure-mcp-server通过conditional系列注解同时兼容webmvc和webflux,然后再通过 spring-ai-starter-mcp-server-webmvc 和 spring-ai-starter-mcp-server-webflux,引入该依赖 和 webmvc 或 webflux依赖完成 conditional 的选取,从而完美的适配了两种技术栈的逻辑。