一、概念理解
Domain(领域对象 ):DDD 的核心,承载业务逻辑和领域规则,是业务的 "灵魂",仅存在于领域层;
PO(持久化对象) :与数据库表一一映射,是数据持久化的载体,仅存在于基础设施层(持久化层);
DO(领域对象,狭义) :实际开发中常作为Domain 的简写 / 落地形式,等同于聚合根、实体、值对象,聚焦领域逻辑;
DTO(数据传输对象):DTO 的唯一合法流转范围是「接口层↔应用层」,这也是它的核心设计初衷:作为外部系统(前端 / 第三方服务)与应用内部的 "数据隔离层",屏蔽外部数据格式变化对内部层级的影响。DTO 仅包含外部交互的必要字段,无任何业务逻辑,入参 DTO 做参数校验(如 @NotBlank/@NotNull),出参 DTO 做数据裁剪 / 脱敏 / 格式适配(如枚举转中文、多 DO 字段合并)
二、实际项目上看完整的 RESTful 请求对象流转流程
1. 接口层:接收请求(DTO 入参)
java
// 接收前端传来的 RequestDTO(数据传输对象)
@PostMapping("/test")
public BaseResponseDTO<List<ListedTestProduct>> getTestData(@RequestBody TestRequestDTO query) {
BaseResponseDTO<List<ListedTestProduct>> resp = new BaseResponseDTO<>();
return resp.setResults(testService.syncTestInfo(query));
}
输入对象:TestRequestDTO(数据传输对象,用于接口层和应用层之间传参)
职责:仅作为数据容器,不包含任何业务逻辑。
2. 应用层:接收请求并转换为领域对象(DO)
java
public List<ListedTestProduct> syncTestInfo(TestRequestDTO request) {
return assembler.assemble2ListedTestProduct(
assembler.assemble(request, TestInfoDO.class).getTestDatas()
);
}
转换动作:assembler.assemble(request, TestInfoDO.class)
底层逻辑:在IAssembler接口中,通过BeanUtils.copyProperties将TestRequestDTO的属性复制到TestInfoDO(领域对象)。
java
default D assemble(Q req, Class<D> dClass) {
D d = DomainFactory.create(dClass);
BeanUtils.copyProperties(req, d);
return d;
}
3. 领域层:执行业务逻辑并返回领域对象
java
// TestInfoDO.getTestDatas() 方法
public List<TestInfoDO> getTestDatas() {
List<TestInfoDO> tests = testConvertor.convert(
testRepository.selectLatestTestData(condition(this))
);
// 业务逻辑
}
return tests ;
}
核心职责:TestInfoDO(领域对象)封装了业务逻辑,负责数据过滤和计算。
流转结果:返回处理后的List。
4. 应用层:领域对象转换为出参对象
java
// assembler.assemble2ListedTestProduct 方法
public List<ListedProduct> assemble2ListedTestProduct(List<TestInfoDO> tests) {
List<ListedProduct> pList = Lists.newLinkedList();
tests.forEach(test-> {
// 业务逻辑映射
pList.add(...);
});
return pList;
}
转换动作:将领域对象TestInfoDO转换为给前端的出参对象ListedProduct(可以看作是一种特殊的 DTO)。
- 接口层:封装响应并返回
java
// 封装为 BaseResponseDTO 返回给前端
return resp.setResults(testService.syncInfo(query));
输出对象:BaseResponseDTO<List>(统一响应格式,包含业务结果和元信息)。
三、完整流转链路总结:
请求正向流转(接口层→基础设施层)+ 响应反向流转(基础设施层→接口层)
plaintext
外部系统(前端/第三方)
↓↑ (JSON/HTTP协议)
接口层:RequestDTO ↔ ResponseDTO (仅此处用DTO)
↓↑ (Assembler转换器转换)
应用层:RequestDTO → 领域对象DO / 领域对象DO → ResponseDTO
↓↑ (直接传递/领域层内部处理,无转换)
领域层:领域对象DO(聚合根/实体/值对象)
↓↑ (Convertor转换器转换)
基础设施层:领域对象DO → PO / PO → 领域对象DO
↓↑ (MyBatis映射,直接交互)
数据库/缓存(MySQL/Redis)
各层对象职责明确: DTO(跨外部传输)、DO(承载业务逻辑)、PO(数据库持久化)三者各司其职,不交叉、不替代;
转换动作统一管控: 接口层↔应用层的转换由Assembler负责,领域层↔基础设施层的 DO/PO 互转由Convertor负责(如MapStruct 自动生成的 Convertor),转换逻辑集中管理,不散落 在业务代码中。
四、DDD 对象转换的核心规范
结合你的项目和 DDD 最佳实践,我帮你整理了一套可以直接落地的对象转换规范:
- 分层隔离原则
禁止跨层传递对象:接口层不能直接接收领域对象,领域层也不能返回 DTO。
层间转换必须通过转换器:所有对象转换都要通过专门的Assembler或Converter来完成,避免在业务代码中写硬编码的属性映射。
示例:项目中TestAssembler就是很好的实践,它统一了DTO ↔ DO ↔ 出参对象的转换逻辑。 - 转换器职责单一原则
一个转换器负责一类对象的转换:比如TestAssembler只负责和TestInfoDO相关的转换。
转换逻辑集中管理:避免在Service或Controller中零散地写BeanUtils.copyProperties。 - 领域对象纯粹性原则
领域对象只承载业务逻辑:不能包含任何和数据传输、持久化相关的代码。
禁止在领域对象中直接依赖 DTO/PO:项目中TestInfoDO只依赖领域层的TestRepository。 - DTO 设计原则
入参 DTO(RequestDTO):仅包含接口需要的字段,避免冗余。比如TestRequestDTO只定义了查询所需的参数。
出参 DTO(ResponseDTO):仅返回前端需要的字段,避免数据泄露。比如ListedTestProduct只包含前端展示的字段。
统一响应包装:所有接口返回都用BaseResponseDTO统一包装,包含状态码、消息和结果。 - 转换工具选择原则
简单映射用BeanUtils:对于属性名基本一致的简单转换,可以用BeanUtils.copyProperties。
复杂映射手动实现:对于需要类型转换、字段拼接的复杂场景,必须手动写转换逻辑。
当进行对象之间的转换时, 也可以用MapStruct(下述有简单介绍)。
五、MapStruct
MapStruct 是一款基于注解的 Java 类型安全映射工具,通过在编译期自动生成映射实现类,替代手动编写繁琐的 Bean 属性拷贝代码。MapStruct 是编译期工具,而非运行时框架,编译时(mvn compile/IDE 编译),处理器会扫描带 @Mapper 注解的接口,自动生成具体的实现类(如 UserMapperImpl);
用法:
(1)依赖
xml
<!-- MapStruct 核心注解 -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.5.Final</version> <!-- 稳定版,兼容大部分场景 -->
</dependency>