bean拷贝

平时常用的工具:

  • Apache BeanUtils
  • Spring BeanUtils
  • Cglib BeanCopier
  • MapStruct

性能对比:

MapStruct ≈ Cglib BeanCopier > Spring BeanUtils > Apache BeanUtils

拷贝场景:

  • 同名同类型字段拷贝
  • 不同类型的属性拷贝,比如基本类型与其包装类型等
  • 不同字段名属性拷贝,当然字段名应该尽量保持一致,但是实际业务中,由于不同开发人员,或者笔误拼错单词,这些原因都可能导致会字段名不一致的情况
  • 浅拷贝/深拷贝,浅拷贝会引用同一对象,如果稍微不慎,同时改动对象,就会踩到意想不到的坑

使用方法:

一、Apache BeanUtiles

定义bean:

bean拷贝_第1张图片

 测试:

String不能转换为Date类型,所以需要自定义转换器并注册。

@Test
    public void test() throws InvocationTargetException, IllegalAccessException {
        StudentDTO studentDTO = new StudentDTO();
        studentDTO.setName("小明");
        studentDTO.setAge(18);
        studentDTO.setNo("6666");

        List subjects = new ArrayList<>();
        subjects.add("math");
        subjects.add("english");
        studentDTO.setSubjects(subjects);
        studentDTO.setCourse(new Course("CS-1"));
        studentDTO.setCreateDate("2020-08-08");

        StudentDO studentDO = new StudentDO();

        ConvertUtils.register(new Converter() {
            @SneakyThrows
            @Override
            public  Date convert(Class type, Object value) {
                if (value == null) {
                    return null;
                }
                if (value instanceof String) {
                    return (Date) DateUtils.parseDate((String)value, "yyyy-MM-dd");
                }
                return null;


            }
        }, Date.class);

        BeanUtils.copyProperties(studentDO, studentDTO);
        System.out.println(studentDO);
    }

结果:

结论:

  • 字段名不一致,属性无法拷贝
  • 类型不一致,将会进行默认类型转换(如Integer和String互转),转换不了的需要自定义转换器——(包装类转基本类型,如果包装类没有初始化,则会抛异常,因为不能给基本类型赋值null,因此最好不要在类变量中使用基本类型
  • 嵌套对象字段,将会与源对象使用同一对象,即使用浅拷贝

二、Spring BeanUtils

测试:要注意spring beanUtils的参数(源、目标)和apache beanUtils是相反的。

// 赋值代码同上
BeanUtils.copyProperties(studentDTO, studentDO);

结果:bean拷贝_第2张图片

结论:

  • 字段名不一致,属性无法拷贝
  • 类型不一致,属性无法拷贝(基本类型 转 对应的包装类,这种可以转化,但是反过来如上个例子所说,如果包装类未赋值,转换的时候会抛异常
  • 嵌套对象字段,将会与源对象使用同一对象,即使用浅拷贝

 三、Cglib BeanCopier

测试:bean定义同上

// 赋值代码同上
BeanCopier copier = BeanCopier.create(StudentDTO.class, StudentDO.class, false);
copier.copy(studentDTO, studentDO, null);

结果:

bean拷贝_第3张图片

 结论:

  • 字段名不一致,属性无法拷贝
  • 类型不一致,属性无法拷贝。(注意不同点,即使是基础类型转对应的包装类也不能拷贝
  • 集合类型的嵌套对象字段,将会与源对象使用同一对象,即使用浅拷贝
  • 非集合类的嵌套对象字段(比如自定义的一个类对象),拷贝不了,目标值为null

当类型不一致时,可以使用自定义转换器:

BeanCopier copier = BeanCopier.create(StudentDTO.class, StudentDO.class, true);
copier.copy(studentDTO, studentDO, new Converter() {
            @SneakyThrows
            @Override
            public Object convert(Object o, Class aClass, Object o1) {
                if (o instanceof String) {
                    return DateUtils.parseDate((String)o, "yyyy-MM-dd");
                } else if (o instanceof Integer) {
                    return String.valueOf(o);
                }
                return o;
            }
        });

但是有个问题是如果同一种类型,但是目标字段的类型不同,则处理不了。比如示例中的createDate、no、name都是String类型,createDate需要转换为Date,后两者不需要类型转换。

Cglib BeanCopier 的原理与上面两个 Beanutils 原理不太一样,其主要使用 字节码技术动态生成一个代理类,代理类实现get 和 set方法。生成代理类过程存在一定开销,但是一旦生成,我们可以缓存起来重复使用,所有 Cglib 性能相比以上两种 Beanutils 性能比较好。

四、MapStruct

依赖:

        
            org.mapstruct
            mapstruct
            1.4.1.Final
        
        
            org.mapstruct
            mapstruct-processor
            1.4.1.Final
        

bean定义同上

测试:

@Mapper(componentModel = "spring") 
public interface StudentMapper {
    StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class); 
    StudentDO convert(StudentDTO dto);
}


// componentModel = "spring": 生成的实现类上面会自动添加一个@Component注解,可以通过Spring的@Autowired方式进行注入。
// Mappers.getMapper(Class): 获取自动生成的实例对象,便于在没有启动spring容器的时候使用。

 createDate字段从String转换Date异常,需要使用注解@Mapping指定转换方式

如果表达式中的语句会抛异常,需要做下封装(比如DateUtils.parseDate封装到DateUtilsParse),明确捕获异常才行,否则编译失败(java: 未报告的异常错误java.text.ParseException; 必须对其进行捕获或声明以便抛出)

错误示范:
​​​​​​​@Mapping(target = "createDate", expression = "java(org.apache.commons.lang3.time.DateUtils.parseDate(dto.getCreateDate(), \"yyyy-MM-dd\"))")

正确示范:

@Mapper(componentModel = "spring")
public interface StudentMapper {
    StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class);

    @Mapping(target = "createDate", expression = "java(DateUtilsParse(dto.getCreateDate()))")
    StudentDO convert(StudentDTO dto);

    default Date DateUtilsParse(String dateStr) {
        try {
            return DateUtils.parseDate(dateStr, "yyyy-MM-dd");
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return null;
    }
}

结果:

bean拷贝_第4张图片

target目录下查看编译结果,自动生成实现类:

@Component
public class StudentMapperImpl implements StudentMapper {

    @Override
    public StudentDO convert(StudentDTO dto) {
        if ( dto == null ) {
            return null;
        }

        StudentDO studentDO = new StudentDO();

        studentDO.setNumber( dto.getNo() );
        studentDO.setName( dto.getName() );
        if ( dto.getAge() != null ) {
            studentDO.setAge( String.valueOf( dto.getAge() ) ); // 可见是浅拷贝
        }
        List list = dto.getSubjects();
        if ( list != null ) {
            studentDO.setSubjects( new ArrayList( list ) ); // 可见是深拷贝
        }
        studentDO.setCourse( dto.getCourse() );

        studentDO.setCreateDate( DateUtilsParse(dto.getCreateDate()) );

        return studentDO;
    }
}

bean定义中有枚举字段的情况:

public enum GenderEnum {
    BOY("boy", "男孩"),
    GIRL("girl", "女孩");

    GenderEnum(String code, String desc) {
    }
}


源字段类型:String
目标字段类型:GenderEnum
编译生成的实现:
if ( dto.getGender() != null ) {
    studentDO.setGender( Enum.valueOf( GenderEnum.class, dto.getGender() ) );
}


源字段类型:GenderEnum
目标字段类型:String
编译生成的实现:
if ( dto.getGender() != null ) {
     studentDO.setGender( dto.getGender().name() );
}

结论:

  • 字段名不一致,属性无法拷贝,可以用注解@Mapping指定映射关系实现拷贝(@Mapping(target = "number", source = "dto.no"))
  • 类型不一致,将会进行默认类型转换(如Integer和String互转),转换不了的需要用注解@Mapping指定转换方式
  • 自动做枚举和String的转换
  • 集合类是深拷贝
  • 嵌套对象字段,将会与源对象使用同一对象,即使用浅拷贝

​​​​​​​

总结

Apache BeanUtiles底层源码为了追求完美,加了过多的包装,使用了很多反射,做了很多校验,导致性能较差,所以阿里巴巴开发手册上强制规定避免使用 Apache BeanUtil

  • 不使用Apache BeanUtils
  • 对性能有要求,使用MapStruct或者Cglib BeanCopier+缓存(类型不一致时BeanCopier的转换器存在局限性,没有MapStruct灵活)
  • 没有性能要求,可以使用 Spring Beanutils ,因为Spring 的包大部分应用都在使用,无需导入其他包
  • 个人最推荐使用MapStruct,编译期生成拷贝实现类,性能高,且原理一目了然,灵活自定义映射关系。

你可能感兴趣的:(spring,java)