【SpringBoot 3.x 第160节】main、app、infra 分模块实践,避免“全模块互相依赖”的烂尾结构,提升构建速度,并建立清晰的模块边界与责任定义,一文给你讲透!
🏆本文收录于《滚雪球学SpringBoot 3.x》,专门攻坚指数提升,本年度国内最系统+最专业+最详细(永久更新)。
该专栏致力打造最硬核 SpringBoot3 从零基础到进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中…欢迎大家订阅持续学习。 如果想快速定位学习,可以看这篇【SpringBoot3教程导航帖】,你想学习的都被收集在内,快速投入学习!!两不误。
若还想学习更多,可直接订阅 《Spring Boot实战合集》,一次订阅,持续学习,后续更新内容无需重复付费,适合长期收藏与系统进阶。
演示环境说明:
- 开发工具:IDEA 2021.3
- JDK版本: JDK 17(推荐使用 JDK 17 或更高版本,因为 Spring Boot 3.x 系列要求 Java 17,Spring Boot 3.5.4 基于 Spring Framework 6.x 和 Jakarta EE 9,它们都要求至少 JDK 17。)
- Spring Boot版本:3.5.4(于25年7月24日发布)
- Maven版本:3.8.2 (或更高)
- Gradle:(如果使用 Gradle 构建工具的话):推荐使用 Gradle 7.5 或更高版本,确保与 JDK 17 兼容。
- 操作系统:Windows 11
全文目录:
-
- 1. 为什么 Spring Boot 3.x 时代更需要做模块治理?
- 2. 先把边界讲清楚:什么是 `api / domain / app / infra`?
- 3. 一张图看懂推荐的依赖方向
- 4. 一个可落地的 Maven 多模块实践
- 5. 一个可落地的 Gradle 多模块实践
- 6. 如何彻底避免“全模块互相依赖”
- 7. 构建速度优化:从“能跑”到“跑得快”
- 8. 以订单系统为例,从接口到落库完整走一遍
- 9. 测试策略:模块内测、模块间测、集成测
- 10. 常见坑位与排查清单
- 11. 你真正应该记住的治理原则
- 12. 附录:推荐的项目骨架与检查表
- 结语
- 🧧 学习福利 · 限时开放 🧧
- 🫵 Who am I?
1. 为什么 Spring Boot 3.x 时代更需要做模块治理?
很多团队第一次接触 Spring Boot 时,都会被它“上手快”的特性吸引:一个启动类、几个 Controller、几个 Service、一个 Repository,项目就跑起来了。短期看,这种方式非常高效;但一旦系统进入稳定迭代阶段,问题也会迅速浮现:目录越来越乱、包越来越深、某个功能改动会牵一大片、测试越来越慢、编译越来越久、很多类彼此都能直接调用,最后谁也说不清边界在哪里。
到了 Spring Boot 3.x,这个问题比以前更值得重视。
第一,Spring Boot 3.x 把运行时和构建期的要求都抬高了。它以 Java 17 为基线,同时对 Jakarta 体系进行了全面切换,这意味着你在升级时,不能只看“代码能不能编译”,还要看“结构是否足够清晰、依赖是否足够健康、模块是否足够独立”。官方在 3.0 版本发布时就明确强调了 Java 17 基线、Jakarta EE 10 / EE 9 baseline、AOT 与原生镜像支持等变化。
第二,Spring Boot 的推荐开发方式本身就是“约定优于配置”。这句话很容易被误解成“少写配置就等于少做治理”。恰恰相反,约定优于配置的前提是团队必须形成统一约定:哪些类放在 API 层,哪些类属于领域层,哪些类只允许做适配,哪些模块可以依赖谁,哪些模块绝不允许反向依赖。Spring Boot 官方文档本身也把构建系统、代码结构、自动配置、依赖注入等内容放在开发最佳实践里,而不是只讲启动器。
第三,现代 Java 项目越来越强调构建效率。无论是 Maven 还是 Gradle,官方文档都把多模块、并行执行、代码复用、构建逻辑共享、增量构建当作核心能力来讲。Maven 官方明确说明,多模块项目不仅能做聚合,还能借助依赖管理保持可复现构建;Gradle 官方也直接把多项目构建、并行执行和代码复用作为组织大型工程的主要方式。
所以,这篇文章不只是教你“怎么拆模块”,更重要的是教你:怎么让模块拆得有意义,拆得有边界,拆得能持续演进。
2. 先把边界讲清楚:什么是 api / domain / app / infra?
在很多项目里,模块名本身没有问题,问题在于名字背后没有责任定义。一个叫 common 的模块,最后常常装下了工具类、DTO、常量、枚举、第三方客户端、数据库实体、业务逻辑,甚至还会出现启动类。这样的模块并不是“公共模块”,而是“垃圾桶模块”。
我们今天采用的是一种更适合中大型 Spring Boot 项目的四层模块拆分方式:
2.1 api:对外契约层
api 负责描述系统对外暴露的“约定”,它通常包含:
- 请求参数对象(Request / Command)
- 响应结果对象(Response / DTO)
- 对外开放的接口定义
- 统一错误码、错误对象
- Feign / RPC 的契约模型(如果有)
api 层的原则非常简单:只描述,不实现。
它不应该关心数据库,不应该关心事务,也不应该关心某个具体仓储怎么实现。它像一份合同,写清楚“你能找我做什么、我会返回什么”。
2.2 domain:领域核心层
domain 是整个系统最重要的模块,它负责承载业务本身。这里一般会放:
- 聚合根、实体、值对象
- 领域服务
- 领域事件
- 仓储接口
- 领域异常
- 业务规则与不变量校验
domain 关注的是“业务是什么”,而不是“怎么存、怎么传、怎么查”。
在一个健康的 Spring Boot 项目里,domain 应该尽量保持纯净,尽量少依赖 Spring 生态中的具体实现。它可以使用少量基础注解,也可以完全不依赖 Spring,从而让单元测试更加轻量,业务逻辑更加稳定。
2.3 app:应用编排层
app 是系统的入口和流程编排层,通常放:
- 启动类
@SpringBootApplication - Web Controller
- Application Service(应用服务)
- 事务边界控制
- 参数装配与结果组装
- 调用
domain与infra的协调代码
app 不是业务规则的所在,它的职责是“把业务流程串起来”。
简单地说,Controller 收到请求后,不应该直接堆满判断逻辑,而应当把请求转换成应用层命令,再由应用层协调领域对象完成业务动作。
2.4 infra:基础设施层
infra 负责和外部世界打交道,它通常包含:
- Repository 实现
- 数据库映射
- Redis、MQ、ElasticSearch 等外部组件适配
- 第三方 API 客户端
- 文件存储、对象存储、短信服务实现
infra 是“怎么做”的世界。它可以依赖 Spring、JPA、MyBatis、Redis Client、HTTP Client,但它不应该反过来侵入领域层。
2.5 为什么要这样拆
这种拆法的核心,不是为了“看起来很架构”,而是为了把变化隔离开:
- 业务规则变化,尽量只影响
domain - 接口协议变化,尽量只影响
api - 流程编排变化,尽量只影响
app - 技术实现变化,尽量只影响
infra
当变化被隔离之后,系统就不会出现“改一个接口,半个项目重编译”的情况。
3. 一张图看懂推荐的依赖方向
先看最核心的结构图。这里的重点不是“模块数量”,而是“依赖方向”。
这张图传达了两个非常重要的原则。
第一,领域层位于中心。 领域层不应该依赖控制器、不应该依赖数据库实现、不应该依赖某个具体外部系统。它只关心业务规则本身。
第二,应用层和基础设施层是外围适配。 它们可以依赖领域层,但领域层不要反向依赖它们。这样,即使你今天把 MyBatis 换成 JPA,或者把 Redis 换成数据库,领域层大概率都不需要改。
如果你只记住一句话,那就是:业务核心要稳定,技术实现要可替换。
4. 一个可落地的 Maven 多模块实践
下面我们用 Maven 来做一个完整的四模块工程。Maven 的优势在于结构清晰、生态成熟、依赖传递和版本管理稳定,适合很多传统 Java 团队。Maven 官方把多模块项目称为聚合工程,父 POM 可以聚合多个模块一起构建;同时,Maven 的依赖管理机制也非常适合大规模多模块工程的版本统一。
4.1 推荐目录结构
order-system/
├── pom.xml # 根聚合 POM
├── api/
│ ├── pom.xml
│ └── src/main/java/com/example/orders/api/...
├── domain/
│ ├── pom.xml
│ └── src/main/java/com/example/orders/domain/...
├── infra/
│ ├── pom.xml
│ └── src/main/java/com/example/orders/infra/...
└── app/
├── pom.xml
└── src/main/java/com/example/orders/app/...
这个结构有两个好处。
一是清晰。每个模块都有自己的定位,不会把所有代码堆到一个项目里。
二是可控。你可以一眼看出谁依赖谁,也能一眼看出哪些模块可以单独测试、单独编译。
4.2 根 pom.xml
根 POM 的作用只有一个:统一管理版本、聚合模块、共享基础配置。 它本身不要承载业务代码。
<!-- 根 pom.xml -->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>order-system</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>api</module>
<module>domain</module>
<module>infra</module>
<module>app</module>
</modules>
<properties>
<!-- 统一 Java 版本,Spring Boot 3.x 推荐 Java 17 起步 -->
<java.version>17</java.version>
<spring-boot.version>3.2.5</spring-boot.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- 统一管理 Spring Boot 相关依赖版本 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
这里要特别注意一件事:根工程最好是聚合 + 统一管理,不要直接塞业务实现。 很多团队把公共代码都堆在根工程,导致根工程既像一个公共库,又像一个业务模块,最后边界越来越模糊。
4.3 api 模块
api 只放契约,不放实现。
api/pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>order-system</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>api</artifactId>
<dependencies>
<!-- 只引入校验 API,不要引入 Web、JPA 等重量级实现依赖 -->
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
</dependency>
</dependencies>
</project>
OrderCreateRequest.java
package com.example.orders.api;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import java.math.BigDecimal;
import java.util.List;
/**
* 下单请求对象
* 说明:只描述外部契约,不承载业务规则实现。
*/
public class OrderCreateRequest {
@NotBlank(message = "用户ID不能为空")
private String userId;
@NotEmpty(message = "订单项不能为空")
private List<Item> items;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public List<Item> getItems() {
return items;
}
public void setItems(List<Item> items) {
this.items = items;
}
public static class Item {
@NotBlank(message = "商品ID不能为空")
private String productId;
@Min(value = 1, message = "购买数量必须大于0")
private int quantity;
private BigDecimal price;
public String getProductId() {
return productId;
}
public void setProductId(String productId) {
this.productId = productId;
}
public int getQuantity() {
return quantity;
}
public void setQuantity(int quantity) {
this.quantity = quantity;
}
public BigDecimal getPrice() {
return price;
}
public void setPrice(BigDecimal price) {
this.price = price;
}
}
}
OrderDTO.java
package com.example.orders.api;
import java.math.BigDecimal;
import java.util.List;
/**
* 订单返回对象
*/
public class OrderDTO {
private String orderId;
private String userId;
private String status;
private BigDecimal totalAmount;
private List<OrderItemDTO> items;
public String getOrderId() {
return orderId;
}
public void setOrderId(String orderId) {
this.orderId = orderId;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public BigDecimal getTotalAmount() {
return totalAmount;
}
public void setTotalAmount(BigDecimal totalAmount) {
this.totalAmount = totalAmount;
}
public List<OrderItemDTO> getItems() {
return items;
}
public void setItems(List<OrderItemDTO> items) {
this.items = items;
}
public static class OrderItemDTO {
private String productId;
private int quantity;
private BigDecimal price;
public String getProductId() {
return productId;
}
public void setProductId(String productId) {
this.productId = productId;
}
public int getQuantity() {
return quantity;
}
public void setQuantity(int quantity) {
this.quantity = quantity;
}
public BigDecimal getPrice() {
return price;
}
public void setPrice(BigDecimal price) {
this.price = price;
}
}
}
4.4 domain 模块
domain 模块是整个工程的核心,尽量少依赖 Spring。它可以定义仓储接口,但不要定义仓储实现。
domain/pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>order-system</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>domain</artifactId>
<dependencies>
<!-- 领域层只依赖 api 中的契约对象,避免反向依赖 app/infra -->
<dependency>
<groupId>com.example</groupId>
<artifactId>api</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
OrderStatus.java
package com.example.orders.domain;
/**
* 订单状态
*/
public enum OrderStatus {
CREATED,
PAID,
CANCELLED
}
OrderItem.java
package com.example.orders.domain;
import java.math.BigDecimal;
/**
* 订单项,属于领域对象
*/
public class OrderItem {
private final String productId;
private final int quantity;
private final BigDecimal price;
public OrderItem(String productId, int quantity, BigDecimal price) {
if (productId == null || productId.isBlank()) {
throw new IllegalArgumentException("商品ID不能为空");
}
if (quantity <= 0) {
throw new IllegalArgumentException("购买数量必须大于0");
}
if (price == null || price.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("商品价格不能为负数");
}
this.productId = productId;
this.quantity = quantity;
this.price = price;
}
public String getProductId() {
return productId;
}
public int getQuantity() {
return quantity;
}
public BigDecimal getPrice() {
return price;
}
public BigDecimal subtotal() {
return price.multiply(BigDecimal.valueOf(quantity));
}
}
Order.java
package com.example.orders.domain;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
/**
* 订单聚合根
*/
public class Order {
private final String id;
private final String userId;
private final List<OrderItem> items;
private OrderStatus status;
private Order(String id, String userId, List<OrderItem> items) {
if (userId == null || userId.isBlank()) {
throw new IllegalArgumentException("用户ID不能为空");
}
if (items == null || items.isEmpty()) {
throw new IllegalArgumentException("订单项不能为空");
}
this.id = id;
this.userId = userId;
this.items = new ArrayList<>(items);
this.status = OrderStatus.CREATED;
}
public static Order create(String userId, List<OrderItem> items) {
return new Order(UUID.randomUUID().toString(), userId, items);
}
public String getId() {
return id;
}
public String getUserId() {
return userId;
}
public List<OrderItem> getItems() {
return Collections.unmodifiableList(items);
}
public OrderStatus getStatus() {
return status;
}
public BigDecimal totalAmount() {
return items.stream()
.map(OrderItem::subtotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
public void markPaid() {
if (status != OrderStatus.CREATED) {
throw new IllegalStateException("只有待支付订单才能支付");
}
this.status = OrderStatus.PAID;
}
public void cancel() {
if (status == OrderStatus.PAID) {
throw new IllegalStateException("已支付订单不能取消");
}
this.status = OrderStatus.CANCELLED;
}
}
OrderRepository.java
package com.example.orders.domain;
import java.util.Optional;
/**
* 领域层仓储接口
* 说明:只定义能力,不关心具体存储方式。
*/
public interface OrderRepository {
void save(Order order);
Optional<Order> findById(String orderId);
}
OrderDomainService.java
package com.example.orders.domain;
import java.util.List;
/**
* 领域服务:处理需要跨多个领域对象的业务规则
*/
public class OrderDomainService {
public Order createOrder(String userId, List<OrderItem> items) {
// 这里可以加入更复杂的业务校验,比如库存、限购、黑名单等
return Order.create(userId, items);
}
}
4.5 infra 模块
infra 模块只负责实现 domain 里定义的接口。
infra/pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>order-system</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>infra</artifactId>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>domain</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
InMemoryOrderRepository.java
package com.example.orders.infra;
import com.example.orders.domain.Order;
import com.example.orders.domain.OrderRepository;
import org.springframework.stereotype.Repository;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
/**
* 内存版仓储实现
* 说明:用于演示模块边界,方便本地直接运行,不依赖数据库。
*/
@Repository
public class InMemoryOrderRepository implements OrderRepository {
private final Map<String, Order> storage = new ConcurrentHashMap<>();
@Override
public void save(Order order) {
storage.put(order.getId(), order);
}
@Override
public Optional<Order> findById(String orderId) {
return Optional.ofNullable(storage.get(orderId));
}
}
4.6 app 模块
app 模块承载 Spring Boot 启动、Web 接口和应用服务。
app/pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>order-system</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>app</artifactId>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>api</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>domain</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>infra</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
启动类 OrderApplication.java
package com.example.orders.app;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Spring Boot 启动类
*/
@SpringBootApplication(scanBasePackages = "com.example.orders")
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
应用服务 OrderAppService.java
package com.example.orders.app;
import com.example.orders.api.OrderCreateRequest;
import com.example.orders.api.OrderDTO;
import com.example.orders.domain.Order;
import com.example.orders.domain.OrderDomainService;
import com.example.orders.domain.OrderItem;
import com.example.orders.domain.OrderRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
/**
* 应用服务:负责流程编排,不写复杂业务规则
*/
@Service
public class OrderAppService {
private final OrderRepository orderRepository;
private final OrderDomainService orderDomainService;
public OrderAppService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
this.orderDomainService = new OrderDomainService();
}
@Transactional
public OrderDTO createOrder(OrderCreateRequest request) {
List<OrderItem> items = request.getItems().stream()
.map(item -> new OrderItem(item.getProductId(), item.getQuantity(), item.getPrice()))
.collect(Collectors.toList());
Order order = orderDomainService.createOrder(request.getUserId(), items);
orderRepository.save(order);
return toDTO(order);
}
@Transactional(readOnly = true)
public OrderDTO getOrder(String orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new IllegalArgumentException("订单不存在: " + orderId));
return toDTO(order);
}
private OrderDTO toDTO(Order order) {
OrderDTO dto = new OrderDTO();
dto.setOrderId(order.getId());
dto.setUserId(order.getUserId());
dto.setStatus(order.getStatus().name());
dto.setTotalAmount(order.totalAmount());
dto.setItems(order.getItems().stream().map(item -> {
OrderDTO.OrderItemDTO itemDTO = new OrderDTO.OrderItemDTO();
itemDTO.setProductId(item.getProductId());
itemDTO.setQuantity(item.getQuantity());
itemDTO.setPrice(item.getPrice());
return itemDTO;
}).collect(Collectors.toList()));
return dto;
}
}
控制器 OrderController.java
package com.example.orders.app;
import com.example.orders.api.OrderCreateRequest;
import com.example.orders.api.OrderDTO;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Web 层只做协议转换,不承载业务规则
*/
@RestController
@RequestMapping("/orders")
public class OrderController {
private final OrderAppService orderAppService;
public OrderController(OrderAppService orderAppService) {
this.orderAppService = orderAppService;
}
@PostMapping
public OrderDTO create(@Valid @RequestBody OrderCreateRequest request) {
return orderAppService.createOrder(request);
}
@GetMapping("/{orderId}")
public OrderDTO detail(@PathVariable String orderId) {
return orderAppService.getOrder(orderId);
}
}
4.7 一个最小可运行的请求示例
启动应用后,可以用下面的 JSON 请求下单:
{
"userId": "U10001",
"items": [
{
"productId": "P1001",
"quantity": 2,
"price": 199.00
},
{
"productId": "P1002",
"quantity": 1,
"price": 99.00
}
]
}
这个请求会经历如下流程:
- Controller 接收并校验请求。
- 应用服务把请求对象转换为领域对象。
- 领域服务创建订单,领域对象计算金额并维护状态。
- 仓储接口由
infra的内存实现保存。 - 应用服务把领域对象转换为返回 DTO。
这样一来,Web、业务、存储三者都被明确分开。
5. 一个可落地的 Gradle 多模块实践
如果你的团队更偏向 Gradle,那么多项目构建会非常适合模块化治理。Gradle 官方把多项目构建定义为一个根项目加多个子项目,并强调这种方式适合模块化、并行执行和代码复用;官方还建议通过合理拆分项目,使独立模块可以独立构建,从而减少不必要的整体重建。
Gradle 的特点是:
- 配置灵活
- 构建逻辑可以很优雅地抽到约定插件
- 增量构建体验通常很好
- 适合大型工程做构建治理
但 Gradle 的代价也很明显:如果团队没有统一约定,build 文件很容易失控。 所以,Gradle 项目更要强调“约定”和“模板化”。
5.1 推荐目录结构
order-system/
├── settings.gradle.kts
├── build.gradle.kts
├── api/
├── domain/
├── infra/
└── app/
5.2 settings.gradle.kts
rootProject.name = "order-system"
include("api", "domain", "infra", "app")
5.3 根 build.gradle.kts
plugins {
id("org.springframework.boot") version "3.2.5" apply false
id("io.spring.dependency-management") version "1.1.4" apply false
java
}
allprojects {
group = "com.example"
version = "1.0.0-SNAPSHOT"
}
subprojects {
apply(plugin = "java")
apply(plugin = "io.spring.dependency-management")
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(17))
}
}
repositories {
mavenCentral()
}
dependencies {
add("testImplementation", platform("org.springframework.boot:spring-boot-dependencies:3.2.5"))
add("testImplementation", "org.springframework.boot:spring-boot-starter-test")
}
tasks.withType<Test> {
useJUnitPlatform()
}
}
5.4 各子模块的依赖关系
api/build.gradle.kts
dependencies {
implementation("jakarta.validation:jakarta.validation-api")
}
domain/build.gradle.kts
dependencies {
implementation(project(":api"))
}
infra/build.gradle.kts
dependencies {
implementation(project(":domain"))
}
app/build.gradle.kts
plugins {
id("org.springframework.boot")
}
dependencies {
implementation(project(":api"))
implementation(project(":domain"))
implementation(project(":infra"))
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-validation")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
5.5 Gradle 的真正优势在哪里?
Gradle 最值得团队关注的,不是“写法更酷”,而是它对大工程的性能和组织能力更友好。你可以把公共构建逻辑抽成 convention plugin,把所有子模块统一套模板;你也可以开启并行构建、构建缓存和配置缓存,让构建在大项目里保持可接受的速度。
但要记住一点:构建工具只是放大器,不是救命药。 如果模块边界本身混乱,再强的构建工具也只能把混乱更快地执行完。
6. 如何彻底避免“全模块互相依赖”
“全模块互相依赖”通常不是某一个人故意写坏架构,而是团队在快速交付中不断妥协的结果。
一开始,某个功能想复用一段代码,于是把它挪到公共模块;后来另一个功能也要用,于是再加一点;再后来某个接口要调数据库,又顺手把 Repository 塞进公共模块;最后所有模块都开始依赖这个公共模块,甚至反向调用彼此的实现类。到了这个阶段,任何一个改动都像拆炸弹。
6.1 常见烂尾结构长什么样
common/
├── dto/
├── entity/
├── util/
├── service/
├── repository/
├── config/
└── controller/
这种结构的问题是:
entity里混入数据库实体和领域实体service里既有业务逻辑又有流程编排repository里既有接口又有实现controller被别的模块直接调用- 所有东西都能被
common引用,最后就变成“谁都能改,谁都不敢删”
6.2 用“禁止反向依赖”做第一道防线
最简单有效的原则就是:下层可以依赖上层的契约,上层不要依赖下层的实现。
更直白一点:
api不依赖appdomain不依赖appdomain不依赖infrainfra只依赖domainapp可以依赖api、domain、infra
只要这条线守住,大多数混乱就不会发生。
6.3 不要把“公共”当成“万能”
公共模块应该很少,而且要极其克制。适合放到公共模块里的,通常只有这几类:
- 与业务无关的基础工具
- 稳定且通用的错误码模型
- 很薄的通用基础设施抽象
- 极少量跨业务的基础类型
不适合放进去的东西更多:
- 具体业务 DTO
- 具体数据库表实体
- 具体某个业务场景的服务实现
- 只在一个模块里使用的枚举和常量
越“公共”的东西,越应该稳定;越不稳定的业务代码,越不应该塞到公共模块里。
6.4 用包名与模块名同时约束
只靠模块拆分还不够。你还要配合包名约束:
com.example.orders.api
com.example.orders.domain
com.example.orders.infra
com.example.orders.app
包名和模块名最好一一对应。这样做的价值在于:
- 读代码时更容易理解归属
- 配合检查工具时更容易写规则
- PR 审查时更容易发现“错放”的类
6.5 用自动化检查守住边界
架构不是靠口头承诺维持的,而是靠工具约束的。你可以引入:
- 架构测试
- 依赖分析插件
- 包结构检查
- CI 里的模块依赖校验
例如,某些规则可以写成“domain 不允许引用 app 包名中的类”“api 不允许依赖 Spring Web 类型”“infra 不允许被 domain 直接引用具体实现”。这些规则一旦落地,架构就能持续稳定。
7. 构建速度优化:从“能跑”到“跑得快”
多模块拆分的目的之一,就是让构建更快。但如果拆完之后构建反而更慢,那说明你只做了形式上的分层,没有做真正的工程治理。
7.1 Maven 的提速思路
Maven 的提速,核心不是“加很多插件”,而是减少无意义的全量构建。
7.1.1 只构建受影响模块
当你只改了 domain 中的某个类,不要每次都从根目录做完全量重建。多模块项目本来就支持按模块构建,CI 里也应该尽量按变更范围触发。
7.1.2 父 POM 只做版本与规则
根 POM 不要引入一大堆业务依赖,更不要把业务 starter 全塞进去。父 POM 越干净,子模块越清晰,依赖解析也越容易。
7.1.3 减少无效传递依赖
很多构建慢,不是因为模块多,而是因为每个模块都在带一堆没必要的传递依赖。你可以检查:
- 哪些依赖只在编译期用
- 哪些依赖只在运行期用
- 哪些依赖只是为了测试
- 哪些依赖根本没用到
依赖越轻,构建越快,Classpath 越短,排查也越容易。
7.1.4 使用 Maven 的局部构建思路
在日常开发里,通常不需要每次都全量构建所有模块。对于改动较小的场景,按模块编译、按模块测试、按模块打包会更高效。大型系统里,这种开发习惯非常重要。
7.2 Gradle 的提速思路
Gradle 在多模块上通常更容易做速度优化,因为它天生就偏向增量和缓存。
7.2.1 并行执行
如果模块之间依赖关系清晰,Gradle 可以并行编译不相关的子项目。模块拆得越合理,并行收益越明显。
7.2.2 构建缓存
重复执行相同任务时,缓存能显著减少构建成本。尤其是 CI 场景,缓存命中率高的时候,速度差异非常明显。
7.2.3 配置缓存
构建配置阶段如果每次都重复扫描大量脚本,会非常慢。配置缓存能帮助减少这部分开销,但前提是你的构建脚本写得足够规范。
7.2.4 约定插件抽公共逻辑
不要把所有 subprojects {} 逻辑都堆在根构建脚本里。应该把公共约定抽成插件,减少重复配置。这样既能提升可维护性,也能减少构建脚本自身的复杂度。
7.3 影响构建速度的真实原因往往不是工具
很多团队总以为构建慢是 Maven 慢、Gradle 慢,其实常常不是。
真正拖慢构建的通常是这些问题:
- 模块之间依赖过密
- 公共模块过大
- 测试代码和生产代码混在一起
- 一个小改动导致大量模块重新编译
- build 文件充满重复和动态逻辑
- 集成测试环境搭建方式过重
所以,想让构建快,第一步不是换工具,而是让工程结构更“局部化”。
7.4 最值得落地的三条优化建议
如果你今天就要开始优化,我建议先做这三件事:
- 删掉不必要的公共模块依赖
- 把只属于某个业务域的代码从 common 拆出去
- CI 改成按变更模块触发构建,而不是每次全量跑
这三件事往往比“再加一个构建插件”更有效。
8. 以订单系统为例,从接口到落库完整走一遍
理论讲完,我们再回到一个最常见的业务:订单创建。
8.1 业务流程
用户提交订单时,系统需要完成这些事:
- 校验请求参数
- 转换为领域对象
- 执行业务规则
- 生成订单编号
- 保存订单
- 返回结果
如果把这些事情都写进 Controller,代码会很快失控。正确方式是:
api负责定义请求与响应app负责流程编排domain负责业务规则infra负责保存与适配
8.2 请求进入后的链路图

这条链路清楚地说明了谁应该负责什么。
8.3 为什么应用服务要存在
很多零基础同学一开始会问:为什么不能 Controller 里直接调领域对象,然后直接返回?
答案是:可以写,但不推荐。
因为 Controller 的职责应该是 HTTP 协议相关的事情:
- 接收参数
- 触发校验
- 返回 HTTP 结果
- 处理协议异常
而应用服务应该做:
- 业务流程编排
- 事务控制
- 领域对象组合
- 外部依赖协调
一旦把这两层混在一起,后续你想接入 MQ、补偿任务、分布式事务、异步处理、权限检查,就会非常难扩展。
8.4 领域层为什么要有自己的规则
假设有一个订单项数量是 0,Controller 层可以做校验,@Min(1) 也可以挡住一部分错误。但这并不意味着领域对象内部可以不做校验。因为领域层面对的并不一定只是 HTTP 请求,还可能是批处理、消息消费、定时任务、测试代码,甚至是别的应用服务构造出来的对象。
所以,外部校验只能减少错误,领域校验才能守住业务不变量。
这也是为什么 OrderItem 构造器里要校验数量和价格,Order 聚合里要校验状态流转。
8.5 一个简单的单元测试示例
为了保证领域规则稳定,我们应该优先测试领域层。
package com.example.orders.domain;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
/**
* 订单领域测试
*/
class OrderDomainServiceTest {
@Test
void should_create_order_and_calculate_total_amount() {
OrderDomainService service = new OrderDomainService();
Order order = service.createOrder(
"U10001",
List.of(
new OrderItem("P1001", 2, new BigDecimal("199.00")),
new OrderItem("P1002", 1, new BigDecimal("99.00"))
)
);
assertEquals("U10001", order.getUserId());
assertEquals(new BigDecimal("497.00"), order.totalAmount());
}
@Test
void should_not_allow_zero_quantity() {
assertThrows(IllegalArgumentException.class, () ->
new OrderItem("P1001", 0, new BigDecimal("199.00"))
);
}
}
领域测试应该尽量轻、快、稳定。只要领域层写得干净,测试就会很好写;反过来,领域层一旦和 Spring、数据库、HTTP 强耦合,测试成本就会飙升。
9. 测试策略:模块内测、模块间测、集成测
模块治理做得好不好,最终要看测试是否容易组织。
9.1 模块内测试:优先测 domain
domain 的测试价值最大,因为这里装的是业务规则。你应该优先写:
- 聚合创建测试
- 状态流转测试
- 值对象校验测试
- 领域服务测试
这类测试不需要启动 Spring 容器,执行快、稳定性高。
9.2 app 层测试:测流程,不测规则
app 层测试的重点是流程编排,例如:
- 接收请求后是否正确调用领域服务
- 结果映射是否正确
- 异常是否正确转换成接口错误
这里不应该重复测试领域规则,否则会造成测试重复。
9.3 infra 层测试:测适配,不测业务
infra 层测试应重点验证:
- Repository 实现是否正确
- 外部系统适配是否符合接口契约
- 序列化、反序列化是否正确
如果用 JPA、Redis、MQ、HTTP Client,这些地方都应该有专门的适配测试。
9.4 集成测试:验证模块拼装是否正确
集成测试的目标不是覆盖所有分支,而是确认模块之间真正能组装起来:
app能否注入domain和infra- 启动类扫描范围是否正确
- 配置是否完整
- 接口调用链是否可用
多模块项目如果没有集成测试,最常见的问题就是:单模块都能跑,拼起来却启动失败。
9.5 不要把所有测试都写成 SpringBootTest
这是很多团队非常常见的浪费。
@SpringBootTest 很方便,但启动成本高。它适合少量关键集成测试,不适合把所有单元测试都改成全量启动。正确做法应该是:
- 单元测试尽量不启动 Spring
- 只有真正需要容器的场景才启动 Spring
- 优先测试领域层,少测技术噪音
这样整个工程的测试速度才会稳。
10. 常见坑位与排查清单
多模块工程最容易踩坑的地方,通常不是语法,而是结构。
10.1 坑一:api 里放了实现类
如果 api 里出现 Controller、RepositoryImpl、ServiceImpl,那么这个模块就不再是契约层,而是混杂层。它会让其他模块误以为可以直接依赖实现,最终把边界彻底打穿。
解决方式: api 只保留 DTO、Command、Response、接口定义、错误模型。
10.2 坑二:domain 依赖 spring-web 或 spring-data
一旦领域层开始依赖这些技术组件,领域层就失去纯粹性了。它会让业务对象和框架耦合,导致测试困难、迁移困难、复用困难。
解决方式: domain 只保留业务模型与接口,不直接依赖框架实现。
10.3 坑三:把“公共工具类”无限膨胀
公共工具类一旦没有边界,就会越长越大,最后变成新的“上帝模块”。
解决方式: 工具类可以有,但必须极少且稳定;业务复用应优先通过领域抽象来解决,而不是靠工具类硬拼。
10.4 坑四:一个模块既负责业务又负责存储
如果某个模块既有复杂业务逻辑,又直接操作数据库实体,那么它就是耦合点。后续任何改动都会牵动整个模块。
解决方式: 让 domain 负责规则,让 infra 负责存储,让 app 负责编排。
10.5 坑五:模块之间直接调用彼此的 Controller
这属于非常危险的设计。Controller 是 HTTP 协议入口,不是给别的模块内部调用的业务 API。
解决方式: 模块间调用应该通过应用服务、领域服务或明确的接口契约,不要跨层直接调 Controller。
10.6 排查清单
每次你怀疑项目开始失控时,可以问自己以下问题:
- 这个类的职责是不是单一的?
- 它所在模块的名字,和它的真实职责一致吗?
- 领域层是否依赖了技术实现?
- 公共模块是不是塞了太多业务东西?
- 改一个业务点,会不会引发大量无关模块重编译?
- 测试是不是越来越依赖 Spring 容器?
如果答案大部分是“是”,那就说明你的模块治理已经开始退化了。
11. 你真正应该记住的治理原则
学多模块工程,不是为了“拆得更多”,而是为了“控制变化”。
原则一:模块是边界,不是目录美化
很多人把多模块理解成“把代码搬到不同文件夹里”。这其实不够。
真正的模块化,意味着:
- 每个模块有独立责任
- 每个模块依赖方向明确
- 每个模块的变化范围可控
- 每个模块都能被单独理解和验证
原则二:领域优先于技术
Spring Boot 提供了很强的自动配置、依赖注入和生态整合能力,但这些能力不应该掩盖业务本身。你先要把业务说清楚,再决定用什么技术来实现。
原则三:契约和实现分离
API、领域模型、基础设施实现,应该尽量分离。这样以后即使协议变了、存储变了、技术栈变了,也不会把整个系统拖下水。
原则四:构建工具服务于结构
Maven 和 Gradle 都很好,但它们解决的是“如何编译和组织构建”,不是“如何替你设计架构”。结构不好,工具越强,混乱传播越快。
原则五:越早治理,成本越低
等到系统已经写成一团,再重构模块,代价会非常高。最理想的做法,是在项目还不大时就把边界约定好,让团队从第一天就沿着正确方向写代码。
12. 附录:推荐的项目骨架与检查表
下面给出一个适合 Spring Boot 3.x 的推荐骨架,你可以直接拿去做初始模板。
12.1 推荐骨架
order-system/
├── pom.xml 或 build.gradle.kts
├── api/
│ ├── src/main/java/com/example/orders/api/
│ └── src/test/java
├── domain/
│ ├── src/main/java/com/example/orders/domain/
│ └── src/test/java
├── infra/
│ ├── src/main/java/com/example/orders/infra/
│ └── src/test/java
└── app/
├── src/main/java/com/example/orders/app/
├── src/main/resources/
└── src/test/java
12.2 检查表
在提交代码前,你可以快速检查这些项:
api是否只包含契约?domain是否只包含业务核心?app是否只做编排与入口?infra是否只做技术适配?- 有没有跨模块反向依赖?
- 有没有把公共模块写成垃圾桶?
- 有没有不必要的全量构建?
- 单元测试是否优先覆盖
domain? - Controller 是否被写成了业务巨石?
- 有没有一处改动引发全局重编译?
12.3 最后再强调一次
多模块工程治理不是“文档工程”,而是“长期可维护性工程”。
当你的项目从单人开发变成多人协作,从一个功能不断扩展成一条业务线,从一次性项目变成长期演进系统时,模块边界会决定你的团队效率,也会决定你的技术债务速度。
Spring Boot 3.x 给了我们更现代的运行时基线,也给了我们更清晰的工程化背景。你越早把模块边界、依赖方向、构建效率和测试策略梳理清楚,后面的升级、扩容、重构、联调,就越从容。
真正成熟的工程,不是代码很多,而是每一层都知道自己该做什么,不该做什么。
结语
如果把这篇文章浓缩成一句话,那就是:
在 Spring Boot 3.x 时代,模块化不是可选项,而是系统长期健康的前提。
你只要把 api / domain / app / infra 的边界守住,把依赖方向守住,把构建和测试的责任拆开,项目就会越来越清晰;反过来,只要一开始图省事,把所有东西混在一起,后面就会花更多时间为混乱买单。
这也是为什么,真正优秀的 Java 工程师,不只是会写业务代码,更会设计工程边界。
…
ok,同学们,本节课就上到这儿,下课~
🧧 学习福利 · 限时开放 🧧
当然,无论你是计算机专业在读学生,还是对编程充满兴趣的入门者,都强烈建议系统学习SpringBoot全体系专栏:👉 「滚雪球学 Spring Boot」;涵盖SpringBoot所有教学内容。
该专栏以“循序渐进 + 实战驱动”为核心理念,从基础到进阶到就业到架构师逐层展开,帮助你快速建立完整的 Spring Boot 技术体系,带你玩转SpringBoot框架。
📌 学习承诺:
通过该专栏,你将能够:
- 快速掌握 Spring Boot 核心开发能力
- 构建完整的后端项目认知体系
- 实现从“入门”到“独立开发”的跃迁
就像“滚雪球”一样,知识不断积累、能力持续放大,实现指数级成长 🚀
最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。
同时欢迎大家关注技术号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新BAT互联网公司面试题、4000G PDF编程电子书、简历模板、技术文章Markdown文档等海量资料。
ps:本文涉及所有源代码,均已上传至Gitee开源,供同学们直接对照学习 Gitee传送门,同时,原创开源不易,欢迎给个star🌟,想体验下被🌟的感jio,非常感谢❗
🫵 Who am I?
我是 bug菌:
- 热活于 CSDN | 稀土掘金 | InfoQ | 51CTO | 华为云开发者社区 | 阿里云开发者社区 | 腾讯云开发者社区 | 开源中国 | 博客园 | 墨天轮 等各大技术社区;
- CSDN 博客之星 Top30、华为云多年度十佳博主&卓越贡献奖、掘金多年度人气作者 Top40;
- CSDN、掘金、InfoQ、51CTO 等平台签约及优质作者;
- 全网粉丝累计 30w+。
更多高质量技术内容及成长资料,可查看这个合集入口 👉 点击查看 👈️
硬核技术号 「猿圈奇妙屋」 期待你的加入,一起进阶、一起打怪升级。
- End -
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)