平时常用的工具:
- Apache BeanUtils
- Spring BeanUtils
- Cglib BeanCopier
- MapStruct
性能对比:
MapStruct ≈ Cglib BeanCopier > Spring BeanUtils > Apache BeanUtils
拷贝场景:
- 同名同类型字段拷贝
- 不同类型的属性拷贝,比如基本类型与其包装类型等
- 不同字段名属性拷贝,当然字段名应该尽量保持一致,但是实际业务中,由于不同开发人员,或者笔误拼错单词,这些原因都可能导致会字段名不一致的情况
- 浅拷贝/深拷贝,浅拷贝会引用同一对象,如果稍微不慎,同时改动对象,就会踩到意想不到的坑
使用方法:
一、Apache BeanUtiles
定义bean:
测试:
String不能转换为Date类型,所以需要自定义转换器并注册。
@Test
public void test() throws InvocationTargetException, IllegalAccessException {
StudentDTO studentDTO = new StudentDTO();
studentDTO.setName("小明");
studentDTO.setAge(18);
studentDTO.setNo("6666");
List<String> 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> Date convert(Class<Date> 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);
结果:
结论:
- 字段名不一致,属性无法拷贝
- 类型不一致,属性无法拷贝(基本类型 转 对应的包装类,这种可以转化,但是反过来如上个例子所说,如果包装类未赋值,转换的时候会抛异常)
- 嵌套对象字段,将会与源对象使用同一对象,即使用浅拷贝
三、Cglib BeanCopier
测试:bean定义同上
// 赋值代码同上
BeanCopier copier = BeanCopier.create(StudentDTO.class, StudentDO.class, false);
copier.copy(studentDTO, studentDO, null);
结果:
结论:
- 字段名不一致,属性无法拷贝
- 类型不一致,属性无法拷贝。(注意不同点,即使是基础类型转对应的包装类也不能拷贝)
- 集合类型的嵌套对象字段,将会与源对象使用同一对象,即使用浅拷贝
- 非集合类的嵌套对象字段(比如自定义的一个类对象),拷贝不了,目标值为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
依赖:
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.4.1.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.4.1.Final</version>
</dependency>
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;
}
}
结果:
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<String> list = dto.getSubjects();
if ( list != null ) {
studentDO.setSubjects( new ArrayList<String>( 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,编译期生成拷贝实现类,性能高,且原理一目了然,灵活自定义映射关系。