从“大泥球”到“乐高积木”的架构演进之旅传统的Spring Boot单体应用往往陷入这样的困境订单服务直接调用库存仓储用户工具类被订单模块随意引用改一个运费计算可能要翻遍大半个代码库。依赖关系错综复杂形成一个剪不断理还乱的网状结构。这种“大泥球”带来的后果是沉重的脆弱性对用户模块内部逻辑的微小修改可能导致订单处理流程意外崩溃因为两者在代码层面缺乏隔离认知负荷新加入的开发者无法区分哪些类是模块的公开API哪些是内部实现细节很容易错误地依赖本应隐藏的内部组件重构瘫痪当系统最终需要拆分为微服务时由于依赖关系过于混乱剥离任何一个业务领域都几乎不可能。为此不少团队选择了微服务架构。然而实践表明许多组织在未能充分评估其运维复杂性的情况下盲目拆分单体应用最终构建了“分布式单体”——系统虽然在物理上是分布式的但在逻辑上依然紧密耦合同时引入了网络延迟、分布式事务难题以及高昂的运维成本。企业需要的不是非此即彼的极端选择而是一个“中间地带”。这正是Spring Modulith的诞生之地。它提供了一种介于传统单体与微服务之间的架构模式模块化单体。既保留了单体应用一次部署、资源共享、内存调用高效的优势又通过强制模块边界来防止跨模块的非法依赖。一、从“分层孤岛”到“功能模块”的思维转变1.1 传统分层架构的困境许多Spring Boot应用仍然采用按技术层打包的方式bookstore/ ├── config/ ├── entities/ ├── exceptions/ ├── models/ ├── repositories/ ├── services/ └── web/这种按技术分层的风格带来了几个问题代码结构无法表达应用的实际功能你看到的是“repositories”“services”“web”而不是“catalog”“orders”“inventory”业务领域被隐藏在技术文件夹背后。更糟糕的是在这种布局下类往往被设置为public以便被多个包调用导致整个应用没有任何清晰的“公共API”概念任何代码都可以依赖任何代码。1.2 模块化单体的核心思想Spring Modulith借鉴了领域驱动设计的理念将每个模块视为一个“限界上下文”内部独立维护领域模型和业务逻辑。它的核心思想是将系统按业务领域拆分为模块而不是按技术分层。模块之间通过同步调用或异步事件协作并提供模块边界验证和自动生成文档的能力。1.3 为什么不用JPMS一个常见的问题是为何不直接使用Java 9的JPMSJava Platform Module SystemJPMS的设计目标是模块化JDK本身在这方面做得很好。但对于应用开发人员来说JPMS要求每个模块都是单独的JAR集成测试必须打包成单独模块这带来了严重的技术开销。Spring Modulith采用更轻量级的方式基于普通的Java包结构定义模块边界既保持了简单性又提供了足够的架构约束能力。二、核心能力Spring Modulith的四大支柱2.1 强制模块边界让架构约束“可执行”Spring Modulith通过分析包结构来定义模块边界。默认情况下应用程序主包包含SpringBootApplication主类的包下的每个直接子包都被识别为独立的应用程序模块。一个典型的包结构如下com.example.demo/ ├── DemoApplication.java # 主类 ├── order/ # 订单模块应用模块 │ ├── OrderManagement.java # 公开API │ └── internal/ # 内部实现 │ ├── OrderServiceImpl.java │ └── OrderRepository.java ├── inventory/ # 库存模块 │ ├── InventoryManagement.java │ └── internal/ │ └── InventoryServiceImpl.java └── customer/ # 客户模块 ├── CustomerController.java └── internal/ └── CustomerService.javaSpring Modulith通过静态分析与运行时验证双重机制管理并约束模块之间的依赖关系只能通过API调用其他模块模块之间只能通过对方的公开API进行交互禁止访问内部实现直接访问其他模块internal包的代码会被阻止依赖关系图必须是有向无环图不允许存在循环依赖。要验证模块结构是否符合约定只需编写一个简单的测试class ModularityTests { Test void verifyModularity() { ApplicationModules.of(DemoApplication.class).verify(); } }如果inventory模块直接调用了order.internal中的类测试会以明确的错误消息失败。2.2 即时文档永不失效的架构图软件开发中一个永恒的难题是文档尤其是架构图几乎总是在编写完成的那一刻就开始过时。代码在不断演进但手动维护的图表却常常被遗忘最终沦为不可信的废纸。Spring Modulith的Documenter功能彻底解决了这个问题。它能直接扫描代码结构将模块间的真实依赖关系自动生成为标准的可视化图表组件图通过writeModulesAsPlantUml()方法自动生成清晰展示所有模块及其依赖关系的架构图应用模块画布为每个模块生成详尽的说明书清晰列出模块的入口点、核心领域对象、对外承诺的契约以及依赖的外部信号。文档与代码实现100%同步因为唯一真相来源就是代码本身。2.3 精准测试模块化集成测试传统的SpringBootTest在大型应用中是一个效率杀手。每次运行测试它都会启动完整的Spring应用上下文加载成百上千个Bean导致测试过程极其缓慢。ApplicationModuleTest注解是Spring Modulith为集成测试量身打造的利器ApplicationModuleTest class InventoryModuleTests { Test void testInventoryLogic() { // 只启动inventory模块所需的Spring上下文 // 隔离其他无关模块测试速度极快 } }在默认的STANDALONE模式下它只会启动当前测试用例所在模块所需的Spring上下文。如果需要测试模块间的交互可以通过设置启动模式为DIRECT_DEPENDENCIES加载直接依赖的模块或ALL_DEPENDENCIES加载整个依赖树。Spring Modulith 2.1进一步增强了测试能力PublishedEvents和Scenario现在从整个应用程序捕获事件而非仅限于线程绑定使得来自独立线程池如outbox集成使用的事件也能被测试感知。2.4 运行时观测洞察模块交互Spring Modulith提供了运行时观测能力能够自动为模块发布的应用程序事件创建Micrometer计数器。开发者可以实时监控模块间的交互行为及时发现异常依赖。三、模块间通信同步与异步的双向选择3.1 同步通信通过公开API模块之间可以通过显式声明的公开API进行同步调用。以一个配送服务应用为例// customer模块公开的API订单Controller依赖 RestController RequestMapping(/api/customer) public class CustomerController { private final PriceCalculator priceCalculator; // calculator模块的API private final ShipmentService shipmentService; // shipment模块的API // 通过其他模块的公开Bean进行同步调用 }同步通信保持了调用的即时性和类型安全适用于业务逻辑紧密关联的场景。3.2 异步通信事件驱动Spring Modulith鼓励使用Spring Framework的应用事件作为模块间异步交互的主要方式。它通过事件发布注册中心对事件进行了增强该注册中心通过持久化事件确保了事件的可靠交付——即便整个应用发生了崩溃或者只有一个模块接收到了事件注册中心依然能够确保事件正常交付。Spring Modulith 2.1进一步支持了基于Outbox模式的事件外部化支持多实例、保序的消息发布为模块化单体向分布式系统的演进提供了坚实的基础。3.3 命名接口显式声明API边界NamedInterface注解允许开发者显式声明哪些包是模块的公开API哪些是内部实现ApplicationModule( displayName 订单管理模块, allowedDependencies {inventory, payment} ) package com.example.demo.order; import org.springframework.modulith.ApplicationModule;这种显式的声明不仅让模块边界一目了然也使得IDE和测试工具能够提供更精准的验证。四、实战构建一个模块化电商应用4.1 环境准备在pom.xml中添加依赖dependencyManagement dependency groupIdorg.springframework.modulith/groupId artifactIdspring-modulith-bom/artifactId version${spring-modulith.version}/version typepom/type scopeimport/scope /dependency /dependencyManagement dependency groupIdorg.springframework.modulith/groupId artifactIdspring-modulith-starter-core/artifactId /dependency !-- 可选运行时观测 -- dependency groupIdorg.springframework.modulith/groupId artifactIdspring-modulith-starter-insight/artifactId /dependency4.2 定义模块边界按照业务领域划分模块而非技术分层。每个模块内部可以继续采用六边形架构组织代码com.example.ecommerce/ ├── DemoApplication.java ├── order/ # 订单模块 │ ├── application/ │ │ └── OrderApplicationService.java │ ├── domain/ │ │ ├── Order.java # 聚合根 │ │ └── OrderItem.java │ ├── infrastructure/ │ │ └── JpaOrderRepository.java │ └── interfaces/ │ └── OrderController.java ├── inventory/ # 库存模块 │ ├── application/ │ │ └── InventoryApplicationService.java │ ├── domain/ │ │ └── ProductStock.java │ └── infrastructure/ │ └── JpaStockRepository.java └── payment/ # 支付模块 └── ... # 类似的内部结构4.3 编写验证测试package com.example.ecommerce; import org.junit.jupiter.api.Test; import org.springframework.modulith.core.ApplicationModules; import org.springframework.modulith.docs.Documenter; class ModularityTests { ApplicationModules modules ApplicationModules.of(DemoApplication.class); Test void verifyModularity() { modules.verify(); // 验证模块边界任何违规都会导致测试失败 } Test void createModuleDocumentation() { new Documenter(modules) .writeModulesAsPlantUml() // 生成PlantUML架构图 .writeModuleCanvases(); // 生成每个模块的详细说明文档 } }4.4 模块间事件通信// 订单模块发布事件 Service public class OrderApplicationService { private final ApplicationEventPublisher events; public void completeOrder(OrderId orderId) { // 订单完成逻辑... events.publishEvent(new OrderCompletedEvent(orderId, userId, totalAmount)); } } // 库存模块监听事件 Component class InventoryEventListener { EventListener Async public void handleOrderCompleted(OrderCompletedEvent event) { // 预留库存 - 跨模块异步解耦 inventoryService.reserve(event.getOrderId()); } }4.5 IntelliJ IDEA中的Spring Modulith支持IntelliJ IDEA Ultimate为Spring Modulith提供了出色的工具支持。添加依赖后IDE会自动识别模块边界在Project工具窗口中顶层模块以绿色锁标记内部组件以红色锁标记。IDE提供了一系列检查和快速修复操作帮助你保持应用结构符合Spring Modulith架构原则这些问题的严重程度默认为错误级别。五、架构演进从模块化单体到微服务Spring Modulith最令人欣赏的设计之一是渐进式演进。它支持从单体平滑过渡到微服务避免一次性不可逆的大决策。5.1 演进路径第一阶段模块化单体使用Spring Modulith划分模块边界基于包结构定义模块模块间通过同步API或异步事件协作单个部署单元零运维成本保留单体的全部优势。第二阶段独立部署模块当单体性能出现瓶颈时将高频模块拆分为独立微服务模块已通过事件方式解耦且有清晰的API边界拆分成本极低模块间通信方式无需改变只是从进程内调用变为HTTP/RPC调用。第三阶段完整微服务治理引入服务发现、API网关、熔断降级等基础设施按需扩展核心域服务享受微服务的弹性伸缩优势。Spring Modulith的Outbox模式支持为这种演进提供了坚实的底层基础——当模块最终被拆分为独立微服务时现有的异步事件机制可以平滑升级为消息队列。六、核心要点总结维度核心要点定位单体与微服务之间的“中间地带”兼顾单体的简便性与微服务的模块化模块定义主包下的直接子包 应用模块internal子包自动视为内部实现边界验证ApplicationModules.of().verify()将架构约束转化为可执行的测试文档生成Documenter自动生成PlantUML组件图和模块API画布文档永不失效集成测试ApplicationModuleTest仅加载当前模块上下文测试速度极快模块通信同步通过公开API异步通过领域事件 Event Publication Registry观测性自动为模块API和事件处理创建Micrometer指标演进能力从模块化单体平滑演进到微服务无需推翻重来七、实战避坑指南① 模块不要划分得过于精细模块数量过多会引入不必要的依赖管理开销。一般建议每个模块对应一个明确的业务领域不要为“可能”的复用而过度拆分。② 不要将跨模块调用“偷渡”进internal包internal包是模块的最后一道防线。一旦有外部模块直接访问internal中的类模块边界就失去了意义。③ 谨慎处理循环依赖循环依赖的模块往往意味着职责划分不够清晰。遇到这种情况请重新审视两个模块的边界是否合理必要时引入新的领域服务作为协调层。④ 不要过早拆分微服务Spring Modulith的设计初衷就是在业务早期保持模块化单体的低成本优势。只有当模块边界真正稳定、性能瓶颈明确时才值得考虑物理拆分。⑤ 合理利用命名接口不要把所有公开类都当作API。通过NamedInterface显式声明哪些包是对外公开的可以减少模块间的耦合度。写在最后架构不是目的解耦才是Spring Modulith是一个“固执己见”的工具包它就像Spring Boot对应用的技术安排有自己的看法一样对如何从功能上构建应用程序并允许其各个逻辑部分相互交互有自己的实现。它的真正价值在于将架构约束转化为可执行的代码让模块边界不再是写在文档上的空话而是可以被测试验证的硬约束让团队在业务早期专注于业务本身而不是陷入微服务的运维泥潭为系统的长期演进铺平道路让从单体到微服务的过渡变得渐进、可控、低成本。正如项目负责人Oliver Drotbohm所强调的团队不应仅仅因为技术平台支持某种架构就匆忙采用而应让用户感受到同等水准的支持无论他们选择何种架构。Spring Modulith正是这条理性道路上的指南针。它不试图让你相信单体胜于微服务也不试图让你相信微服务胜于单体。它的使命很纯粹在你需要单体的时候帮你把单体做干净在你需要微服务的时候让你从干净的单体出发。 参考资源*- Spring Modulith官方文档**- IntelliJ IDEA Spring Modulith支持**- Spring Modulith GitHub仓库**- Spring Modulith示例应用*