当前位置: 首页 > 后端技术 > Java

Java对象转换方案分析及mapstruct实践

时间:2023-04-02 01:03:39 Java

介绍:随着系统模块分层的不断细化,在Java的日常开发中不可避免地涉及到各种对象的转换,如DO、DTO、VO等。映射和转换代码是一项繁琐、重复且容易出错的任务。一个好的工具可以帮助减少工作量,提高开发效率,减少bug的发生。随着系统模块层次结构的不断细化,Java的日常开发中不可避免地涉及到各种对象的转换,例如:DO、DTO、VO等,编写映射转换代码是一项繁琐、重复且容易出错的工作,一个好的工具有助于减少工作量,提高开发工作的效率,减少bug的发生。两种常见方案及分析1fastjsonCarDTOentity=JSON.parseObject(JSON.toJSONString(carDO),CarDTO.class);该方案性能非常差,因为它生成一个中间的json格式字符串,然后将其转换为目标对象。中间会生成一个JSON格式的字符串。如果转换太多,gc会很频繁。同时对复杂场景的支持不足,所以很少使用。2BeanUtil类BeanUtil.copyProperties()结合手写get和set,简单转换直接用BeanUtil,复杂转换自己手动写get和set。该方案的痛点是代码编写效率低,冗余复杂,略显丑陋,BeanUtil由于使用反射调用赋值,性能不高。只适合bean数量少,内容量少,转换不频繁的场景。apache.BeanUtilsorg.apache.commons.beanutils.BeanUtils.copyProperties(做,实体);由于使用反射和自身的设计问题,该解决方案性能不佳。团体发展法规明确禁止使用它。spring.BeanUtilsorg.springframework.beans.BeanUtils.copyProperties(do,entity);该方案对apache的BeanUtils做了很多优化,整体性能提升不少,但还是不如使用反射处理native代码,其次对复杂场景支持不够。3beanCopierBeanCopiercopier=BeanCopier.create(CarDO.class,CarDTO.class,false);copier.copy(do,dto,null);该方案动态生成代理类的子类,实际上是通过字节码转换为性能最好的get和set方法,重要的开销是创建BeanCopier,整体性能接近原生代码处理,比BeanUtils好很多,尤其是当数据量很大,但对复杂场景的支持不足时。4各种Mapping框架的分类ObjectMapping技术从大的角度可以分为两类,一类是运行时转换,一类是编译时转换:运行时反射调用set/get或者直接给member赋值变数。这种方式通过invoke进行赋值,一般使用beanutil、Javassist等开源库来实现。运行时对象转换的代表主要是Dozer和ModelMaper。set/get代码的class文件在编译时动态生成,运行时直接调用类的set/get方法。其实这个方法里面还是会有set/get的代码,只是开发者不需要自己写。这一类的代表有:MapStruct、Selma、Orika。不管分析什么样的Mapping框架,基本上都是通过xml配置文件或者注解来进行用户配置,然后生成映射关系。编译时生成class文件的方法要求DTO仍然有set/get方法,但调用被阻塞;而运行时反射方法在一些直接填充字段的方案中也可以省略set/get代码。采用编译时生成类的方式,将源码保存在本地,方便排查问题。编译时生成类的方法是因为java和class文件只在编译时出现,所以热部署会受到一定影响。由于反射式的很多内容都是黑盒,在排查问题的时候不如编译时生成类的方式方便。参考GitHub上的java-object-mapper-benchmark项目,查看主要框架的性能对比。由于反射调用是在运行时根据映射关系执行的,因此其执行速度会明显降低N个数量级。通过编译生成类代码的方法与直接编写代码没有太大区别,但是由于代码是通过模板生成的,代码质量没有手工编写的那么高,也会造成一定的性能损耗。从综合性能、成熟度、易用性、扩展性等方面来说,mapstruct是一个优秀的框架。三Mapstruct使用指南1Maven介绍2简单入门案例DO和DTO这里使用lombok来简化代码。lombok的原理也是在编译时生成get、set等简化代码。@DatapublicclassCar{privateStringmake;私有intnumberOfSeats;私人CarType类型;}@DatapublicclassCarDTO{privateStringmake;私人座位数;私有字符串类型;}定义Mapper@Mapper中描述的映射,编辑mapstruct时会根据这个描述生成实现类:当一个属性与其目标实体副本同名时,会被隐式映射。当目标实体中的属性具有不同的名称时,可以通过@Mapping注释指定其名称。@Mapper公共接口CarMapper{@Mapping(source="numberOfSeats",target="seatCount")CarDTOCarToCarDTO(Carcar);}使用Mapper通过Mappers工厂生成静态实例。@MapperpublicinterfaceCarMapper{CarMapperINSTANCE=Mappers.getMapper(CarMapper.class);@Mapping(source="numberOfSeats",target="seatCount")CarDTOCarToCarDTO(汽车汽车);}汽车汽车=新汽车(...);CarDTOcarDTO=CarMapper.INSTANCE.CarToCarDTO(汽车);getMapper会去加载接口的Impl后缀的实现类。通过生成springbean注入,Mapper注解加上spring配置会自动生成一个bean,可以直接通过bean注入访问。@Mapper(componentModel="spring")公共接口CarMapper{@Mapping(source="numberOfSeats",target="seatCount")CarDTOCarToCarDTO(Carcar);自动生成的MapperImpl内容如果配置了springbean访问,会在注解中自动添加@Component就可以了。3反向映射的高级使用如果是双向映射,比如从DO到DTO,从DTO到DO,正向方法和反向方法的映射规则通常是相似的,通过切换source和reverse方法可以很容易的实现反向映射目标。使用注解@InheritInverseConfiguration表示一个方法应该继承对应的逆向方法的逆向配置。@MapperpublicinterfaceCarMapper{CarMapperINSTANCE=Mappers.getMapper(CarMapper.class);@Mapping(source="numberOfSeats",target="seatCount")CarDTOCarToCarDTO(汽车汽车);@InheritInverseConfiguration汽车CarDTOToCar(CarDTOcarDTO);}Updatebeanmapping在某些情况下,不需要通过映射转换生成新的bean,而是更新已有的bean。@MapperpublicinterfaceCarMapper{CarMapperINSTANCE=Mappers.getMapper(CarMapper.class);@Mapping(source="numberOfSeats",target="seatCount")voidupdateDTOFromCar(Carcar,@MappingTargetCarDTOcarDTO);集合映射集合类型(List、Set、Map等)以与映射bean类型相同的方式完成,即通过在映射器接口中定义具有所需源和目标类型的映射方法。MapStruct支持JavaCollectionFramework中的各种可迭代类型。生成的代码将包含循环遍历源集合,转换每个元素并将其放入目标集合。如果在给定的映射器或它使用的映射器中找到集合元素类型的映射方法,将调用此方法执行元素转换,如果源和目标元素类型存在隐式转换,则此转换将是叫。@MapperpublicinterfaceCarMapper{CarMapperINSTANCE=Mappers.getMapper(CarMapper.class);@Mapping(source="numberOfSeats",target="seatCount")CarDTOCarToCarDTO(汽车汽车);ListcarsToCarDtos(Listcars);SetintegerSetToStringSet(Set整数);@MapMapping(valueDateFormat="dd.MM.yyyy")MaplongDateMapToStringStringMap(Mapsource);}编译时生成的实现类:MultipleSourceParameterMappingMapStruct也支持多源参数的映射方式。例如,将多个实体组合成一个数据传输对象。原来的情况下,添加了一个Person对象,CarDTO中添加了driverName属性,从Person对象中获取。@MapperpublicinterfaceCarMapper{CarMapperINSTANCE=Mappers.getMapper(CarMapper.class);@Mapping(source="car.numberOfSeats",target="seatCount")@Mapping(source="person.name",target="driverName")CarDTOCarToCarDTO(Carcar,Personperson);}编译生成的代码:默认值和常量映射如果对应的源属性为null,可以指定一个默认值,为目标属性设置一个预定义的值。在任何情况下,都可以指定常量来设置此类预定义值。默认值和常量被指定为字符串值。当目标类型是原始类型或装箱类型时,字符串值将采用字面量,在这种情况下,只要是有效字面量,位/八进制/十进制/十六进制模式都是允许的。在所有其他情况下,常量或默认值通过内置转换或调用其他映射方法进行类型转换以匹配目标属性的所需类型。@MapperpublicinterfaceSourceTargetMapper{SourceTargetMapperINSTANCE=Mappers.getMapper(SourceTargetMapper.class);@Mapping(target="stringProperty",source="stringProp",defaultValue="undefined")@Mapping(target="longProperty",source="longProp",defaultValue="-1")@Mapping(target="stringConstant",constant="ConstantValue")@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")TargetsourceToTarget(Sources);自定义映射方法或映射器在某些情况下,可能需要手动实现从一种类型到另一种类型的特定映射,而MapStruct无法生成这些映射。Mapper中可以定义默认的实现方法,生成的转换代码会调用相关方法:@MapperpublicinterfaceCarMapper{CarMapperINSTANCE=Mappers.getMapper(CarMapper.class);@Mapping(source="numberOfSeats",target="seatCount")@Mapping(source="length",target="lengthType")CarDTOCarToCarDTO(汽车汽车);默认StringgetLengthType(intlength){if(length>5){return"large";}else{返回“小”;您还可以定义其他映射器。在以下情况下,需要将Car中的Date转换为DTO中的String:publicclassDateMapper{publicStringasString(Datedate){returndate!=null?newSimpleDateFormat("yyyy-MM-dd").format(date):null;}publicDateasDate(Stringdate){try{returndate!=null?newSimpleDateFormat("yyyy-MM-dd").parse(date):null;}赶上(ParseExceptione){抛出新的RuntimeException(e);}}}@Mapper(uses=DateMapper.class)publicinterfaceCarMapper{CarMapperINSTANCE=Mappers.getMapper(CarMapper.class);@Mapping(source="numberOfSeats",target="seatCount")CarDTOCarToCarDTO(汽车汽车);}编译生成的代码:如果遇到多个相似的方法调用有歧义,需要使用@qualifiedBy指定:@MapperpublicinterfaceCarMapper{CarMapperINSTANCE=Mappers.getMapper(CarMapper.class);@Mapping(source="numberOfSeats",target="seatCount")@Mapping(source="length",target="lengthType",qualifiedByName="newStandard")CarDTOCarToCarDTO(汽车汽车);@Named("oldStandard")默认字符串getLengthType(intlength){if(length>5){return"large";}else{返回“小”;}}@Named("newStandard")默认字符串getLengthType2(intlength){if(length>7){返回“大”;}else{返回“小”;}}}Expressions通过表达式自定义映射,可以包含来自多种语言的结构目前只支持Java作为语言。例如,此功能可用于调用构造函数,并且可以在表达式中使用整个源对象。应注意仅插入有效的Java代码:MapStruct不会在构建时验证表达式,但在编译期间会在生成的类中显示错误。@Data@AllArgsConstructorpublicclassDriver{私有字符串名称;私人年龄;}@MapperpublicinterfaceCarMapper{CarMapperINSTANCE=Mappers.getMapper(CarMapper.class);@Mapping(source="car.numberOfSeats",target="seatCount")@Mapping(target="driver",expression="java(newcom.alibaba.my.mapstruct.example4.beans.Driver(person.getName(),person.getAge()))")CarDTOCarToCarDTO(Carcar,Person人);}默认表达式是默认值和表达式的组合:@Mapper(imports=UUID.class)publicinterfaceSourceTargetMapper{SourceTargetMapperINSTANCE=Mappers.getMapper(SourceTargetMapper.class);@Mapping(target="id",source="sourceId",defaultExpression="java(UUID.randomUUID().toString())")TargetsourceToTarget(Sources);}DecoratorCustomMapping在某些情况下,可能需要自定义生成,比如在目标对象中设置生成方法实现无法设置的额外属性。实现起来也非常简单。使用装饰器模式实现映射器的抽象类。在映射器Mapper中添加注解@DecoratedWith指向装饰器类。使用时还是正常调用。@Mapper@DecoratedWith(CarMapperDecorator.class)publicinterfaceCarMapper{CarMapperINSTANCE=Mappers.getMapper(CarMapper.class);@Mapping(source="numberOfSeats",target="seatCount")CarDTOCarToCarDTO(汽车汽车);}publicabstractclassCarMapperDecoratorimplementsCarMapper{privatefinalCarMapperdelegate;protectedCarMapperDecorator(CarMapperdelegate){this.delegate=delegate;}@OverridepublicCarDTOCarToCarDTO(Carcar){CarDTOdto=delegate.CarToCarDTO(car);dto.setMakeInfo(car.getMake()+""+newSimpleDateFormat("yyyy-MM-dd").format(car.getCreateDate()));返回数据;}}原文链接本文为阿里云原创内容,未经许可不得转载。