1. 概述
微服务架构近年来已成为主流。其核心特征之一是模块化、隔离性强、易于扩展。✅
但微服务并非孤岛,它们需要协同工作并交换数据。为了实现这一点,我们通常会定义数据传输对象(DTO)作为服务间通信的载体。
本文将探讨在微服务架构中,如何合理地共享 DTO,避免常见的“踩坑”问题。⚠️
2. 领域对象与 DTO 的分离
微服务通常围绕业务领域建模,领域模型(Domain Model)封装了核心业务逻辑。为了避免将领域复杂性暴露给外部,我们不会直接暴露领域对象。
取而代之的是:
✅ 通过 REST 接口暴露 DTO
✅ 在服务内部将 DTO 转换为领域对象进行处理
这种分层设计能有效解耦外部接口与内部实现,提升系统可维护性。
下图展示了典型的分层架构中,DTO 与领域对象的流转关系:
该图来自 Xebia 的服务门面与 DTO 实践,清晰地展示了服务层、DTO 与领域模型之间的协作流程。
3. 微服务间 DTO 的共享方式
以“用户下单”为例,涉及 Customer
和 Order
两个服务。Customer 服务需要调用 Order 服务创建订单。
假设请求数据如下:
"order": {
"customerId": 1,
"itemId": "A152"
}
服务之间通过契约(Contract)进行通信,通常以 JSON 格式传输。在 Java 中,这个契约由 OrderDTO
类表示:
public class OrderDTO {
private int customerId;
private String itemId;
// constructor, getters, setters
}
💡 除了 JSON,也可使用 Protocol Buffer(Protobuf)定义契约。此时需共享
.proto
文件,用于生成客户端桩代码(Client Stubs)。
3.1 使用客户端模块(Client Module)共享 DTO
当多个服务需要调用同一服务时,简单地将 DTO 放在公共库中会带来严重问题。
问题场景
假设有一个支付服务,它需要的 CustomerDTO
是:
public class CustomerDTO {
private String firstName;
private String lastName;
private String cardNumber;
// getters, setters
}
而另一个配送服务需要的 CustomerDTO
是:
public class CustomerDTO {
private String firstName;
private String lastName;
private String homeAddress;
private String contactNumber;
// getters, setters
}
如果共用一个 CustomerDTO
,字段会不断膨胀,导致“一个 DTO 被多方绑架”的问题。❌
解决方案:为每个微服务创建独立的 client 模块
推荐做法是:
✅ 每个微服务拆分为 client
和 server
两个模块
结构如下:
order-service
|__ order-client
|__ order-server
order-client
模块包含:
order-service
└──order-client
OrderClient.java
OrderClientImpl.java
OrderDTO.java
其中:
OrderClient
是调用接口:
public interface OrderClient {
OrderResponse order(OrderDTO orderDTO);
}
- 实现类使用
RestTemplate
发起调用:
String serviceUrl = "http://localhost:8002/order-service";
OrderResponse orderResponse = restTemplate.postForObject(
serviceUrl + "/create",
request,
OrderResponse.class
);
customer-service
引入order-client
作为依赖:
[INFO] --- maven-dependency-plugin:3.1.2:list (default-cli) @ customer-service ---
[INFO] The following files have been resolved:
[INFO] com.baeldung.orderservice:order-client:jar:1.0-SNAPSHOT:compile
order-server
提供对应的接口:
@PostMapping("/create")
public OrderResponse createOrder(@RequestBody OrderDTO request) {
// 处理逻辑
}
这种方式的优势
✅ 高内聚低耦合:每个 client 模块只服务于特定调用场景
✅ 契约变更可控:DTO 修改仅影响使用该 client 的服务
✅ 避免代码冗余:无需在每个调用方重复定义 DTO
✅ 易于测试:client 模块可独立测试和版本管理
⚠️ 注意:client 模块应保持轻量,仅包含 DTO 和调用接口,避免引入过多业务逻辑。
4. 总结
在微服务架构中,DTO 的共享方式直接影响系统的可维护性和扩展性。简单粗暴地将 DTO 放入公共库是典型的反模式。❌
推荐做法是:
✅ 为每个微服务创建独立的 client
模块,封装 DTO 与调用逻辑
✅ 将 client 模块作为依赖引入调用方,实现契约的“按需共享”
这种方式带来的好处显而易见:
- ✅ 消除 DTO 代码重复
- ✅ 契约变更影响范围可控
- ✅ 提升服务间的解耦程度
配套的 Spring Boot 示例代码已发布在 GitHub:
https://github.com/eugenp/tutorials/tree/master/spring-cloud-modules/spring-cloud-bootstrap