DTO/DO/VO分层与拷贝

作者简介:大家好,我是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

进群,大家一起学习,一起进步,一起对抗互联网寒冬

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/256089.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

我的创作纪念日【512】

机缘 学知识 收获 沉下心来安静学习的能力 日常 创作学习 成就 只要在学习&#xff0c;没有混时间就有成就感 憧憬 早日成为一个健康、漂亮、自律的富婆。

Android Studio(3.6.2版本)安装 java2smali 插件,java2smali 插件的使用方法简述

一、Android Studio&#xff08;3.6.2版本&#xff09;安装 java2smali 插件 1、左上角File—>Setting&#xff0c;如下图 2、Setting界面中&#xff1a;点击Plugins—>选择右侧上方Marketplace—>搜索栏输入java2smali&#xff0c;如下图 3、点击Install按钮—>点…

c语言:指针作为参数传递

探究实参与形参它们相互独立 由于主调函数的变量a&#xff0c;b与被调函数的形参x&#xff0c;y它们相互独立。函数 swap 可以修改变量x&#xff0c;y&#xff0c;但是却无法影响到主调函数中的a&#xff0c;b。 现在利用取地址运算符&#xff0c;分别打印它们的首地址&#x…

枚举enum(学习推荐版,通俗易懂)

定义及特点 第一行的列举名称&#xff08;都是常量&#xff09;&#xff0c;代表每个枚举的对象&#xff08;因为枚举不能创建对象&#xff0c;只能依靠罗列名称确定可使用枚举对象个数&#xff09;&#xff0c;这些名称代表的对象可以使用所在枚举类的所有成员变量、成员方法、…

网络编程day3作业

多进程实现TCP并发服务器 #include<myhead.h>#define PORT 8888 #define IP "192.168.125.130"void hadder(int signo) {if(signo SIGCHLD){while(waitpid(-1,NULL,WNOHANG) > 0);} }int information_exchange(int newfd,struct sockaddr_in cin) {char b…

查验身份证c语言

以下是一个简单的C语言程序&#xff0c;用于验证身份证号码的校验码&#xff1a; #include <stdio.h>#include <string.h>int main() { char id[19]; int i, weight[17] {7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2}; int sum 0; char c…

企业微信自动登录自定义系统

方法一&#xff1a;企业微信构造OAuth2链接跳转登录到自定义系统 企业微信自定义应用配置 构造网页授权链接 如果企业需要在打开的网页里面携带用户的身份信息&#xff0c;第一步需要构造如下的链接来获取code参数&#xff1a; https://open.weixin.qq.com/connect/oauth2/…

Elasticsearch 向量相似搜索

Elasticsearch 向量相似搜索的原理涉及使用密集向量(dense vector)来表示文档,并通过余弦相似性度量来计算文档之间的相似性。以下是 Elasticsearch 向量相似搜索的基本原理: 向量表示文档: 文档的文本内容经过嵌入模型(如BERT、Word2Vec等)处理,得到一个密集向量(den…

在openSUSE-Leap-15.5-DVD-x86_64中使用deepin-wine-6.0.0.62再使用微信3.9.5

在openSUSE-Leap-15.5-DVD-x86_64中使用deepin-wine-6.0.0.62再使用微信3.9.5 参考文章&#xff1a; 《记录-下fedora 33安装deepin qq和微信 &#xff0c;不需要安装deepinwine》 https://tieba.baidu.com/p/7279470269 《opensuse使用virtualbox安装win10》 https://blog.c…

简便实用:在 ASP.NET Core 中实现 PDF 的加载与显示

前言 在Web应用开发中&#xff0c;经常需要实现PDF文件的加载和显示功能。本文小编将为您介绍如何在ASP.NET Core中实现这一功能&#xff0c;以便用户可以在Web应用中查看和浏览PDF文件。 实现步骤 1&#xff09;在服务器端创建PDF 打开 Visual Studio 并创建新的 ASP. NET…

PDF转为图片

PDF转为图片 背景pdf展示目标效果 发展过程最终解决方案&#xff1a;python PDF转图片pdf2image注意&#xff1a;poppler 安装 背景 最近接了一项目&#xff0c;主要的需求就是本地的文联单位&#xff0c;需要做一个电子刊物阅览的网站&#xff0c;将民族的刊物发布到网站上供…

Apipost检测接口工具的基本使用方法

&#x1f440; 今天言简意赅的介绍一款和postman一样好用的后端接口测试工具Apipost 专门用于测试后端接口的工具&#xff0c;可以生成接口使用文档官方下载网站&#xff1a;http://www.apipost.cn 傻瓜式安装—>register->项目->创建项目->APIs->新建目录&…

什么是 DDoS ?如何识别DDoS?怎么应对DDOS攻击

什么是DDOS攻击 DDoS攻击&#xff08;Distributed Denial of Service Attack&#xff09;即分布式拒绝服务攻击&#xff0c;是一种利用分布式网络来发起大量的请求&#xff0c;占用目标服务器或网络资源的攻击行为。这种攻击方式可以瘫痪目标系统&#xff0c;导致其无法正常提供…

springboot学习笔记(一)

本期内容&#xff1a; 1.springboot安装 2.springboot Hello world 1.springboot安装&#xff1a; 参考&#xff1a; springboot安装 Spring boot简介及安装 a. eclipse中打开help-->Eclipse Marketplace b. 在search栏目下&#xff0c;输入&#xff1a;spring-tool-…

Redis原理之网络模型笔记

目录 1. 阻塞IO 2. 非堵塞IO 3. IO多路复用 ​3.1 select 3.2 poll 3.3 epoll 4. 信号驱动IO 5. 异步IO 6. Redis是单线程还是多线程 Redis采用单线程模型&#xff0c;这意味着一个Redis服务器在任何时刻都只会处理一个请求。Redis的网络模型涉及到阻塞I/O&#xff08;Blo…

一天吃透Redis面试八股文

目录&#xff1a; Redis是什么&#xff1f;Redis优缺点&#xff1f;Redis为什么这么快&#xff1f;讲讲Redis的线程模型&#xff1f;Redis应用场景有哪些&#xff1f;Memcached和Redis的区别&#xff1f;为什么要用 Redis 而不用 map/guava 做缓存?Redis 数据类型有哪些&…

java SpringCloud版本b2b2c鸿鹄云商平台全套解决方案

使用技术&#xff1a; Spring CloudSpring BootMybatis微服务服务监控可视化运营 B2B2C平台&#xff1a; 平台管理端(包含自营) 商家平台端(多商户入驻) PC买家端、手机wap/公众号买家端 微服务&#xff08;30个通用微服务如&#xff1a;商品、订单、购物车、个人中心、支…

【笑小枫的按步照搬系列】Windows下安装RabbitMQ,图文完整教程

笑小枫的专属目录 1. RabbitMq简介1.1 消息队列中间件简介1.2 什么是RabbitMQ 2. 安装准备工具2.1 百度网盘下载2.2 官网下载erlang2.3 GitHub下载RabbitMQ 3. 安装步骤3.1 erlang安装3.1.1 安装步骤图文讲解3.1.2 环境变量配置图文讲解 3.2 RabbitMq安装3.2.1 解压zip文件到执…

【LeetCode刷题笔记】位运算

231. 2 的幂 解题思路: 1. 除法 , 不断循环判断, 如果能被 2 整除,就不断除以 2 ,直到不能被 2 整除为止,最后结果如果是 1 ,说明可以除尽,是 2 的幂次方,否则就不是。 特判:

北斗三号短报文户外应急产品及应用方案

我国幅员辽阔物产丰富&#xff0c;各大地区生产线都有工人日夜躬耕投入生产&#xff0c;然而与无人区、无网络区域作业人员通信一直是难以解决的技术问题&#xff1b;通信链路不稳定、通信不畅&#xff0c;通信距离短&#xff0c;无法与无人区人员进行实时沟通&#xff0c;对于…