Вы все еще используете BeanUtils для копирования свойств объекта?

Spring
Вы все еще используете BeanUtils для копирования свойств объекта?

При ведении бизнеса, чтобы изолировать изменения, мы будемDAOосведомилсяDOи предоставляется на передний конецDTOизолированные. возможно90%, все они похожи по структуре, но мы не любим писать много многословныхb.setF1(a.getF1())Такой код, поэтому нам нужно упростить метод копирования объекта.

Большую часть времени используяApacheилиSpring``BeanUtils, сегодня рассмотрим более эффективный способ копирования свойств:BeanCopier.

1. Предпосылки

1.1 Концепция копирования объекта

JavaВ , типы данных делятся на типы значений (базовые типы данных) и ссылочные типы.int,double,byte,boolean,charи другие простые типы данных, а ссылочные типы включают сложные типы, такие как классы, интерфейсы и массивы.

Копия объекта делится наМелкая копия (мелкий клон)иглубокая копия (глубокий клон).

  • Разница между поверхностным и глубоким копированием
Классификация мелкая копия глубокая копия
разница Создайте новый объект, затем скопируйте нестатические поля текущего объекта в новый объект, выполнив копирование поля, если поле имеет тип значения, или копирование поля, если поле является ссылочным типом.ссылка, но не копирование ссылочного объекта. Таким образом, исходный объект и его копия относятся к одному и тому же объекту. Создайте новый объект, затем скопируйте нестатические поля текущего объекта в новый объект, независимо от того, имеют ли поля значение или ссылочный тип,сделать отдельную копию. Когда вы изменяете содержимое одного из объектов, это не влияет на содержимое другого объекта.

Справочная статья

1.2 Подготовка перед примером

  • класс атрибутов исходного объектаUserDO.class(В следующем примере исходные объекты используют это)
@Data
public class UserDO {

    private int id;
    private String userName;
    /**
     * 以下两个字段用户模拟自定义转换
     */
    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 {

    /**
     * 模拟查询出一条数据
     * @return
     */
    public static UserDO createData() {
        return new UserDO(1, "Van", LocalDateTime.now(),new BigDecimal(100L));
    }

    /**
     * 模拟查询出多条数据
     * @param num 数量
     * @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 для копирования объектов

ApacheиSpringобаBeanUtilsИнструменты,ApacheизBeanUtilsСтабильность и эффективность не очень хороши;SpringизBeanUtilsОн относительно стабилен, и затраты времени не будут значительно увеличиваться из-за большого количества, поэтому он обычно используется.SpringизBeanUtils.

2.1 Интерпретация исходного кода

SpringсерединаBeanUtils, который реализован очень простым способом, заключающимся в простом выполнении простой операции над одноименными свойствами в двух объектах.get/set, который только проверяет доступность свойства.

Исходный код BeanUtils

Как видите, назначение переменных-членов основано на списке членов целевого объекта и будет пропущено.ignoreи не существует в исходном объекте, поэтому этот метод безопасен и не вызовет ошибок из-за структурных различий между двумя объектами, но должен гарантировать, что две переменные-члены с одинаковыми именами имеют один и тот же тип.

2.2 Пример

@Slf4j
public class BeanUtilsDemo {

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        UserDO userDO = DataUtil.createData();
        log.info("拷贝前,userDO:{}", userDO);
        UserDTO userDTO = new UserDTO();
        BeanUtils.copyProperties(userDO,userDTO);
        log.info("拷贝后,userDO:{}", userDO);
    }
}
  • результат
18:12:11.734 [main] INFO cn.van.parameter.bean.copy.demo.BeanUtilsDemo - 拷贝前,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 - 拷贝后,userDO:UserDO(id=1, userName=Van, gmtBroth=2019-11-02T18:12:11.730, balance=100)

3. BeanCopier копии объекта

BeanCopierиспользуется в двухbeanАтрибуты копируются между ними.BeanCopierПоддерживаются два способа:

  1. Один не использоватьConverterкстати, только на двоихbeanКопировать между переменными с одинаковым именем и типом атрибута;
  2. Другой представляетConverter, вы можете выполнять специальные операции над определенными значениями свойств.

3.1 Основное использование

  • полагаться
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib-nodep</artifactId>
    <version>3.3.0</version>
</dependency>

**Примечание.** Эта зависимость не требуется, посколькуSpringбыл интегрирован вcglib, блогер используетorg.springframework.cglib.beans.BeanCopier.

3.1.1 Имена и типы атрибутов совпадают

  • класс атрибута целевого объекта
@Data
public class UserDTO {
    private int id;
    private String userName;
}
  • Методы испытаний
/**
 * 属性名称、类型都相同(部分属性不拷贝)
 */
private static void normalCopy() {
	// 模拟查询出数据
    UserDO userDO = DataUtil.createData();
    log.info("拷贝前:userDO:{}", userDO);
    // 第一个参数:源对象, 第二个参数:目标对象,第三个参数:是否使用自定义转换器(下面会介绍),下同
    BeanCopier b = BeanCopier.create(UserDO.class, UserDTO.class, false);
    UserDTO userDTO = new UserDTO();
    b.copy(userDO, userDTO, null);
    log.info("拷贝后:userDTO:{}", userDTO);
}
  • Результат: копирование выполнено успешно
18:24:24.080 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopierDemo - 拷贝前: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 - 拷贝后:userDTO:UserDTO(id=1, userName=Van)

3.1.2 Атрибуты с одинаковыми именами, но разными типами

  • класс атрибута целевого объекта
@Data
public class UserEntity {
    private Integer id;
    private String userName;
}
  • Методы испытаний
/**
 * 属性名称相同、类型不同
 */
private static void sameNameDifferentType() {
    // 模拟查询出数据
    UserDO userDO = DataUtil.createData();
    log.info("拷贝前:userDO:{}", userDO);

    BeanCopier b = BeanCopier.create(UserDO.class, UserEntity.class, false);
    UserEntity userEntity = new UserEntity();
    b.copy(userDO, userEntity, null);
    log.info("拷贝后:userEntity:{}", userEntity);
}
  • результат
19:43:31.645 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopierDemo - 拷贝前: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 - 拷贝后:userEntity:UserEntity(id=null, userName=Van)
  • анализировать

Через журнал можно узнать:UserDOизintТипidнельзя копировать вUserEntityизIntegerизid.

Раздел 3.1.3

BeanCopierКопируются только свойства с тем же именем и типом.

Даже если исходный тип является примитивным типом (int, shortиcharи т. д.), целевой тип — это его тип оболочки (Integer, ShortиCharacterд.), или наоборот: ни один из них не будет скопирован.

3.2 Пользовательский конвертер

пройти через3.1.2Видно, что когда типы атрибутов исходного и целевого классов различны, атрибут не может быть скопирован.В это время мы можем достичьConverterинтерфейс для настройки конвертеров

3.2.1 Подготовка

  • класс атрибута целевого объекта
@Data
public class UserDomain {
    private Integer id;
    private String userName;
    
    /**
     * 以下两个字段用户模拟自定义转换
     */
    private String gmtBroth;
    private String balance;
}

3.2.2 Не используетсяConverter

  • Методы испытаний
/**
 * 类型不同,不使用Converter
 */
public static void noConverterTest() {
    // 模拟查询出数据
    UserDO userDO = DataUtil.createData();
    log.info("拷贝前:userDO:{}", userDO);
    BeanCopier copier = BeanCopier.create(UserDO.class, UserDomain.class, false);
    UserDomain userDomain = new UserDomain();
    copier.copy(userDO, userDomain, null);
    log.info("拷贝后:userDomain:{}", userDomain);
}
  • результат
19:49:19.294 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopierDemo - 拷贝前: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 - 拷贝后:userDomain:UserDomain(id=null, userName=Van, gmtBroth=null, balance=null)
  • анализировать

Сравнивая до и после печати журнала, поля с различными типами атрибутовid,gmtBroth,balanceНе скопировано.

3.2.3 ИспользованиеConverter

  • выполнитьConverterИнтерфейс для настройки преобразования свойств
public  class UserConverter implements Converter {

    /**
     * 时间转换的格式
     */
    DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    /**
     * 自定义属性转换
     * @param value 源对象属性类
     * @param target 目标对象里属性对应set方法名,eg.setId
     * @param context 目标对象属性类
     * @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();
        }
        // 更多类型转换请自定义
        return value;
    }
}
  • Методы испытаний
/**
 * 类型不同,使用Converter
 */
public static void converterTest() {
    // 模拟查询出数据
    UserDO userDO = DataUtil.createData();
    log.info("拷贝前: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("拷贝后:userDomain:{}", userDomain);
}
  • Результат: все копии
19:51:11.989 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopierDemo - 拷贝前: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 - 拷贝后:userDomain:UserDomain(id=1, userName=Van, gmtBroth=2019-11-02 19:51:11, balance=100)

Раздел 3.2.4

  1. однажды использованныйConverter,BeanCopierиспользовать толькоConverterопределенные правила для копирования атрибутов, поэтому вconvert()В методе учитываются все свойства.
  2. Однако, используяConverterЗамедляет копирование объектов.

3.3 BeanCopierСуммировать

  1. Когда имена и типы атрибутов исходного класса и целевого класса совпадают, копирование не представляет проблемы.
  2. Когда свойства исходного объекта и целевого объекта имеют одинаковое имя, но разные типы, свойства с одинаковым именем, но разными типами не будут скопированы. Обратите внимание, что примитивные типы (int,short,char), а их типы-оболочки рассматриваются здесь как разные типы, поэтому они не копируются.
  3. исходный или целевой классsetterСравниватьgetterМеньше, копирование не проблема, в это времяsetterЛишнее, но не сообщит об ошибке.
  4. Исходный и целевой классы имеют одинаковые свойства (обаgetterоба существуют), но целевой классsetterне существует, он выкинетNullPointerException.

4. Сравнение скорости между BeanUtils и BeanCopier

Не много ерунды, я сразу продемонстрирую здесь два инструмента10000Длительное сравнение копирования данных

4.1 BeanUtils

  • тестовый код
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);
}
  • Результат (затраты времени:232ms)
20:14:24.380 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopyComparedDemo - BeanUtils cotTime: 232ms

4.2 BeanCopier

  • тестовый код
private static void beanCopier() {
    // 工具类生成10w条数据
    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);
}
  • Результат (затраты времени:116ms)
20:15:24.380 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopyComparedDemo - BeanCopier costTime: 116ms

4.3 КэшBeanCopierЭкземпляры повышают производительность

BeanCopierСкорость копирования высокая, а узкое место производительности возникает при созданииBeanCopierв процессе пример. Итак, поместите созданныйBeanCopierЭкземпляр помещается в кеш и может быть получен непосредственно в следующий раз для повышения производительности.

  • тестовый код
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 加缓存后 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();
}
  • Результат (затраты времени:6ms)
20:32:31.405 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopyComparedDemo - BeanCopier 加缓存后 costTime: 6ms

Пять, резюме и исходный код

Сцены Занимает много времени (10000 звонков) принцип
BeanUtils 232ms отражение
BeanCopier 116ms Изменить байт-код
BeanCopier (плюс кеш) 6ms Изменить байт-код

Пример кода на гитхабе

рекомендовать:Анализ исходного кода BeanCopier

Обмен технологиями

  1. Пыль Блог
  2. Пыль Блог - Блог Парк

Подпишитесь на официальный аккаунт, чтобы узнать больше:

风尘博客