引言
Spring Modulith 通过物理包隔离和事件驱动通信,为单体应用提供了清晰的模块化方案。本文将构建一个完整的订单系统,包含 订单模块 和 库存模块,通过 200+ 行完整代码演示模块化单体的核心实践。
完整代码目录结构
text
src/main/java/com/example/
├── MainApplication.java # 主启动类
├── order/ # 订单模块
│ ├── OrderModuleApi.java # 模块对外接口
│ ├── events/
│ │ └── OrderCreatedEvent.java
│ └── internal/ # 内部实现(禁止外部引用)
│ ├── Order.java
│ ├── OrderService.java
│ └── OrderController.java
└── inventory/ # 库存模块
├── InventoryModuleApi.java
├── listener/
│ └── OrderEventListener.java
└── internal/
├── InventoryItem.java
└── InventoryService.java
1. 环境准备(pom.xml)
xml
<?xml version="1.0" encoding="UTF-8"?>
<project>
<!-- 继承 Spring Boot -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.0</version>
</parent>
<dependencies>
<!-- Web 支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Modulith 核心 -->
<dependency>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-modulith-starter</artifactId>
<version>1.0.0</version>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
2. 主启动类
java
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}
}
3. 订单模块完整实现
3.1 模块接口定义(API 门面)
java
// OrderModuleApi.java
package com.example.order;
public interface OrderModuleApi {
OrderOperation orders();
interface OrderOperation {
String createOrder(String productId, int quantity);
}
}
3.2 领域事件定义
java
// OrderCreatedEvent.java
package com.example.order.events;
import org.springframework.modulith.events.Externalized;
@Externalized // 标记为跨模块事件
public record OrderCreatedEvent(
String productId,
int quantity
) {}
3.3 内部实现(对外不可见)
java
// Order.java(领域对象)
package com.example.order.internal;
public record Order(
String orderId,
String productId,
int quantity
) {}
java
// OrderService.java(核心服务)
package com.example.order.internal;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import com.example.order.OrderModuleApi;
import com.example.order.events.OrderCreatedEvent;
@Service
public class OrderService implements OrderModuleApi.OrderOperation {
private final ApplicationEventPublisher eventPublisher;
public OrderService(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
@Override
public String createOrder(String productId, int quantity) {
String orderId = java.util.UUID.randomUUID().toString();
Order order = new Order(orderId, productId, quantity);
// 发布领域事件
eventPublisher.publishEvent(
new OrderCreatedEvent(productId, quantity)
);
return orderId;
}
}
java
// OrderController.java(REST 端点)
package com.example.order.internal;
import org.springframework.web.bind.annotation.*;
import com.example.order.OrderModuleApi;
@RestController
public class OrderController {
private final OrderModuleApi.OrderOperation orderOperation;
public OrderController(OrderModuleApi.OrderOperation orderOperation) {
this.orderOperation = orderOperation;
}
@PostMapping("/orders")
public String createOrder(
@RequestParam String productId,
@RequestParam int quantity
) {
return orderOperation.createOrder(productId, quantity);
}
}
4. 库存模块完整实现
4.1 模块接口定义
java
// InventoryModuleApi.java
package com.example.inventory;
public interface InventoryModuleApi {
InventoryQuery inventory();
interface InventoryQuery {
int getStock(String productId);
}
}
4.2 内部实现
java
// InventoryItem.java(库存实体)
package com.example.inventory.internal;
public record InventoryItem(
String productId,
int stock
) {
// 库存扣减方法
public InventoryItem deduct(int quantity) {
return new InventoryItem(productId, stock - quantity);
}
}
java
// InventoryService.java(库存服务)
package com.example.inventory.internal;
import org.springframework.stereotype.Service;
import com.example.inventory.InventoryModuleApi;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class InventoryService implements InventoryModuleApi.InventoryQuery {
private final ConcurrentHashMap<String, InventoryItem> store =
new ConcurrentHashMap<>();
public InventoryService() {
// 初始化测试数据
store.put("P001", new InventoryItem("P001", 100));
store.put("P002", new InventoryItem("P002", 50));
}
// 扣减库存(仅内部调用)
void deductStock(String productId, int quantity) {
store.computeIfPresent(productId, (k, v) -> v.deduct(quantity));
}
@Override
public int getStock(String productId) {
return store.getOrDefault(
productId,
new InventoryItem(productId, 0)
).stock();
}
}
4.3 事件监听器
java
// OrderEventListener.java(跨模块监听)
package com.example.inventory.listener;
import org.springframework.modulith.ApplicationModuleListener;
import org.springframework.stereotype.Component;
import com.example.order.events.OrderCreatedEvent;
import com.example.inventory.internal.InventoryService;
@Component
public class OrderEventListener {
private final InventoryService inventoryService;
public OrderEventListener(InventoryService inventoryService) {
this.inventoryService = inventoryService;
}
@ApplicationModuleListener // 跨模块事件监听注解
void handleOrderCreated(OrderCreatedEvent event) {
inventoryService.deductStock(
event.productId(),
event.quantity()
);
System.out.printf("""
[库存更新] 商品: %s
扣减数量: %d
剩余库存: %d
""",
event.productId(),
event.quantity(),
inventoryService.getStock(event.productId())
);
}
}
5. 集成测试套件
java
package com.example;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.modulith.core.ApplicationModules;
import com.example.inventory.InventoryModuleApi;
import com.example.order.OrderModuleApi;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
class IntegrationTests {
@Autowired
OrderModuleApi.OrderOperation orderOperation;
@Autowired
InventoryModuleApi.InventoryQuery inventoryQuery;
// 测试模块交互
@Test
void testInventoryUpdate() {
int initialStock = inventoryQuery.getStock("P001");
orderOperation.createOrder("P001", 5);
assertThat(inventoryQuery.getStock("P001"))
.isEqualTo(initialStock - 5);
}
// 验证模块结构
@Test
void verifyModuleStructure() {
ApplicationModules modules =
ApplicationModules.of(MainApplication.class);
modules.forEach(System.out::println);
modules.verify(); // 验证模块隔离性
}
}
6. 运行与验证
6.1 启动应用
bash
mvn spring-boot:run
6.2 测试 API
bash
# 创建订单
curl -X POST "http://localhost:8080/orders?productId=P001&quantity=5"
6.3 控制台输出
text
[库存更新] 商品: P001
扣减数量: 5
剩余库存: 95
架构设计解析
物理隔离实现
-
包层级隔离
textcom.example.order # 订单模块根包 com.example.inventory # 库存模块根包
-
内部实现隐藏
所有具体实现类放入internal
子包,其他模块无法直接引用
模块通信机制
- 单向事件驱动
- 订单模块发布
OrderCreatedEvent
- 库存模块通过监听器响应事件
- 订单模块发布
- API 接口契约
- 通过模块接口暴露能力(如
InventoryQuery
) - 禁止直接访问内部 Service 类
- 通过模块接口暴露能力(如
关键注解说明
@Externalized
:标记跨模块传输的事件对象@ApplicationModuleListener
:声明跨模块事件监听@ModuleTest
(进阶):模块切片测试
扩展实践建议
-
模块文档生成
javaApplicationModules modules = ApplicationModules.of(MainApplication.class); modules.writeDocumentation(); // 生成模块结构文档
-
数据库隔离
- 每个模块使用独立 Schema
- 通过 Flyway 管理模块专属迁移脚本
-
监控增强
java// 跟踪模块间事件流转 @Bean PublishedEventsTracer eventTracer() { return new PublishedEventsTracer(); }
总结
通过本案例可以看到,Spring Modulith 实现了:
- 物理可见的模块边界(包即模块)
- 低耦合通信机制(事件驱动 + 接口契约)
- 可测试性保障(模块独立验证)
延伸思考
- 如何将库存模块逐步演进为独立微服务?
- 怎样实现模块的数据库事务边界控制?
- 如何结合 Spring Security 实现模块权限隔离?