作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬
这一篇其实没太多实质内容,本来不打算写的,但想到当初从传统通信行业跳到互联网公司后,面对突如其来的DTO、DO、VO感到一脸懵逼的自己,还是简单说两句吧...
DTO/DO/VO案例简介
《阿里巴巴开发手册》以及网上各种博客或多或少都有提到诸如DTO、DO、BO、PO、VO等等,也提倡对实体类进行分层。至于为什么要分层,它们的理由是“避免暴露内部设计细节,只展示必要的字段”,但我个人最大的感受其实是“解耦”。我曾遇到一件无奈的事,接口已经开发完毕,前后端也联合好了,结果产品临时要大改,Service层的逻辑基本要推倒重来,连查的表都不一样了。好在得益于DTO和VO的隔离,并没有影响到其它层,前端甚至完全不知道后端全部重写了,Swagger文档也和原来一模一样...
不过个人觉得没必要去扣这些概念,比如BO、PO是啥我也记不清。一般来说,POJO分为三类即可:
- 客户端/前端传入的DTO
- 与数据库字段映射的DO
- 返回给客户端/前端的VO
DO和VO一般没太多争议,至于DTO,有些公司又会细分各种O,至于有没有必要,则是仁者见仁智者见智了。
@Slf4j
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("getUser")
public Result<UserVO> getUser(@Validated UserDTO userDTO) {
return Result.success(userService.getUser(userDTO));
}
}
@Service
public UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
public Result<UserVO> getUser(UserDTO userDTO) {
LambdaQueryWrapper<UserDO> lambdaQuery = Wrappers.lambdaQuery();
queryWrapper.eq(UserDO::getName, userDTO.getName());
queryWrapper.orderByDesc(UserDO::getId);
List<UserDO> userList = userDao.selectList(queryWrapper);
if (CollectionUtils.isEmpty(userList)) {
return Result.success(null);
}
UserVO userVO = new UserVO();
BeanUtils.copyProperties(userList.get(0), userVO);
return Result.success(userVO);
}
}
public interface UserDao extends BaseMapper<UserDO> {
}
不需要考虑转换带来的性能问题,和数据库查询比起来,影响微乎其微
大部分公司都会直接使用Spring提供的BeanUtils进行数据拷贝,但它有两个问题:
- 没有提供额外的映射关系,所以两个实体类字段名必须完全一致
- 不支持深拷贝
市面上有很多比Spring的BeanUtils更强大的转换工具,比如MapStruct啥的,大家可以去了解一下,用法也很简单,这里不再介绍,没太多技术含量。我们尝试自己封装一个MyBeanUtils并解决深拷贝问题,然后讨论一些实际开发中可能遇到的Bean转换的场景。
深浅拷贝的概念
在正式封装MyBeanUtils之前,我们先来聊聊深浅拷贝:
- 什么时候需要考虑深浅拷贝问题:对象内部还有对象
- 定义
-
- 浅拷贝: 不额外创建子对象,只是把子对象的引用拷贝过去
- 深拷贝: 创建新的子对象并拷贝属性
一般来说,绝大部分工具类提供的copy功能都是浅拷贝模式,分两步:
- 先创建一个新的同类型对象
- 把原对象的各个属性值拷贝到对应字段
基于这样的特性,当对象内部出现子对象时,如果不改变拷贝策略,还是原原本本拷贝属性值的话,就会发生下面这种情况:
由于BeanUtils.copyProperties(sourceBean, destBean)只针对Person对象,所以只会把左边Person对象的属性值拷贝到右边,至于子对象department,也只是拷贝了引用。
来验证一下是不是这么回事:
public class MyBeanUtilsTest {
private static List<Person> list;
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Person implements Serializable {
private String name;
private Integer age;
private Department department;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Department implements Serializable{
private String name;
}
static {
list = new ArrayList<>();
list.add(new Person("笑脸", 18, new Department("行政部")));
}
public static void main(String[] args) {
Person bean = list.get(0);
Person copyBean = new Person();
BeanUtils.copyProperties(bean, copyBean);
System.out.println(bean == copyBean);
System.out.println("==== copyBean的属性 ====");
System.out.println(copyBean.getName());
System.out.println(copyBean.getDepartment().getName());
bean.setName("哭脸");
bean.getDepartment().setName("财务部");
System.out.println("==== sourceBean修改后,copyBean的属性 ====");
System.out.println(copyBean.getName());
System.out.println(copyBean.getDepartment().getName());
}
}
结果
false
==== copyBean的属性 ====
笑脸
行政部
==== sourceBean修改后,copyBean的属性 ====
笑脸
财务部
从测试结果来看,浅拷贝确实在内存中新建了一个Person对象,但没有新建department子对象。由于新旧Person的department都指向同一个对象,所以会互相影响。我们期望的结果是,在拷贝Person的同时顺便把里面的Department也拷贝一份,也就是深拷贝:
MyBeanUtils
public final class MyBeanUtils {
public static final Logger LOGGER = LoggerFactory.getLogger(MyBeanUtils.class);
/**
* 浅拷贝
* 将包含BeanA的list,转换为包含BeanB的list。应用场景示例:List<DO>转List<TO>
*
* @param sourceList
* @param targetClass
* @return
*/
public static <T> List<T> copyList(List<?> sourceList, Class<T> targetClass) {
if (sourceList == null) {
return null;
}
List<T> targetList = new ArrayList<T>();
for (Object tempSrc : sourceList) {
try {
T t = copyBean(tempSrc, targetClass);
targetList.add(t);
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
}
return targetList;
}
/**
* 浅拷贝,本质和Spring的BeanUtils一模一样
*
* @param srcBean 待转换Bean
* @param targetClass 目标Bean的Class
* @param <T> 目标Bean
* @return
*/
public static <T> T copyBean(Object srcBean, Class<T> targetClass) {
try {
T t = targetClass.newInstance();
BeanUtils.copyProperties(srcBean, t);
return t;
} catch (Exception e) {
LOGGER.error("copyBean failed");
throw new RuntimeException(e.getMessage());
}
}
/**
* 深拷贝
*
* @param src
* @param <T>
* @return
* @throws IOException
* @throws ClassNotFoundException
*/
public static <T> List<T> deepCopy(List<T> src) throws IOException, ClassNotFoundException {
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(byteOut);
out.writeObject(src);
ByteArrayInputStream byteIn = new ByteArrayInputStream(byteOut.toByteArray());
ObjectInputStream in = new ObjectInputStream(byteIn);
@SuppressWarnings("unchecked")
List<T> dest = (List<T>) in.readObject();
return dest;
}
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
List<Person> copyList = MyBeanUtils.deepCopy(list);
System.out.println("==== copyBean的属性 ====");
System.out.println(list.get(0).getName());
System.out.println(list.get(0).getDepartment().getName());
list.get(0).setName("哭脸");
list.get(0).getDepartment().setName("财务部");
System.out.println("==== sourceBean修改后,copyBean的属性 ====");
System.out.println(copyList.get(0).getName());
System.out.println(copyList.get(0).getDepartment().getName());
}
结果
==== copyBean的属性 ====
笑脸
行政部
==== sourceBean修改后,copyBean的属性 ====
笑脸
行政部
修改原Person的department并不会影响新Person的属性值。还有一种类似的做法:
public static void main(String[] args) throws JsonProcessingException {
// List<Person> copyList = MyBeanUtils.deepCopy(list);
ObjectMapper objectMapper = new ObjectMapper();
String newListStr = objectMapper.writeValueAsString(list);
List<Person> newList = objectMapper.readValue(newListStr, new TypeReference<List<Person>>() {
});
System.out.println("==== copyBean的属性 ====");
System.out.println(list.get(0).getName());
System.out.println(list.get(0).getDepartment().getName());
list.get(0).setName("哭脸");
list.get(0).getDepartment().setName("财务部");
System.out.println("==== sourceBean修改后,copyBean的属性 ====");
System.out.println(newList.get(0).getName());
System.out.println(newList.get(0).getDepartment().getName());
}
通过ObjectMapper或者JSON序列化、反序列化也是可以的,具体性能如何,大家可以自己测试。
如果你们公司没有专门用来拷贝List工具类,那就老老实实写笨代码吧:
public List<UserVO> getList() {
// 查询
List<UserDO> userDOList = userDao.listUser();
// 返回数据
return Optional.of(userDOList).map(userDOList -> {
List<UserVO> userVOList = Lists.newArrayList();
UserVO userVO;
for (UserDO userDO : userDOList) {
userVO = new UserVO();
BeanUtils.copyProperties(userDO, userVO);
userVOList.add(userVO);
}
return userVOList;
}).orElse(Collections.emptyList());
}
public List<UserVO> getList() {
// 查询
List<UserDO> userDOList = userDao.listUser();
if (CollectionUtils.isEmpty(userDoList)) {
return Collections.emptyList();
}
// 返回数据
return userDoList.stream().map(userDO -> {
UserVO userVO = new UserVO();
BeanUtils.copyProperties(userDO, userVO);
return userVO;
}).collect(Collectors.toList());
}
public List<ProductExtendsTO> getList(Integer page, Integer pageSize) {
// 1.查询Product
List<Product> productList = listProduct(page, pageSize);
// 2.取出里面的所有uid(省略null判断)
List<Long> uids = productList.stream()
.map(Product::getUid).collect(Collectors.toList());
// 3.查出userList(省略null判断)
List<User> userList = listUser(uids);
// 4.把List转成Map
Map<Long, User> userMap = new HashMap<Long, User>();
for(userList : user){
userMap.put(user.getId(), user);
}
// 组合并返回数据
List<ProductExtendsTO> result = new ArrayList<>();
productList.foreach(product->{
ProductExtendsTO productExtends = new ProductExtendsTO();
BeanUtils.copyProperties(product, productExtends);
// 根据product的uid从userMap获取user(省略null判断)
User user = userMap.get(product.getUid());
productExtends.setUserAge(user.getUserAge());
productExtends.setUserName(user.getUserName());
result.add(productExtends);
})
return result;
}
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
进群,大家一起学习,一起进步,一起对抗互联网寒冬