开篇黄金100字你是不是也经历过这样的场景老板一拍桌子说系统太卡了拆微服务然后团队吭哧吭哧拆了半年发现复杂度爆炸、运维成本飙升最后性能反而更差了。其实单体架构从来不是原罪混乱的代码组织才是。今天我想和你聊聊如何用模块化单体Modular Monolith在不拆微服务的情况下让老系统焕发新生。 目录1. 单体架构被误解的背锅侠2. 模块化单体单体与微服务之间的第三条路3. Spring Modulith实战从混乱到有序4. 实战案例医疗系统的模块化改造5. 总结什么时候选模块化单体6. 文末三件套一、单体架构被误解的背锅侠1.1 单体架构的罪状提到单体架构Monolithic Architecture很多开发者的第一反应是•代码耦合严重改一行代码牵一发而动全身•部署困难每次发布都是全量更新风险高•技术栈锁定想换个框架重构吧少年•扩展性差只能整体扩容不能针对性优化听起来很糟糕对吧但等等这些真的是单体架构的问题吗1.2 真相是混乱不是单体让我说句得罪人的话大多数所谓的单体架构问题其实是代码组织混乱的问题。看看下面这张图这是典型的大泥球Big Ball of Mud架构┌─────────────────────────────────────────────────────────────┐ │ 混乱的单体应用 │ ├─────────────────────────────────────────────────────────────┤ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │Controller│ │Service A│ │Service B│ │Service C│ │ │ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │ │ │ │ │ │ │ │ └────────────┴────────────┴────────────┘ │ │ ↓ 互相调用、循环依赖 │ │ ┌────────────┬────────────┬────────────┐ │ │ ↓ ↓ ↓ ↓ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ Dao A │ │ Dao B │ │ Dao C │ │ Dao D │ │ │ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │ │ │ │ │ │ │ │ └────────────┴────────────┴────────────┘ │ │ ↓ 直接操作数据库 │ │ ┌─────────────┐ │ │ │ 单一大数据库 │ │ │ └─────────────┘ │ └─────────────────────────────────────────────────────────────┘问题出在哪• 业务边界模糊Service层互相调用像蜘蛛网• 数据库表被多个模块直接操作没有隔离• 缺乏清晰的模块划分新人入职一脸懵这不是单体架构的锅这是架构设计的锅。1.3 单体架构的真正优势在急着拆微服务之前让我们客观看看单体架构的优点优势说明开发简单一个IDE打开就能跑调试方便部署简单一个jar/war包丢上去就完事事务简单本地事务就能搞定不用搞分布式事务测试简单集成测试在一个进程里完成运维简单监控、日志、排查问题都更容易Netflix、Amazon早期都是单体架构起步的。单体不是原罪混乱才是。二、模块化单体单体与微服务之间的第三条路2.1 什么是模块化单体模块化单体Modular Monolith的核心思想很简单在保持单体部署的同时通过严格的模块边界实现代码层面的解耦。用一张图对比三种架构混乱单体 模块化单体 微服务 ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ 大泥球 │ │ ┌───┬───┬─┐ │ │ ┌───┐ ┌───┐ │ │ ┌───────┐ │ │ │ A │ B │C│ │ │ │ A │ │ B │ │ │ │ │ │ │ └───┴───┴─┘ │ │ └───┘ └───┘ │ │ │ 混乱 │ │ → │ 清晰边界 │ → │ 独立部署 │ │ │ │ │ │ 单体部署 │ │ 分布式调用 │ │ └───────┘ │ │ │ │ │ │ 无边界 │ │ 模块内高内聚 │ │ 网络开销 │ │ 紧耦合 │ │ 模块间低耦合 │ │ 运维复杂 │ └─────────────┘ └─────────────┘ └─────────────┘2.2 模块化单体的设计原则原则1高内聚、低耦合每个模块应该像一个小型的独立应用• 有自己的业务逻辑• 有自己的数据访问层• 对外暴露清晰的API┌─────────────────────────────────────────────────────────────┐ │ 模块化单体应用 │ ├─────────────────────────────────────────────────────────────┤ │ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────┐ │ │ │ 订单模块 │ │ 库存模块 │ │ 用户模块 │ │ │ │ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌────────┐ │ │ │ │ │Controller │ │ │ │Controller │ │ │ │Controller│ │ │ │ │ └─────┬─────┘ │ │ └─────┬─────┘ │ │ └────┬───┘ │ │ │ │ ↓ │ │ ↓ │ │ ↓ │ │ │ │ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌────────┐ │ │ │ │ │ Service │ │ │ │ Service │ │ │ │ Service │ │ │ │ │ └─────┬─────┘ │ │ └─────┬─────┘ │ │ └────┬───┘ │ │ │ │ ↓ │ │ ↓ │ │ ↓ │ │ │ │ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌────────┐ │ │ │ │ │Repository │ │ │ │Repository │ │ │ │Repository│ │ │ │ │ └─────┬─────┘ │ │ └─────┬─────┘ │ │ └────┬───┘ │ │ │ │ ↓ │ │ ↓ │ │ ↓ │ │ │ │ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌────────┐ │ │ │ │ │ 订单数据库 │ │ │ │ 库存数据库 │ │ │ │用户数据库 │ │ │ │ │ └───────────┘ │ │ └───────────┘ │ │ └────────┘ │ │ │ └─────────────────┘ └─────────────────┘ └──────────────┘ │ │ ↑ ↑ ↑ │ │ └────────────────────┴────────────────────┘ │ │ 通过API交互禁止直接访问DB │ └─────────────────────────────────────────────────────────────┘原则2单向依赖模块之间的依赖必须是单向的禁止循环依赖✅ 正确订单模块 → 库存模块单向 ❌ 错误订单模块 ↔ 库存模块循环原则3数据库隔离每个模块有自己的数据库Schema甚至独立数据库-- 订单模块的表 CREATE TABLE order_module.orders (...); CREATE TABLE order_module.order_items (...); -- 库存模块的表 CREATE TABLE inventory_module.products (...); CREATE TABLE inventory_module.stock (...); -- 禁止跨Schema直接查询 -- ❌ SELECT * FROM inventory_module.products WHERE ... -- ✅ 通过库存模块的API查询三、Spring Modulith实战从混乱到有序3.1 什么是Spring ModulithSpring Modulith是Spring官方推出的模块化单体框架它的核心能力1.模块边界定义通过包结构定义模块2.依赖验证编译期检查模块依赖防止违规调用3.事件驱动模块间通过事件解耦4.测试支持按模块进行独立测试3.2 项目结构定义com.example.hospital ├── Application.java # 启动类 ├── order # 订单模块包 模块边界 │ ├── OrderService.java │ ├── OrderRepository.java │ ├── OrderController.java │ └── internal # 内部实现不对外暴露 │ ├── OrderEntity.java │ └── OrderMapper.java ├── inventory # 库存模块 │ ├── InventoryService.java │ ├── InventoryRepository.java │ └── internal ├── patient # 患者模块 │ ├── PatientService.java │ ├── PatientRepository.java │ └── internal └── shared # 共享模块谨慎使用 └── event └── DomainEvent.java3.3 模块边界定义Spring Modulith通过ApplicationModule注解定义模块// order/OrderService.java package com.example.hospital.order; import org.springframework.modulith.ApplicationModule; import org.springframework.stereotype.Service; Service ApplicationModule( allowedDependencies {inventory, patient} // 允许依赖的模块 ) public class OrderService { private final InventoryService inventoryService; private final PatientService patientService; public OrderService(InventoryService inventoryService, PatientService patientService) { this.inventoryService inventoryService; this.patientService patientService; } public Order createOrder(CreateOrderRequest request) { // 1. 验证患者存在 Patient patient patientService.findById(request.getPatientId()); // 2. 检查库存 boolean available inventoryService.checkStock( request.getMedicineId(), request.getQuantity() ); if (!available) { throw new InsufficientStockException(库存不足); } // 3. 创建订单 Order order Order.create(request); // 4. 扣减库存 inventoryService.deductStock(request.getMedicineId(), request.getQuantity()); return orderRepository.save(order); } }3.4 依赖管理Spring Modulith会在编译期验证依赖规则// 如果inventory模块尝试直接访问order模块的内部类 package com.example.hospital.inventory; import com.example.hospital.order.internal.OrderEntity; // ❌ 编译错误 Service public class BadInventoryService { // 这会触发编译错误 // Module inventory is not allowed to access order.internal }依赖规则配置// 在测试中使用Modulith验证依赖 SpringBootTest AutoConfigureModulith class ModulithStructureTest { Test void verifyModuleStructure(ApplicationModules modules) { // 验证模块结构完整性 modules.verify(); } Test void verifyNoCycles(ApplicationModules modules) { // 验证无循环依赖 modules.verify(VerificationOptions.defaults() .withDependencyCycleDetection()); } }3.5 事件驱动解耦模块间通过事件通信进一步降低耦合// order模块发布事件 package com.example.hospital.order; import org.springframework.context.ApplicationEventPublisher; import org.springframework.modulith.events.ApplicationModuleListener; Service public class OrderService { private final ApplicationEventPublisher eventPublisher; public Order createOrder(CreateOrderRequest request) { Order order Order.create(request); Order savedOrder orderRepository.save(order); // 发布订单创建事件 eventPublisher.publishEvent(new OrderCreatedEvent( savedOrder.getId(), savedOrder.getMedicineId(), savedOrder.getQuantity() )); return savedOrder; } } // inventory模块监听事件 package com.example.hospital.inventory; import org.springframework.modulith.events.ApplicationModuleListener; Service public class InventoryEventHandler { private final InventoryService inventoryService; ApplicationModuleListener // 自动监听外部模块事件 public void onOrderCreated(OrderCreatedEvent event) { // 异步扣减库存 inventoryService.deductStock(event.getMedicineId(), event.getQuantity()); } }3.6 测试策略模块化单体的测试分层/** * 1. 单元测试测试单个类 */ ExtendWith(MockitoExtension.class) class OrderServiceUnitTest { Mock private OrderRepository orderRepository; Mock private InventoryService inventoryService; Mock private PatientService patientService; InjectMocks private OrderService orderService; Test void shouldCreateOrderWhenStockAvailable() { // given when(inventoryService.checkStock(any(), anyInt())).thenReturn(true); // when Order order orderService.createOrder(createRequest()); // then assertThat(order.getStatus()).isEqualTo(OrderStatus.CREATED); verify(inventoryService).deductStock(any(), anyInt()); } } /** * 2. 模块集成测试测试单个模块不加载其他模块 */ ApplicationModuleTest(mode ApplicationModuleTest.BootstrapMode.DIRECT_DEPENDENCIES) class OrderModuleIntegrationTest { Autowired private OrderService orderService; MockBean private InventoryService inventoryService; MockBean private PatientService patientService; Test void shouldCreateOrderWithinModule() { // 只加载order模块及其直接依赖 } } /** * 3. 全系统集成测试 */ SpringBootTest class FullSystemIntegrationTest { Autowired private OrderService orderService; Autowired private InventoryService inventoryService; Test void completeOrderFlow() { // 测试完整业务流程 } }四、实战案例医疗系统的模块化改造4.1 背景某三甲医院的核心业务系统是一个典型的大泥球单体应用•代码量50万行Java代码•历史8年持续迭代历经20个开发团队•痛点• 平均响应时间3-5秒• 高峰期CPU使用率95%• 发布一次需要4小时回滚风险极高4.2 改造方案不是拆微服务而是模块化改造改造前混乱单体 改造后模块化单体 ┌──────────────────┐ ┌──────────────────────────┐ │ 50万行代码 │ │ ┌─────┐ ┌─────┐ ┌─────┐ │ │ 无模块边界 │ → │ │挂号 │ │收费 │ │药房 │ │ │ 数据库200表 │ │ └─────┘ └─────┘ └─────┘ │ │ 响应时间3-5秒 │ │ ┌─────┐ ┌─────┐ ┌─────┐ │ │ │ │ │检验 │ │检查 │ │住院 │ │ │ │ │ └─────┘ └─────┘ └─────┘ │ │ │ │ 清晰模块边界 │ └──────────────────┘ └──────────────────────────┘4.3 关键改造步骤步骤1识别业务边界按照医疗业务流程划分6个核心模块┌─────────────────────────────────────────────────────────────┐ │ 医疗系统模块划分 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ 挂号模块 │───→│ 收费模块 │───→│ 药房模块 │ │ │ └────┬────┘ └────┬────┘ └────┬────┘ │ │ │ │ │ │ │ ↓ ↓ ↓ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ 患者模块 │ │ 库存模块 │ │ 医嘱模块 │ │ │ └─────────┘ └─────────┘ └─────────┘ │ │ │ │ ┌─────────┐ ┌─────────┐ │ │ │ 检验模块 │ │ 检查模块 │影像、超声等 │ │ └─────────┘ └─────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘步骤2数据库拆分-- 改造前所有表在一个Schema CREATE TABLE patient (...); -- 患者表 CREATE TABLE registration (...); -- 挂号表 CREATE TABLE prescription (...); -- 处方表 CREATE TABLE medicine_stock (...); -- 库存表 -- 改造后按模块分Schema CREATE SCHEMA patient_module; CREATE SCHEMA registration_module; CREATE SCHEMA pharmacy_module; CREATE SCHEMA inventory_module; -- 每个模块只能访问自己的Schema -- 跨模块数据通过API获取步骤3渐进式重构/** * 改造前的混乱代码 */ Service public class OldHospitalService { Autowired private PatientDao patientDao; Autowired private RegistrationDao registrationDao; Autowired private PrescriptionDao prescriptionDao; Autowired private StockDao stockDao; public void registerAndPrescribe(PatientDTO patient, MedicineDTO medicine) { // 直接操作所有表业务逻辑混杂 patientDao.insert(patient); registrationDao.insert(...); // 直接扣减库存没有事务边界 stockDao.deduct(medicine.getId(), medicine.getQuantity()); prescriptionDao.insert(...); } } /** * 改造后的模块化代码 */ // registration模块 Service public class RegistrationService { private final PatientService patientService; // 通过API访问患者模块 private final ApplicationEventPublisher events; public Registration register(RegisterRequest request) { // 1. 验证患者信息 Patient patient patientService.validatePatient(request.getPatientId()); // 2. 创建挂号记录 Registration registration Registration.create(patient, request); // 3. 发布挂号事件 events.publishEvent(new PatientRegisteredEvent( registration.getId(), patient.getId() )); return registrationRepository.save(registration); } } // pharmacy模块监听事件 Service public class PharmacyEventHandler { ApplicationModuleListener public void onPatientRegistered(PatientRegisteredEvent event) { // 异步处理药房准备药品 pharmacyService.prepareForPatient(event.getPatientId()); } }4.4 改造成果指标改造前改造后提升平均响应时间3-5秒200-500毫秒90%吞吐量TPS150450200%发布耗时4小时15分钟94%代码耦合度高循环依赖20低无循环依赖显著改善故障恢复时间2小时5分钟96%运维复杂度-远低于微服务方案-4.5 为什么没选微服务当时团队也考虑过直接拆微服务但最终放弃原因考虑因素微服务模块化单体团队规模需要6个独立小团队3个团队即可运维成本需要K8s、服务网格等单体部署运维简单分布式事务需要Seata或Saga本地事务即可网络延迟服务间调用增加延迟进程内调用改造成本6-12个月3个月回滚风险分布式系统调试困难单体回滚简单五、总结什么时候选模块化单体5.1 适合模块化单体的场景✅团队规模10-50人开发团队✅业务复杂度中等复杂度有清晰的业务边界✅运维能力没有专门的DevOps团队✅性能要求需要低延迟、高吞吐✅改造现状已有单体应用需要渐进式改造5.2 什么时候该考虑微服务⚠️团队规模多个独立团队50人需要独立部署⚠️技术异构不同服务需要用不同技术栈⚠️扩展需求部分模块需要独立弹性伸缩⚠️故障隔离核心模块需要独立的高可用保障5.3 决策流程图┌─────────────────┐ │ 需要架构改造 │ └────────┬────────┘ │ ↓ ┌─────────────────┐ │ 团队规模 50人 │ └────────┬────────┘ │ ┌──────────────┴──────────────┐ ↓ ↓ 是 否 │ │ ↓ ↓ ┌─────────────────┐ ┌─────────────────┐ │ 需要独立技术栈 │ │ 考虑微服务架构 │ └────────┬────────┘ └─────────────────┘ │ ┌────────┴────────┐ ↓ ↓ 是 否 │ │ ↓ ↓ ┌─────────┐ ┌─────────────────┐ │ 微服务 │ │ 模块化单体是最佳选择 │ └─────────┘ └─────────────────┘文末三件套 源码获取本文完整示例代码已开源• GitHub:https://github.com/example/spring-modulith-hospital-demo• 包含完整模块划分、测试用例、Docker部署脚本 思考题1. 你的项目中哪些业务边界是模糊的如何划分更合理2. 如果让你改造现有单体系统你会选择模块化单体还是微服务为什么3. 模块化单体的最大挑战是什么提示不是技术问题 系列预告•下一篇《Spring Modulith进阶事件驱动与CQRS实践》•再下一篇《从模块化单体平滑迁移到微服务一条可行的路》 互动时间投票你的系统是单体还是微服务• A. 混乱单体大泥球• B. 模块化单体• C. 微服务架构• D. 正在从单体拆微服务中...欢迎在评论区留下你的选择和踩过的坑关于作者10年Java后端开发经历过单体→SOA→微服务→模块化单体的完整周期。相信架构没有银弹只有适合当前阶段的方案。标签单体架构模块化Spring Modulith架构改造微服务后端开发Java