DTO、DO、VO,你是怎么转换的?_do vo dto 等如何转换,在哪一层进行

2023年,我刚从外包跳到一家新能源车企时,对DTO、DO、VO这些概念是真懵——光听名字就绕,更别说实际用了。后来踩了几次坑才慢慢明白,这些东西看着复杂,核心就俩字: 省事

先说说DTO、DO、VO到底解决啥问题

刚开始写代码,我总喜欢一个实体类用到底:查数据库用它,接口入参用它,返回给前端还用它。直到有一次,产品突然说要大改接口返回字段,我改着改着发现不对劲——数据库实体(比如 UserDO )里有密码、创建时间这些敏感字段,之前直接返回给前端了!更坑的是,因为实体类被Service、Controller、前端共用,改一个字段得全链路检查,差点没改崩。

后来才明白,DTO、DO、VO的分层,本质是 给不同层划清界限

  • DO(Data Object) :和数据库表一一对应,只在Dao层和Service层之间用,里面全是数据库字段(比如 user_id password create_time )。
  • DTO(Data Transfer Object) :前端传给后端的参数载体,比如用户登录时传的 username password ,只包含接口需要的字段,多余的一概不要。
  • VO(View Object) :后端返回给前端的结果,会根据前端需求“裁剪”DO里的字段,比如隐藏密码,只返回 username nickname 这些前端需要展示的。

这么一分层,好处立马就显出来了。就像我之前遇到的产品大改:Service层逻辑全重写了,查的表都换了,但因为Controller层只认DTO和VO,我只要保证入参DTO和返回VO的格式不变,前端完全不用改,Swagger文档也没动——这就是解耦的威力。

其实不用死记BO、PO这些细分概念,日常开发里,把实体类分成这三类基本够用了。咱们看个简单例子: // 前端传参:只需要name(DTO)
public class UserDTO {
private String name;
// getter/setter
}

// 数据库映射:包含id、name、password等(DO)
public class UserDO {
private Long id;
private String name;
private String password;
// getter/setter
}

// 返回给前端:只包含id和name(VO)
public class UserVO {
private Long id;
private String name;
// getter/setter
}

Controller接收DTO,Service把DTO转成DO查库,再把DO转成VO返回——每层各司其职,改起来就不会牵一发而动全身。

对象转换的坑:深浅拷贝

分层后绕不开一个问题:DO、DTO、VO之间总得转换吧?比如把 UserDO 转成 UserVO ,总不能手动一个个set字段(字段多了能写哭)。最常用的就是Spring的 BeanUtils.copyProperties ,但这东西有俩坑,我踩过好几次。

先看个例子:如果 UserDO 里嵌套了一个 Department 对象,用 BeanUtils 拷贝会咋样? // DO里有个子对象
@Data
public class UserDO {
private String name;
private Department dept; // 部门子对象
}

@Data
public class Department {
private String deptName;
}

BeanUtils.copyProperties(do, vo) 拷贝后,如果你改了原 UserDO 里的 dept.deptName ,会发现 UserVO 里的 dept.deptName 也跟着变了!这就是 浅拷贝 的问题:它只拷贝对象本身的字段,但对子对象(比如 dept )只拷贝引用,原对象和新对象的子对象其实指向同一个内存地址。

那啥是 深拷贝 ?就是不仅拷贝主对象,连里面的子对象也一起复制一份,两边改了互不影响。比如用序列化的方式:把对象转成字节流,再读回来,相当于重新创建了一个完全独立的对象。

自己封装个MyBeanUtils,解决这些破事

既然Spring的 BeanUtils 不够用,不如自己封装一个工具类,既支持浅拷贝,也能搞定深拷贝,还能批量转换List(日常开发里转List的场景太多了)。

1. 浅拷贝:应付简单对象

大部分时候,对象里没有子对象,浅拷贝就够用了。直接封装一个 copyBean 方法,再扩展一个 copyList 批量转换: public final class MyBeanUtils {
private static final Logger log = LoggerFactory.getLogger(MyBeanUtils . class ) ;

// 单个对象浅拷贝
public static T copyBean (Object source, Class targetClass) {
try {
T target = targetClass.newInstance(); // 新建目标对象
BeanUtils.copyProperties(source, target); // 拷贝属性
return target;
} catch (Exception e) {
log.error( "对象拷贝失败" , e);
throw new RuntimeException( "对象转换出错" );
}
}

// List批量浅拷贝(比如List转List)
public static List copyList (List sourceList, Class targetClass) {
if (sourceList == null ) return null ;

List targetList = new ArrayList<>();
for (Object source : sourceList) {
targetList.add(copyBean(source, targetClass));
}
return targetList;
}
}

用起来贼简单,一行代码搞定: // 单个对象转换
UserVO vo = MyBeanUtils.copyBean(userDO, UserVO . class ) ;

// List转换
List voList = MyBeanUtils.copyList(doList, UserVO . class ) ;

2. 深拷贝:处理嵌套对象

如果对象里有子对象,就得用深拷贝了。最常用的方式是序列化(要求对象实现 Serializable ): public final class MyBeanUtils {
// 深拷贝(基于序列化)
public static List deepCopy (List sourceList) {
try {
// 序列化:把对象转成字节流
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(byteOut);
out.writeObject(sourceList);

// 反序列化:把字节流转回对象(全新对象)
ByteArrayInputStream byteIn = new ByteArrayInputStream(byteOut.toByteArray());
ObjectInputStream in = new ObjectInputStream(byteIn);
return (List) in.readObject();
} catch (Exception e) {
log.error( "深拷贝失败" , e);
throw new RuntimeException( "深拷贝出错" );
}
}
}

或者用JSON工具(比如Jackson)也能实现深拷贝,原理差不多:把对象转成JSON字符串,再解析成新对象,缺点是性能比序列化稍差,但胜在不用实现 Serializable // 用Jackson做深拷贝
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(sourceList);
List newList = mapper.readValue(json, new TypeReference>() {});

实际开发中常见的转换场景

除了简单的DO转VO,还有些场景也很常用,比如:

1. 组合多个对象到一个TO里

比如查商品列表时,需要把商品信息( ProductDO )和对应的用户信息( UserDO )合并到 ProductExtendsTO 里返回: public List getProductList () {
// 1. 查商品列表
List productList = productDao.list();
if (CollectionUtils.isEmpty(productList)) {
return Collections.emptyList();
}

// 2. 批量查商品对应的用户(避免N+1查询)
List userIds = productList.stream()
.map(ProductDO::getUserId)
.collect(Collectors.toList());
List userList = userDao.listByIds(userIds);
// 转成Map,方便根据userId快速取用户
Map userMap = userList.stream()
.collect(Collectors.toMap(UserDO::getId, u -> u));

// 3. 组合数据到TO
return productList.stream().map(product -> {
ProductExtendsTO to = new ProductExtendsTO();
// 拷贝商品基本信息
MyBeanUtils.copyBean(product, to);
// 从userMap里取用户信息,设置到TO里
UserDO user = userMap.get(product.getUserId());
if (user != null ) {
to.setUserName(user.getName());
to.setUserAge(user.getAge());
}
return to;
}).collect(Collectors.toList());
}

2. 空集合处理

转List时一定要注意空指针!如果原List是 null ,直接遍历会报错。可以用 Optional 或者先判断空: // 安全的List转换
public List getUserVos (List doList) {
// 用Optional避免null,空集合返回空List而非null
return Optional.ofNullable(doList)
.map(list -> list.stream()
.map(doObj -> MyBeanUtils.copyBean(doObj, UserVO . class ))
. collect ( Collectors . toList ()))
. orElse ( Collections . emptyList ())
;
}

最后说句大实话

其实不用纠结工具类的性能——和数据库查询、网络请求比起来,对象转换的耗时几乎可以忽略。日常开发里,能少写重复代码、少踩坑才是关键。

如果团队里没有统一的转换工具,Spring的 BeanUtils +自己封装的 copyList 就够用了;如果字段映射复杂(比如字段名不一样),可以试试MapStruct(编译时生成转换代码,性能好还支持自定义映射)。

说到底,DTO、DO、VO这些“O”和转换工具,都是为了让代码更规整、改起来更省心。刚开始可能觉得麻烦,但用熟了就会发现:真香!


原文链接:,转发请注明来源!