无聊的笔记:之二(再来看看到底怎么高性能的使用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就好。
上一篇:Spring中常见的设计模式——原型模式


下一篇:C#初探深拷贝和浅拷贝