Are you still copying object properties with BeanUtils?

When doing business, in order to isolate changes, we will separate DO queried by DAO from DTO provided by front end. About 90% of the time, their structures are similar; however, we don't like to write a lot of lengthy code like b.setF1(a.getF1()), so we need to simplify the object copying method.

Most of the time, Apache or Spring`BeanUtils are used. Today, let's look at a more efficient way to copy attributes: BeanCopier '.

Background

1.1 object copy concept

In Java, data types are divided into value types (basic data types) and reference types. Value types include simple data types such as int, double, byte, boolean, char, and reference types include complex types such as class, interface, array, etc.

Object copies are divided into shallow copies (shallow clones) and deep copies (deep clones).

  • Differences between shallow copy and deep copy
classification Shallow copy Deep copy
Difference Create a new object, and then copy the non static field of the current object to the new object. If the field is of value type, copy the field; if the field is of reference type, copy the referenced object but not the referenced object. As a result, the original object and its copies refer to the same object. Create a new object, and then copy the non-static field of the current object to the new object. Whether the field is of value type or reference type, a separate copy will be copied. When you modify anything in one object, it doesn't affect the content of the other.

Reference articles

1.2 sample preparation

  • Source object property class userdo.class (for the following example, this is used by source objects)
@Data
public class UserDO {

    private int id;
    private String userName;
    /**
     * The following two fields are user simulated custom transformations
     */
    private LocalDateTime gmtBroth;
    private BigDecimal balance;

    public UserDO(Integer id, String userName, LocalDateTime gmtBroth, BigDecimal balance) {
        this.id = id;
        this.userName = userName;
        this.gmtBroth = gmtBroth;
        this.balance = balance;
    }
}
  • DataUtil.class
public class DataUtil {

    /**
     * Simulate and query a piece of data
     * @return
     */
    public static UserDO createData() {
        return new UserDO(1, "Van", LocalDateTime.now(),new BigDecimal(100L));
    }

    /**
     * Simulate to query multiple data
     * @param num Number
     * @return
     */
    public static List<UserDO> createDataList(int num) {
        List<UserDO> userDOS = new ArrayList<>();
        for (int i = 0; i < num; i++) {
            UserDO userDO = new UserDO(i+1, "Van", LocalDateTime.now(),new BigDecimal(100L));
            userDOS.add(userDO);
        }
        return userDOS;
    }
}

2. BeanUtils of object copy

Both Apache and spring have BeanUtils tool classes, and Apache's BeanUtils are not stable and efficient. Spring's BeanUtils are relatively stable, which will not increase significantly because of large amount and time consumption, so spring's BeanUtils are generally used.

2.1 source code interpretation

The implementation of BeanUtils in Spring is very simple, which is to simply get/set the properties with the same name in two objects, and only check the accessibility of the properties.

BeanUtils source code

As you can see, the member variable assignment is based on the member list of the target object, and ignore s and does not exist in the source object. Therefore, this method is safe and will not cause errors due to the structural differences between the two objects, but it must be ensured that the two member variables with the same name have the same type.

2.2 example

@Slf4j
public class BeanUtilsDemo {

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        UserDO userDO = DataUtil.createData();
        log.info("Before copying,userDO:{}", userDO);
        UserDTO userDTO = new UserDTO();
        BeanUtils.copyProperties(userDO,userDTO);
        log.info("After copy,userDO:{}", userDO);
    }
}
  • Result
18:12:11.734 [main] INFO cn.van.parameter.bean.copy.demo.BeanUtilsDemo - Before copying,userDO:UserDO(id=1, userName=Van, gmtBroth=2019-11-02T18:12:11.730, balance=100)
18:12:11.917 [main] INFO cn.van.parameter.bean.copy.demo.BeanUtilsDemo - After copy,userDO:UserDO(id=1, userName=Van, gmtBroth=2019-11-02T18:12:11.730, balance=100)

3. BeanCopier of object copy

bean copier is used to copy properties between two beans. BeanCopier supports two ways:

  1. One is to copy only variables with identical property names and types between two bean s without using Converter;
  2. The other introduces Converter, which can perform special operations on some specific property values.

3.1 basic use

  • rely on
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib-nodep</artifactId>
    <version>3.3.0</version>
</dependency>

Note: this dependency is not necessary because cglib has been integrated in Spring. The blogger uses org.springframework.cglib.beans.BeanCopier.

3.1.1 attribute name and type are the same

  • Target object property class
@Data
public class UserDTO {
    private int id;
    private String userName;
}
  • test method
/**
 * Property name and type are the same (some properties are not copied)
 */
private static void normalCopy() {
    // Simulate query data
    UserDO userDO = DataUtil.createData();
    log.info("Before copying: userDO:{}", userDO);
    // First parameter: source object, second parameter: target object, third parameter: whether to use a custom converter (described below), the same below
    BeanCopier b = BeanCopier.create(UserDO.class, UserDTO.class, false);
    UserDTO userDTO = new UserDTO();
    b.copy(userDO, userDTO, null);
    log.info("After copying: userDTO:{}", userDTO);
}
  • Result: copy succeeded
18:24:24.080 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopierDemo - Before copying: userDO:UserDO(id=1, userName=Van, gmtBroth=2019-11-02T18:24:24.077, balance=100)
18:24:24.200 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopierDemo - After copying: userDTO:UserDTO(id=1, userName=Van)

3.1.2 same attribute name and different type

  • Target object property class
@Data
public class UserEntity {
    private Integer id;
    private String userName;
}
  • test method
/**
 * Same property name and different type
 */
private static void sameNameDifferentType() {
    // Simulate query data
    UserDO userDO = DataUtil.createData();
    log.info("Before copying: userDO:{}", userDO);

    BeanCopier b = BeanCopier.create(UserDO.class, UserEntity.class, false);
    UserEntity userEntity = new UserEntity();
    b.copy(userDO, userEntity, null);
    log.info("After copying: userEntity:{}", userEntity);
}
  • Result
19:43:31.645 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopierDemo - Before copying: userDO:UserDO(id=1, userName=Van, gmtBroth=2019-11-02T19:43:31.642, balance=100)
19:43:31.748 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopierDemo - After copying: userEntity:UserEntity(id=null, userName=Van)
  • Analysis

The log shows that the id of int type of UserDO cannot be copied to the id of Integer of UserEntity.

3.1.3 section

BeanCopier copies only attributes of the same name and type.

Even if the source type is the original type (int, short, char, etc.), the target type is its wrapper type (Integer, Short, Character, etc.), or vice versa: will not be copied.

3.2 custom converter

According to 3.1.2, when the attribute types of the source and target classes are different, the attribute cannot be copied. At this time, we can define the Converter by implementing the Converter interface

3.2.1 preparation

  • Target object property class
@Data
public class UserDomain {
    private Integer id;
    private String userName;
    
    /**
     * The following two fields are user simulated custom transformations
     */
    private String gmtBroth;
    private String balance;
}

3.2.2 do not use Converter

  • test method
/**
 * Different types, do not use Converter
 */
public static void noConverterTest() {
    // Simulate query data
    UserDO userDO = DataUtil.createData();
    log.info("Before copying: userDO:{}", userDO);
    BeanCopier copier = BeanCopier.create(UserDO.class, UserDomain.class, false);
    UserDomain userDomain = new UserDomain();
    copier.copy(userDO, userDomain, null);
    log.info("After copying: userDomain:{}", userDomain);
}
  • Result
19:49:19.294 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopierDemo - Before copying: userDO:UserDO(id=1, userName=Van, gmtBroth=2019-11-02T19:49:19.290, balance=100)
19:49:19.394 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopierDemo - After copying: userDomain:UserDomain(id=null, userName=Van, gmtBroth=null, balance=null)
  • Analysis

Through the comparison before and after printing the log, the field ID, gmtbroth and balance with different attribute types are not copied.

3.2.3 using Converter

  • Implement Converter interface to define property transformation
public  class UserConverter implements Converter {

    /**
     * Format of time conversion
     */
    DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    /**
     * Custom attribute conversion
     * @param value Source object property class
     * @param target The set method name corresponding to the attribute in the target object, eg.setId
     * @param context Target object property class
     * @return
     */
    @Override
    public Object convert(Object value, Class target, Object context) {
        if (value instanceof Integer) {
            return value;
        } else if (value instanceof LocalDateTime) {
            LocalDateTime date = (LocalDateTime) value;
            return dtf.format(date);
        } else if (value instanceof BigDecimal) {
            BigDecimal bd = (BigDecimal) value;
            return bd.toPlainString();
        }
        // More type conversion please customize
        return value;
    }
}
  • test method
/**
 * Different types, use Converter
 */
public static void converterTest() {
    // Simulate query data
    UserDO userDO = DataUtil.createData();
    log.info("Before copying: userDO:{}", userDO);
    BeanCopier copier = BeanCopier.create(UserDO.class, UserDomain.class, true);
    UserConverter converter = new UserConverter();
    UserDomain userDomain = new UserDomain();
    copier.copy(userDO, userDomain, converter);
    log.info("After copying: userDomain:{}", userDomain);
}
  • Result: copy all
19:51:11.989 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopierDemo - Before copying: userDO:UserDO(id=1, userName=Van, gmtBroth=2019-11-02T19:51:11.985, balance=100)
19:51:12.096 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopierDemo - After copying: userDomain:UserDomain(id=1, userName=Van, gmtBroth=2019-11-02 19:51:11, balance=100)

3.2.4 section

  1. Once Converter is used, BeanCopier only uses the rules defined by Converter to copy attributes, so all attributes should be considered in the convert() method.
  2. However, using Converter slows down object copying.

3.3 summary of beancopier

  1. When the attribute names and types of source and target classes are the same, copying is OK.
  2. When the source object and the target object have the same attribute name and different type, the attributes with the same name and different type will not be copied. Note that the original types (int, short, char) and their packaging types are treated as different types here, so they are not copied.
  3. There are fewer setters in the source class or target class than in the getter. There is no problem with copying. At this time, the setter is redundant, but no error will be reported.
  4. The source class and the target class have the same properties (both getter s exist), but the setter of the target class does not exist. NullPointerException will be thrown at this time.

IV. speed comparison between BeanUtils and BeanCopier

Needless to say, I will directly demonstrate the time-consuming comparison of 10000 copies of data between the two tools

4.1 BeanUtils

  • Test code
private static void beanUtil() {
    List<UserDO> list = DataUtil.createDataList(10000);
    long start = System.currentTimeMillis();
    List<UserDTO> dtoList = new ArrayList<>();
    list.forEach(userDO -> {
        UserDTO userDTO = new UserDTO();
        BeanUtils.copyProperties(userDO,userDTO);
        dtoList.add(userDTO);
    });
    log.info("BeanUtils cotTime: {}ms", System.currentTimeMillis() - start);
}
  • Results (time: 232ms)
20:14:24.380 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopyComparedDemo - BeanUtils cotTime: 232ms

4.2 BeanCopier

  • Test code
private static void beanCopier() {
    // Tool class generates 10w pieces of data
    List<UserDO> doList = DataUtil.createDataList(10000);
    long start = System.currentTimeMillis();
    List<UserDTO> dtoList = new ArrayList<>();
    doList.forEach(userDO -> {
        BeanCopier b = BeanCopier.create(UserDO.class, UserDTO.class, false);
        UserDTO userDTO = new UserDTO();
        b.copy(userDO, userDTO, null);
        dtoList.add(userDTO);
    });
    log.info("BeanCopier costTime: {}ms", System.currentTimeMillis() - start);
}
  • Results (time: 116ms)
20:15:24.380 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopyComparedDemo - BeanCopier costTime: 116ms

4.3 cache BeanCopier instance to improve performance

The copy speed of BeanCopier is fast, and the performance bottleneck appears in the process of creating BeanCopier instance Therefore, put the created BeanCopier instance into the cache, and you can get it directly next time, improving performance.

  • Test code
private static void beanCopierWithCache() {

    List<UserDO> userDOList = DataUtil.createDataList(10000);
    long start = System.currentTimeMillis();
    List<UserDTO> userDTOS = new ArrayList<>();
    userDOList.forEach(userDO -> {
        UserDTO userDTO = new UserDTO();
        copy(userDO, userDTO);
        userDTOS.add(userDTO);
    });
    log.info("BeanCopier After caching costTime: {}ms", System.currentTimeMillis() - start);

}

public static void copy(Object srcObj, Object destObj) {
    String key = genKey(srcObj.getClass(), destObj.getClass());
    BeanCopier copier = null;
    if (!BEAN_COPIERS.containsKey(key)) {
        copier = BeanCopier.create(srcObj.getClass(), destObj.getClass(), false);
        BEAN_COPIERS.put(key, copier);
    } else {
        copier = BEAN_COPIERS.get(key);
    }
    copier.copy(srcObj, destObj, null);

}
private static String genKey(Class<?> srcClazz, Class<?> destClazz) {
    return srcClazz.getName() + destClazz.getName();
}
  • Results (time: 6ms)
20:32:31.405 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopyComparedDemo - BeanCopier After caching costTime: 6ms

V. summary and source code

scene Time consuming (10000 calls) principle
BeanUtils 232ms reflex
BeanCopier 116ms Modify bytecode
Beancopier (cached) 6ms Modify bytecode

Github sample code

Recommend: Source code analysis of BeanCopier

Technology exchange

  1. Eolian blog
  2. FengChen blog blog Garden
  3. Dust blog - gold digger

Pay attention to the public number and learn more:

Tags: Java Attribute Spring Apache

Posted on Wed, 06 Nov 2019 19:08:26 -0500 by tomsasse