zoukankan      html  css  js  c++  java
  • 无聊的笔记:之二(再来看看到底怎么高性能的使用BeanUtils)

    简介

    可以跳过直接看测试结果

    现在开发一个系统,常常会用到各种各样的模式(MVC,MVP,MVVM...等等)。就算没有全用过,也至少听说过用MVC模式来开发系统。
    这时候就会用到各种 领域模型 (大佬总是喜欢用这么高大上的名字,个人理解就是有特殊用途或者特殊命名规范的java类,比如:DO,VO,DTO...等等)。这些类在各个层中,当作参数传入,或者当作返回值返回。

    常见的领域模型:

    名称 描述
    DTO (Data Transfer Object) 数据传输对象 一般前端传输到后端的值可以封装成要给DTO对象,用过SpringMVC就知道@ReqeustBody后面就是一个对象。或者在不同服务间传输数据,可以封装成一个DTO。
    VO(Value Object/View Object) 值对象/表现层对象 主要封装了前端页面显示需要的一些数据
    AO(Application Object) 应用对象 在Web层与Service层之间抽象的复用对象模型,极为贴近展示层,复用度不高。(PS:不是很清除这个对象有什么必要。)
    BO(Business Object) 业务对象 业务逻辑封装对象,可能包含多个对象。
    PO(Persistent Object)/DO( Data Object) 持久对象 /数据源对象 一般用于表示数据库表格中的一条数据,从数据库中取出的数据用这个对象表示。如:UserPO/UserDO
    POJO(plain ordinary java object) 简单无规则 java 对象 就一个Java类,其实上面的几种都可以是一个POJO。

    可能不同的地方有不同的领域模型规范。这里仅供参考。

    这样就涉及到各种对象的属性拷贝(或者领域对象的转换)。比如 UserDTO -> UserVO , UserVO -> UserDTO
    当类的属性比较少的时候,我们可以这样:

        /**
         * 将 UserDTO转换成 UserVO
         */
        public static UserVO userConvert(UserDTO userDTO){
            UserVO userVO = new UserVO();
            userVO.setUserName(userDTO.getUserName());
            userVO.setAge(userDTO.getAge());
            userVO.setAddress(userDTO.getAddress());
            userVO.setSex(userDTO.isSex());
            userVO.setProperties(userDTO.getProperties());
            return userVO;
        }
    

    但是如果类的属性较多,就要写很多行的getter/setter的调用。而且这玩意写的到处都是也不好看。
    这时候就可以用几个工具来帮我去掉这些“丑”代码。

    常用的属性拷贝工具:

    • org.apache.commons.beanutils.BeanUtils.copyProperties
    • org.apache.commons.beanutils.PropertyUtils.copyProperties
    • org.springframework.beans.BeanUtils.copyProperties
    • org.springframework.cglib.beans.BeanCopier.copy
    • org.mapstruct

    当然可能还有其他的,这里不再多讨论

    分析

    可以跳过这里,直接看测试结果和结论。

    为什么apache的工具包性能差

    大家都说org.apache.commons.beanutils.BeanUtils.copyProperties的性能差。
    这里截取一段org.apache.commons.beanutils.BeanUtils工具包里的一段代码来瞅瞅:

    public class BeanUtilsBean {
    
         //省略其他代码...
     
          public void copyProperties(Object dest, Object orig) throws IllegalAccessException, InvocationTargetException {
            if (dest == null) {
                throw new IllegalArgumentException("No destination bean specified");
            } else if (orig == null) {
                throw new IllegalArgumentException("No origin bean specified");
            } else {
                if (this.log.isDebugEnabled()) {
                    this.log.debug("BeanUtils.copyProperties(" + dest + ", " + orig + ")");
                }
    
                int var5;
                int var6;
                String name;
                Object value;
                if (orig instanceof DynaBean) {
    
                   //省略代码...
    
                } else if (orig instanceof Map) {
                    
                   //省略代码           
          
                } else {
    
                    //省略代码
    
                }
    
            }
        }
    
        //省略其他代码...
    
    }
    

    可以看到上面的代码充斥着各种类型判断,如orig instanceof DynaBean。还会有日志输出。。。。这些虽然功能很全,但是损耗了大量的效率。再加上使用的是反射,效率当然高不上去。

    为什么Spring的BeanUtils比Apache的BeanUtils效率高。

    Spring的BeanUtils也是使用的反射,但是没有很多的额外操作(比如上面的日志、类型检测等等)。所以效率会高很多。

    为什么Cglib的BeanCopier效率高

    对Cglib就不看源码了,我也讲不清。只能简单的描述一下。

    BeanCopier是一种基于字节码的方式,其实就是在字节码层面生成了性能最好的get、set方式。
    简单的说,就是Cglib帮你手写了vo.setValue(dto.getValue())。所以性能很快,但是Cglib帮你创建这些东西是很浪费时间的,也就是调用BeanCopier beanCopier = BeanCopier.create(...);这个方法会很费时,所以不要频繁的调用这个方法,最好把创建好的一个当作静态变量缓存起来,或者设计个单例模式。

    为什么mapstruct效率高

    我们在使用mapstruct的时候需要写一个映射接口。如下代码:
    UserMapper.java

    import org.mapstruct.Mapper;
    @Mapper
    public interface UserMapper {
        UserVO toVO(UserDTO dto);
    }
    

    上面的代码是将 UserDTO 的属性复制成 UserVO。(详细的可以看下面的测试)

    然后我们编译完成后看编译结果,也就是.class文件。

    UserMapper.class
    UserMapperImpl.class
    

    可以看到我并没有编写UserMapperImpl.java文件,却出现了UserMapperImpl.class。这其实就是mapstruct帮我生成的实现类。
    用IDEA(或者其他的能反编译字节码的工具)把这个class文件打开看看mapstruct到底生成了什么。

    public class UserMapperImpl implements UserMapper {
        public UserMapperImpl() {
        }
    
        public UserVO toVO(UserDTO dto) {
            if (dto == null) {
                return null;
            } else {
                UserVO userVO = new UserVO();
                userVO.setUserName(dto.getUserName());
                userVO.setAge(dto.getAge());
                userVO.setSex(dto.isSex());
                userVO.setAddress(dto.getAddress());
                userVO.setProperties(dto.getProperties());
                return userVO;
            }
        }
    }
    

    这就是mapstruct帮我们在编译器生成的代码。跟手写getter/setter一样了,能达到跟手写一样的效率。(NB)

    mapstructCglib BeanCopier

    这两个工具都是帮我们自动生成代码,不过前者是在编译期生成,后者是在程序运行的时候当调用了BeanCopier.create(...)才生成。

    • mapstruct需要额外的写接口,例子中的UserMapper.java。
    • Cglib BeanCopier需要调用方法去创建。(避免多次的调用create方法)。

    测试环境

    • 系统:windows10 x64
    • 处理器:AMD Ryzen 3
    • 内存:16G
    • 台式电脑
    • JDK1.8

    测试代码

    • UserDTO.java
    public class UserDTO {
    
        private String userName;
        private int age;
        private boolean sex; //性别:true = 男;false = 女;
        private String address ;
        private Object properties;//其他属性
        
        //省略 getter/setter
    }
    
    • UserVO.java
    public class UserVO {
    
        private String userName;
        private int age;
        private boolean sex; //性别:true = 男;false = 女;
        private String address ;
        private Object properties;//其他属性
        
        //省略 getter/setter 
    }
    
    • UserMapper.java

    mapstruct复制属性跟其他几个工具包不一样需要手动编写一个转换接口,在编译器会自动实现接口中的转换方法。

    import org.mapstruct.Mapper;
    
    @Mapper
    public interface UserMapper {
        UserVO toVO(UserDTO dto);
    }
    
    
    • Tester.java

    测试代码

    public class Tester {
    
        private final int count = 10000000;//转换次数
    
        private UserDTO getDTO(){
            UserDTO userDTO = new UserDTO();
            userDTO.setUserName("bob");
            userDTO.setAge(18);
            userDTO.setAddress("china");
            userDTO.setSex(true);
            return userDTO;
        }
    
        /**
         * 手动
         */
        @Test
        public void manual(){
            UserDTO dto = getDTO();
            long startTime = System.currentTimeMillis();
            List<UserVO> vos = new ArrayList<UserVO>(count);
            for(int i = 0 ; i < count ; ++i){
                UserVO userVO = new UserVO();
                userVO.setUserName(dto.getUserName());
                userVO.setAge(dto.getAge());
                userVO.setAddress(dto.getAddress());
                userVO.setSex(dto.isSex());
                userVO.setProperties(dto.getProperties());
                vos.add(userVO);
            }
            System.out.println("用时:"+(System.currentTimeMillis() - startTime));
        }
        /**
         * 测试 mapstruct
         */
        @Test
        public void mapstruct()throws Exception{
            UserMapper struct = Mappers.getMapper(UserMapper.class);
    
            UserDTO dto = getDTO();
            long startTime = System.currentTimeMillis();
            List<UserVO> vos = new ArrayList<UserVO>(count);
            for(int i = 0 ; i < count ; ++i){
                //mapstruct
                UserVO vo = struct.toVO(dto);
                vos.add(vo);
            }
            System.out.println("用时:"+(System.currentTimeMillis() - startTime));
        }
    
        /**
         * 测试 BeanCopier
         */
        @Test
        public void beanCopier()throws Exception{
    
            UserDTO dto = getDTO();
            BeanCopier beanCopier = BeanCopier.create(UserDTO.class,UserVO.class,false);
            long startTime = System.currentTimeMillis();
            List<UserVO> vos = new ArrayList<UserVO>(count);
            for(int i = 0 ; i < count ; ++i){
                //BeanCopier
                UserVO vo = new UserVO();
                beanCopier.copy(dto,vo,null);
                vos.add(vo);
            }
            System.out.println("用时:"+(System.currentTimeMillis() - startTime));
        }
    
        /**
         * 测试spring BeanUtils
         */
        @Test
        public void springBeanUtils()throws Exception{
            UserDTO dto = getDTO();
            long startTime = System.currentTimeMillis();
            List<UserVO> vos = new ArrayList<UserVO>(count);
            for(int i = 0 ; i < count ; ++i){
                UserVO vo = new UserVO();
                //spring BeanUtils
                org.springframework.beans.BeanUtils.copyProperties(dto,vo);
                vos.add(vo);
            }
            System.out.println("用时:"+(System.currentTimeMillis() - startTime));
        }
    
        /**
         * 测试 Apache BeanUtils
         */
        @Test
        public void apacheBeanUtils()throws Exception{
            UserDTO dto = getDTO();
            long startTime = System.currentTimeMillis();
            List<UserVO> vos = new ArrayList<UserVO>(count);
            for(int i = 0 ; i < count ; ++i){
                UserVO vo = new UserVO();
                //Apache BeanUtils
                org.apache.commons.beanutils.BeanUtils.copyProperties(vo,dto);
                vos.add(vo);
            }
            System.out.println("用时:"+(System.currentTimeMillis() - startTime));
        }
    
        /**
         * 测试 Apache PropertyUtils
         */
        @Test
        public void apachePropertyUtils()throws Exception{
            UserDTO dto = getDTO();
            long startTime = System.currentTimeMillis();
            List<UserVO> vos = new ArrayList<UserVO>(count);
            for(int i = 0 ; i < count ; ++i){
                UserVO vo = new UserVO();
                //Apache PropertyUtils
                org.apache.commons.beanutils.PropertyUtils.copyProperties(vo,dto);
                vos.add(vo);
            }
            System.out.println("用时:"+(System.currentTimeMillis() - startTime));
        }
    
    
    }
    
    

    测试结果

    工具/次数 1000次 100000次 10000000次
    手动编写getter/setter 2ms 31ms 2739ms
    mapstruct 2ms 11ms 2794ms
    cglib BeanCopier 2ms 14ms 2650ms
    spring BeanUtils 170ms 282ms 6965ms
    apache BeanUtils 165ms 835ms 47463ms
    apache PropertyUtils 135ms 485ms 30523ms

    测试结果区3次/1次的平均结果。不同机器不同配置和不同版本会有少许区别。

    结论

    1. 从测试结果来看,mapstruct明显是效率最高的(手动写getter和setter不算)。但是需要额外写一个接口。所以适合那种需要大量转换的类使用。因为需要再添加一个转换接口,加大了代码编写的量,没必要对那些转换次数少的使用。
    2. mapstruct最灵活,功能也非常丰富。如果不在乎代码编写量,推荐使用。
    3. cglib 的 BeanCopier 工具性能也很高。但是有个 BeanCopier.create(.....) 操作,如果大量使用的时候推荐先把这个对象实现成单例,不要重复的调用这个方法去创建,使用起来性能才能提升上去。
    4. apache这两个工具包不推荐使用(阿里大佬们也不推荐使用),而且我们通常使用Spring框架,还需要额外的导入相关包。没必要使用。少量的属性拷贝使用Spring的BeanUtils就好。

    作者:BobC

    文章原创。如你发现错误,欢迎指正,在这里先谢过了。博主的所有的文章、笔记都会在优化并整理后发布在个人公众号上,如果我的笔记对你有一定的用处的话,欢迎关注一下,我会提供更多优质的笔记的。
  • 相关阅读:
    JdbcTemplate增删改查案例
    顾问和注解
    使用多种方式实现AOP
    面试题
    Spring Bean的生命周期和作用域
    IOC和AOP交互拓展(二)
    AOP
    错题
    Spring核心概念
    hadoop-MapReduce框架原理之OutputFormat数据输出
  • 原文地址:https://www.cnblogs.com/Eastry/p/13661387.html
Copyright © 2011-2022 走看看