简介: 随着零碎模块分层一直细化,在Java日常开发中不可避免地波及到各种对象的转换,如:DO、DTO、VO等等,编写映射转换代码是一个繁琐反复且还易错的工作,一个好的工具辅助,加重了工作量、晋升开发工作效率的同时还能缩小bug的产生

作者 | 久贤
起源 | 阿里技术公众号

一 前言

随着零碎模块分层一直细化,在Java日常开发中不可避免地波及到各种对象的转换,如:DO、DTO、VO等等,编写映射转换代码是一个繁琐反复且还易错的工作,一个好的工具辅助,加重了工作量、晋升开发工作效率的同时还能缩小bug的产生。

二 罕用计划及剖析

1 fastjson

CarDTO entity = JSON.parseObject(JSON.toJSONString(carDO), CarDTO.class);

这种计划因为通过生成两头json格局字符串,而后再转化成指标对象,性能十分差,同时因为两头会生成json格局字符串,如果转化过多,gc会十分频繁,同时针对简单场景反对能力有余,根本很少用。

2 BeanUtil类

BeanUtil.copyProperties()联合手写get、set,对于简略的转换间接应用BeanUtil,简单的转换本人手工写get、set。该计划的痛点就在于代码编写效率低、冗余繁冗还略显俊俏,并且BeanUtil因为应用了反射invoke去赋值性能不高。

只能适宜bean数量较少、内容不多、转换不频繁的场景。

apache.BeanUtils

org.apache.commons.beanutils.BeanUtils.copyProperties(do, entity);

这种计划因为用到反射的起因,同时自身设计问题,性能比拟差。团体开发规约明确规定禁止应用。

spring.BeanUtils

org.springframework.beans.BeanUtils.copyProperties(do, entity);

这种计划针对apache的BeanUtils做了很多优化,整体性能晋升不少,不过还是应用反射实现比不上原生代码解决,其次针对简单场景反对能力有余。

3 beanCopier

BeanCopier copier = BeanCopier.create(CarDO.class, CarDTO.class, false); copier.copy(do, dto, null);

这种计划动静生成一个要代理类的子类,其实就是通过字节码形式转换成性能最好的get和set形式,重要的开销在创立BeanCopier,整体性能靠近原生代码解决,比BeanUtils要好很多,尤其在数据量很大时,然而针对简单场景反对能力有余。

4 各种Mapping框架

分类

Object Mapping 技术从大的角度来说分为两类,一类是运行期转换,另一类则是编译期转换:

  • 运行期反射调用 set/get 或者是间接对成员变量赋值。这种形式通过invoke执行赋值,实现时个别会采纳beanutil, Javassist等开源库。运行期对象转换的代表次要是Dozer和ModelMaper。
  • 编译期动静生成 set/get 代码的class文件,在运行时间接调用该class的 set/get 办法。该形式实际上仍会存在 set/get 代码,只是不须要开发人员本人写了。这类的代表是:MapStruct,Selma,Orika。

剖析

  • 无论哪种Mapping框架,根本都是采纳xml配置文件 or 注解的形式供用户配置,而后生成映射关系。
  • 编译期生成class文件形式须要DTO依然有set/get办法,只是调用被屏蔽;而运行期反射形式在某些间接填充 field的计划中,set/get代码也能够省略。
  • 编译期生成class形式会有源代码在本地,不便排查问题。
  • 编译期生成class形式因为在编译期才呈现java和class文件,所以热部署会受到肯定影响。
  • 反射型因为很多内容是黑盒,在排查问题时,不如编译期生成class形式不便。参考GitHub上工程java-object-mapper-benchmark能够看出次要框架性能比拟。
  • 反射型调用因为是在运行期依据映射关系反射执行,其执行速度会显著降落N个量级。
  • 通过编译期生成class代码的形式,实质跟间接写代码区别不大,但因为代码都是靠模板生成,所以代码品质没有手工写那么高,这也会造成肯定的性能损失。

综合性能、成熟度、易用性、扩展性,mapstruct是比拟优良的一个框架。

三 Mapstruct使用指南

1 Maven引入

2 简略入门案例

DO和DTO

这里用到了lombok简化代码,lombok的原理也是在编译时去生成get、set等被简化的代码。

@Data public class Car {         private String make;         private int numberOfSeats;         private CarType type; }@Data public class CarDTO {         private String make;         private int seatCount;         private String type; }

定义Mapper

@Mapper中形容映射,在编辑的时候mapstruct将会依据此形容生成实现类:

  • 当属性与其指标实体正本同名时,它将被隐式映射。
  • 当指标实体中的属性具备不同名称时,能够通过@Mapping正文指定其名称。
@Mapper public interface CarMapper {         @Mapping(source = "numberOfSeats", target = "seatCount")         CarDTO CarToCarDTO(Car car); }

应用Mapper

通过Mappers 工厂生成动态实例应用。

@Mapper public interface CarMapper {         CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);      @Mapping(source = "numberOfSeats", target = "seatCount")         CarDTO CarToCarDTO(Car car); }Car car = new Car(...); CarDTO carDTO = CarMapper.INSTANCE.CarToCarDTO(car);

getMapper会去load接口的Impl后缀的实现类。

通过生成spring bean注入应用,Mapper注解加上spring配置,会主动生成一个bean,间接应用bean注入即可拜访。

@Mapper(componentModel = "spring") public interface CarMapper {         @Mapping(source = "numberOfSeats", target = "seatCount")         CarDTO CarToCarDTO(Car car); }

主动生成的MapperImpl内容

如果配置了spring bean拜访会在注解上主动加上@Component。

3 进阶应用

逆向映射

如果是双向映射,例如 从DO到DTO以及从DTO到DO,正向办法和反向办法的映射规定通常是类似的,并且能够通过切换源和指标来简略地逆转。

应用注解@InheritInverseConfiguration 批示办法应继承相应反向办法的反向配置。

@Mapper public interface CarMapper {         CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);        @Mapping(source = "numberOfSeats", target = "seatCount")         CarDTO CarToCarDTO(Car car);        @InheritInverseConfiguration         Car CarDTOToCar(CarDTO carDTO); }

更新bean映射

有些状况下不须要映射转换产生新的bean,而是更新已有的bean。

@Mapper public interface CarMapper {         CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);         @Mapping(source = "numberOfSeats", target = "seatCount")         void updateDTOFromCar(Car car, @MappingTarget CarDTO carDTO);

汇合映射

汇合类型(List,Set,Map等)的映射以与映射bean类型雷同的形式实现,即通过在映射器接口中定义具备所需源类型和指标类型的映射办法。MapStruct反对Java Collection Framework中的多种可迭代类型。

生成的代码将蕴含一个循环,该循环遍历源汇合,转换每个元素并将其放入指标汇合。如果在给定的映射器或其应用的映射器中找到用于汇合元素类型的映射办法,则将调用此办法以执行元素转换,如果存在针对源元素类型和指标元素类型的隐式转换,则将调用此转换。

@Mapper public interface CarMapper {         CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);     @Mapping(source = "numberOfSeats", target = "seatCount")         CarDTO CarToCarDTO(Car car);         List<CarDTO> carsToCarDtos(List<Car> cars);         Set<String> integerSetToStringSet(Set<Integer> integers);         @MapMapping(valueDateFormat = "dd.MM.yyyy")         Map<String, String> longDateMapToStringStringMap(Map<Long, Date> source); }

编译时生成的实现类:

多个源参数映射

MapStruct 还反对具备多个源参数的映射办法。例如,将多个实体组合成一个数据传输对象。

在原案例新增一个Person对象,CarDTO中新增driverName属性,依据Person对象取得。

@Mapper public interface CarMapper {         CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);         @Mapping(source = "car.numberOfSeats", target = "seatCount")         @Mapping(source = "person.name", target = "driverName")         CarDTO CarToCarDTO(Car car, Person person); }

编译生成的代码:

默认值和常量映射

如果相应的源属性是null ,则能够指定默认值以将预约义值设置为指标属性。在任何状况下,都能够指定常量来设置这样的预约义值。默认值和常量被指定为字符串值。当指标类型是原始类型或装箱类型时,String 值将采纳字面量,在这种状况下容许位/八进制/十进制/十六进制模式,只有它们是无效的文字即可。在所有其余状况下,常量或默认值会通过内置转换或调用其余映射办法进行类型转换,以匹配指标属性所需的类型。

@Mapper public interface SourceTargetMapper {         SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );         @Mapping(target = "stringProperty", source = "stringProp", defaultValue = "undefined")         @Mapping(target = "longProperty", source = "longProp", defaultValue = "-1")         @Mapping(target = "stringConstant", constant = "Constant Value")         @Mapping(target = "integerConstant", constant = "14")         @Mapping(target = "longWrapperConstant", constant = "3001")         @Mapping(target = "dateConstant", dateFormat = "dd-MM-yyyy", constant = "09-01-2014")         @Mapping(target = "stringListConstants", constant = "jack-jill-tom")         Target sourceToTarget(Source s); }

自定义映射办法或映射器

在某些状况下,可能须要手动实现 MapStruct 无奈生成的从一种类型到另一种类型的特定映射。

能够在Mapper中定义默认实现办法,生成转换代码将调用相干办法:

@Mapper public interface CarMapper {         CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);         @Mapping(source = "numberOfSeats", target = "seatCount")         @Mapping(source = "length", target = "lengthType")         CarDTO CarToCarDTO(Car car);         default String getLengthType(int length) {                 if (length > 5) {                         return "large";                 } else {                         return "small";                 }         } }

也能够定义其余映射器,如下案例Car中Date须要转换成DTO中的String:

public class DateMapper {         public String asString(Date date) {                 return date != null ? new SimpleDateFormat( "yyyy-MM-dd" ).format( date ) : null;         }         public Date asDate(String date) {                 try {                         return date != null ? new SimpleDateFormat( "yyyy-MM-dd" ).parse( date ) : null;                 } catch ( ParseException e ) {                         throw new RuntimeException( e );                 }         } }@Mapper(uses = DateMapper.class) public interface CarMapper {         CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);         @Mapping(source = "numberOfSeats", target = "seatCount")         CarDTO CarToCarDTO(Car car); }

编译生成的代码:

若遇到多个相似的办法调用时会呈现不置可否,需应用@qualifiedBy指定:

@Mapper public interface CarMapper {         CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);         @Mapping(source = "numberOfSeats", target = "seatCount")         @Mapping(source = "length", target = "lengthType", qualifiedByName = "newStandard")         CarDTO CarToCarDTO(Car car);         @Named("oldStandard")         default String getLengthType(int length) {                 if (length > 5) {                         return "large";                 } else {                         return "small";                 }         }         @Named("newStandard")         default String getLengthType2(int length) {                 if (length > 7) {                         return "large";                 } else {                         return "small";                 }         } }

表达式自定义映射

通过表达式,能够蕴含来自多种语言的构造。

目前仅反对 Java 作为语言。例如,此性能可用于调用构造函数,整个源对象都能够在表达式中应用。应留神仅插入无效的 Java 代码:MapStruct 不会在生成时验证表达式,但在编译期间生成的类中会显示谬误。

@Data @AllArgsConstructor public class Driver {         private String name;         private int age; }
@Mapper public interface CarMapper {         CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);         @Mapping(source = "car.numberOfSeats", target = "seatCount")         @Mapping(target = "driver", expression = "java( new com.alibaba.my.mapstruct.example4.beans.Driver(person.getName(), person.getAge()))")         CarDTO CarToCarDTO(Car car, Person person); } 

默认表达式是默认值和表达式的组合:

@Mapper( imports = UUID.class )public interface SourceTargetMapper {         SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );         @Mapping(target="id", source="sourceId", defaultExpression = "java( UUID.randomUUID().toString() )")         Target sourceToTarget(Source s); }

装璜器自定义映射

在某些状况下,可能须要自定义生成的映射办法,例如在指标对象中设置无奈由生成的办法实现设置的附加属性。

实现起来也很简略,用装璜器模式实现映射器的一个抽象类,在映射器Mapper中增加注解@DecoratedWith指向装璜器类,应用时还是失常调用。

@Mapper @DecoratedWith(CarMapperDecorator.class) public interface CarMapper {         CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);         @Mapping(source = "numberOfSeats", target = "seatCount")         CarDTO CarToCarDTO(Car car); }public abstract class CarMapperDecorator implements CarMapper {         private final CarMapper delegate;         protected CarMapperDecorator(CarMapper delegate) {                 this.delegate = delegate;         }         @Override         public CarDTO CarToCarDTO(Car car) {                 CarDTO dto = delegate.CarToCarDTO(car);                 dto.setMakeInfo(car.getMake() + " " + new SimpleDateFormat( "yyyy-MM-dd" ).format(car.getCreateDate()));                 return dto;         } }

原文链接
本文为阿里云原创内容,未经容许不得转载。