Spring Modulith 完整实战指南:从零构建模块化订单系统

21 阅读4分钟

引言

Spring Modulith 通过物理包隔离和事件驱动通信,为单体应用提供了清晰的模块化方案。本文将构建一个完整的订单系统,包含 订单模块库存模块,通过 200+ 行完整代码演示模块化单体的核心实践。


完整代码目录结构

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 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. 主启动类

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 门面)

// OrderModuleApi.java
package com.example.order;

public interface OrderModuleApi {
    OrderOperation orders();
    
    interface OrderOperation {
        String createOrder(String productId, int quantity);
    }
}

3.2 领域事件定义

// OrderCreatedEvent.java
package com.example.order.events;

import org.springframework.modulith.events.Externalized;

@Externalized  // 标记为跨模块事件
public record OrderCreatedEvent(
    String productId, 
    int quantity
) {}

3.3 内部实现(对外不可见)

// Order.java(领域对象)
package com.example.order.internal;

public record Order(
    String orderId, 
    String productId, 
    int quantity
) {}
// 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;
    }
}
// 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 模块接口定义

// InventoryModuleApi.java
package com.example.inventory;

public interface InventoryModuleApi {
    InventoryQuery inventory();
    
    interface InventoryQuery {
        int getStock(String productId);
    }
}

4.2 内部实现

// 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);
    }
}
// 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 事件监听器

// 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. 集成测试套件

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 启动应用

mvn spring-boot:run

6.2 测试 API

# 创建订单
curl -X POST "http://localhost:8080/orders?productId=P001&quantity=5"

6.3 控制台输出

[库存更新] 商品: P001 
扣减数量: 5 
剩余库存: 95

架构设计解析

物理隔离实现

  1. 包层级隔离
    com.example.order     # 订单模块根包
    com.example.inventory # 库存模块根包
    
  2. 内部实现隐藏
    所有具体实现类放入 internal 子包,其他模块无法直接引用

模块通信机制

  1. 单向事件驱动
    • 订单模块发布 OrderCreatedEvent
    • 库存模块通过监听器响应事件
  2. API 接口契约
    • 通过模块接口暴露能力(如 InventoryQuery
    • 禁止直接访问内部 Service 类

关键注解说明

  • @Externalized:标记跨模块传输的事件对象
  • @ApplicationModuleListener:声明跨模块事件监听
  • @ModuleTest(进阶):模块切片测试

扩展实践建议

  1. 模块文档生成
    ApplicationModules modules = ApplicationModules.of(MainApplication.class);
    modules.writeDocumentation(); // 生成模块结构文档
    
  2. 数据库隔离
    • 每个模块使用独立 Schema
    • 通过 Flyway 管理模块专属迁移脚本
  3. 监控增强
    // 跟踪模块间事件流转
    @Bean
    PublishedEventsTracer eventTracer() {
        return new PublishedEventsTracer();
    }
    

总结

通过本案例可以看到,Spring Modulith 实现了:

  • 物理可见的模块边界(包即模块)
  • 低耦合通信机制(事件驱动 + 接口契约)
  • 可测试性保障(模块独立验证)

延伸思考

  • 如何将库存模块逐步演进为独立微服务?
  • 怎样实现模块的数据库事务边界控制?
  • 如何结合 Spring Security 实现模块权限隔离?