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 实现模块权限隔离?
相关推荐
用户37215742613510 分钟前
Java Excel转PDF方案分享
java
安然~~~27 分钟前
单例模式的理解
java·单例模式
我会冲击波35 分钟前
Easy Naming for IDEA:从命名到注释,您的编码效率助推器
java·intellij idea
池以遇36 分钟前
云原生高级---TOMCAT
java·tomcat
IT毕设实战小研1 小时前
Java毕业设计选题推荐 |基于SpringBoot的水产养殖管理系统 智能水产养殖监测系统 水产养殖小程序
java·开发语言·vue.js·spring boot·毕业设计·课程设计
小小深1 小时前
Spring进阶(八股篇)
java·spring boot·spring
京东云开发者1 小时前
虚引用GC耗时分析优化(由 1.2 降低至 0.1 秒)
java
Java中文社群1 小时前
求职必备!常用拖Offer话术总结
java·后端·面试
Techie峰2 小时前
Redis Key过期事件监听Java实现
java·数据库·redis
JosieBook2 小时前
【SpringBoot】12 核心功能-配置文件详解:Properties与YAML配置文件
java·spring boot·后端