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

引言

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

架构设计解析

物理隔离实现

  1. 包层级隔离

    text 复制代码
    com.example.order     # 订单模块根包
    com.example.inventory # 库存模块根包
  2. 内部实现隐藏
    所有具体实现类放入 internal 子包,其他模块无法直接引用

模块通信机制

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

关键注解说明

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

扩展实践建议

  1. 模块文档生成

    java 复制代码
    ApplicationModules modules = ApplicationModules.of(MainApplication.class);
    modules.writeDocumentation(); // 生成模块结构文档
  2. 数据库隔离

    • 每个模块使用独立 Schema
    • 通过 Flyway 管理模块专属迁移脚本
  3. 监控增强

    java 复制代码
    // 跟踪模块间事件流转
    @Bean
    PublishedEventsTracer eventTracer() {
        return new PublishedEventsTracer();
    }

总结

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

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

延伸思考

  • 如何将库存模块逐步演进为独立微服务?
  • 怎样实现模块的数据库事务边界控制?
  • 如何结合 Spring Security 实现模块权限隔离?
相关推荐
m0_736927048 分钟前
2025高频Java后端场景题汇总(全年汇总版)
java·开发语言·经验分享·后端·面试·职场和发展·跳槽
CodeAmaz23 分钟前
自定义限流方案(基于 Redis + 注解)
java·redis·限流·aop·自定义注解
Felix_XXXXL36 分钟前
Plugin ‘mysql_native_password‘ is not loaded`
java·后端
韩立学长39 分钟前
【开题答辩实录分享】以《基于SpringBoot在线小说阅读平台》为例进行答辩实录分享
java·spring boot·后端
悟能不能悟1 小时前
jsp怎么拿到url参数
java·前端·javascript
KWTXX1 小时前
组合逻辑和时序逻辑的区别
java·开发语言·人工智能
高山上有一只小老虎1 小时前
字符串字符匹配
java·算法
程序猿小蒜1 小时前
基于SpringBoot的企业资产管理系统开发与设计
java·前端·spring boot·后端·spring
纪莫1 小时前
技术面:MySQL篇(为啥会有非关系型数据库?MySQL的数据存储一定在磁盘吗?)
java·数据库·java面试⑧股
计算机学姐1 小时前
基于SpringBoot的健身房管理系统【智能推荐算法+可视化统计】
java·vue.js·spring boot·后端·mysql·spring·推荐算法