本文转载自微信公众号《阿Q说代码》,作者阿Q,转载本文请联系阿Q代码号。前几天,一位远在北京的朋友在群里抛出了“MapStruct”的概念。对于只闻其名却未曾谋面的我来说,决定好好研究一下。在本文中,我们从MapStruct的概念入手,通过具体的代码示例研究其用法,最后与“市面上”的其他工具进行对比!官方介绍首先,我们打开MapStruct的官网地址,映入眼帘的是下面的三部曲:Whatisit?MapStruct是一个代码生成器,它基于约定优于配置的方法极大地简化了JavaBean类型之间映射的实现。生成的映射代码使用普通方法调用,因此速度快、类型安全且易于理解。为什么?多层应用程序通常需要在不同对象模型(例如实体和DTO)之间进行映射。编写这样的映射代码是一项繁琐且容易出错的任务。MapStruct旨在通过尽可能自动化来简化这项工作。与其他映射框架不同,MapStruct在编译时生成bean映射,这确保了高性能,允许快速的开发人员反馈和彻底的错误检查。如何?MapStruct是一个注释处理器,可插入Java编译器,可用于命令行构建(Maven、Gradle等)或您首选的IDE。它使用合理的默认值,但用户可以在配置或实现特殊行为时自定义实现。官方网站上的解释总是冗长晦涩。当你看到这个的时候,你只需要记住MapStruct是用来做实体类映射的——实体类复制就可以了。源码地址:https://github.com/mapstruct/mapstruct官网推荐的Demo:https://github.com/mapstruct/mapstruct-examples简单实现我们注意到官网上有涉及简单示例的实现,我们用2分钟分析一波:1.引入依赖org.mapstructmapstruct-jdk81.3.0.Final//注解处理器,根据注解自动生成mapper实现org.mapstructmapstruct-processor1.2.0.Final我们将报告java:源参数中不存在名为“numberOfSeats”的属性。您指的是“null”吗?编译时出错。查阅资料后发现,mapstruct-processor和Lombok的版本需要统一:mapstruct-processor:1.2.0.Final,Lombok:1.16.14。2、准备实体类Car.java和数据传输类CarDto.java@NoArgsConstructor@AllArgsConstructor@DatapublicclassCar{privateStringmake;privateintnumberOfSeats;privateCarTypetype;}@Data@NoArgsConstructor@AllArgsConstructorpublicclassCarDto{privateStringmake;privateintseatCount;privateStringtype;}3.映射方法定义在@MapperpublicinterfaceCarMapper{CarMapperINSTANCE=Mappers.getMapper(CarMapper.class);@Mapping(source="numberOfSeats",target="seatCount")CarDtocarToCarDto(Carcar);}分析分析:@Mapper将接口标记为映射接口,编译时允许MapStruct处理器启动。这里的@Mapper注解不是mybatis的注解,而是org.mapstruct.Mapper的注解;实际映射方法carToCarDto()期望源对象Car为参数,返回目标对象CarDto,方法名可自由选择;对于源对象和目标对象对于源对象和目标对象中不同名称的属性,可以使用@Mapping注解来配置名称;对于源对象和目标对象中不同类型的属性,也可以使用@Mapping注解进行转换,例如:类型属性会从枚举类型转换为String;一个接口中可以有多个映射方法,对于所有这些方法,MapStruct都会生成一个实现;接口的实现实例可以从Mappers获取,接口声明了一个INSTANCE,为客户端提供了mapper访问的实现。4、实现类我们可以编译代码,然后我们会发现在目标文件中生成了CarMapperImpl.class文件:从代码中可以看出MapStruct自动为我们生成了set/get代码,以及特殊的枚举类处理。5.客户端@TestpublicvoidshouldMapCarToDto(){Carcar=newCar("Morris",5,CarType.SEDAN);CarDtocarDto=CarMapper.INSTANCE.carToCarDto(car);System.out.println(carDto);}执行结果:Summary:Based在mapper接口上,MapStruct在编译时动态生成set/get代码的class文件,在运行时直接调用class文件。MapStruct配置@Mapper下面我们打开上面提到的Mapper注解的源码。注解的解释是:将接口或抽象类标记为mapper,通过MapStruct激活该类实现的生成。我们找到componentModel属性,默认值为default,它有四个值供我们选择:default:mapper不使用组件模型,实例通常通过Mappers.getMapper(java.lang。班级);cdi:生成的映射mapper是一个application-scopedCDIbean,可以通过@Inject获取;spring:生成的mapper是一个Springbean,可以通过@Autowired获取;jsr330:生成的mapper由@javax.inject.Named和@Singleton注解,可以通过@inject获取;上面我们使用的是默认方式,当然我们也可以使用@Autowired来引入接口依赖,这里不再举例,感兴趣的朋友可以自己尝试一下!另外,我们可以看看uses属性:字段转换可以通过定义其他类来完成。下面我们举个小例子来演示一下:1、定义一个CarVo.java类@Data@NoArgsConstructor@AllArgsConstructorpublicclassCarVo{privateStringmake;privateintseatCount;privatebooleantype;}2.在mapper中定义一个vo转换为dto的方法CarDtocarVoToCarDto(CarVocarVo);未添加uses属性时,查看编译后的实现类publicCarDtocarVoToCarDto(CarVocarVo){if(carVo==null){returnnull;}else{CarDtocarDto=newCarDto();carDto.setMake(carVo.getMake());carDto.setSeatCount(carVo.getSeatCount());carDto.setType(String.valueOf(carVo.isType()));returncarDto;}}3.在mapper上添加uses属性,并指定自定义处理类,代码如下:@Mapper(uses={BooleanStrFormat.class})publicinterfaceCarMapper{...}/***自定义转换类*/@ComponentpublicclassBooleanStrFormat{publicStringtoStr(booleantype){if(type){return"Y";}else{return"N";}}publicbooleantoBoolean(Stringtype){if(type.equals("Y")){returntrue;}else{returnfalse;}}}/***查看编译后生成的实现类*/publicCarDtocarVoToCarDto(CarVocarVo){if(carVo==null){returnull;}else{CarDtocarDto=newCarDto();carDto.setMake(carVo.getMake());carDto.setSeatCount(carVo.getSeatCount());//调用自定义类中的方法carDto.setType(this.booleanStrFormat.toStr(carVo.是类型()));returncarDto;}}4.客户端代码@TestpublicvoidshouldMapCarVoToDto(){CarVocarVo=newCarVo("Morris",5,false);CarDtocarDto=CarMapper.INSTANCE.carVoToCarDto(carVo);System.out.println(carDto);}执行结果:@Mapping@Mapping可以用来配置bean属性或枚举常量的映射。默认是映射具有相同名称的属性。当然,您也可以使用source、expression或constant属性手动指定。接下来我们分析一下常用的属性值target:属性的target名称。同一个目标属性不能映射多次。如果用于映射一个枚举常量,会给出常量成员的名称,这种情况下源枚举的多个值可以映射到目标枚举的同一个值。source:属性的来源名称,如果注解的方法有多个来源参数,则属性名称必须用参数名称限定,例如“地址参数.city”;当没有找到匹配的属性时,MapStruct将寻找匹配的参数名称;当用于映射枚举常量时,会给出常量成员的名称;该属性不能与常量或表达式一起使用;dateFormat:通过SimpleDateFormat实现String与Date的相互转换。numberFormat:通过DecimalFormat实现Number和String的数值格式化。常量:设置指定目标属性的常量字符串。当指定的目标属性的类型为:primitive或boxed(如Long)时,MapStruct检查是否可以将primitive赋值给primitive或boxed类型作为有效文本。如果可能,MapStruct将分配为文字;如果不可能,MapStruct将尝试应用用户定义的映射方法。此外,MapStruct将常量作为字符串处理,并将通过应用匹配方法、类型转换方法或内置转换来转换值。此属性不能与source、defaultValue、defaultExpression或expression一起使用。表达式:是根据其设置指定目标属性的表达式。他的属性不能和source,defaultValue,defaultExpression,constant一起使用。ignore:忽略该字段。让我们使用expression属性来实现上面用uses实现的案例:1.定义方法@Mapping(target="type",expression="java(newcom.ittest.controller.BooleanStrFormat().toStr(carVo.isType()))")CarDtocarVoToDtoWithExpression(CarVocarVo);2.生成实现类@OverridepublicCarDtocarVoToDtoWithExpression(CarVocarVo){if(carVo==null){returnnull;}CarDtocarDto=newCarDto();carDto.setMake(carVo.getMake());carDto.setSeatCount(carVo.getSeatCount());carDto.setType(newcom.ittest.controller.BooleanStrFormat().toStr(carVo.isType()));returncarDto;}3.客户端@TestpublicvoidmapCarVoToDtoWithExpression(){CarVocarVo=newCarVo("Morris",5,false);CarDtocarDto=CarMapper.INSTANCE.carVoToDtoWithExpression(carVo);System.out.println(carDto);}运行结果:至于其他用法,大家可以多多探索。重要提示:枚举映射功能已被弃用并被ValueMapping取代。它将在后续版本中删除。@Mappings可以配置多个@Mappings,比如@Mappings({@Mapping(source="id",target="carId"),@Mapping(source="name",target="carName"),@Mapping(source="color",target="carColor")})@MappingTarget用于更新已有的对象,我们用一个例子来说明:1.创建BMWCar.java类@NoArgsConstructor@AllArgsConstructor@DatapublicclassBMWCar{privateStringmake;privateintnumberOfSeats;privateCarTypetype;privateStringcolor;privateStringprice;}2.在mapper中创建update方法,查看实现类//Update方法voidupdateBwmCar(Carcar,@MappingTargetBMWCarbwmCar);//实现类publicvoidupdateBwmCar(Carcar,BMWCarbwmCar){if(car!=null){bwmCar.setMake(car.getMake());bwmCar.setNumberOfSeats(car.getNumberOfSeats());bwmCar.setType(car.getType());}}3.客户端代码@TestpublicvoidupdateBwmCar(){Carcar=newCar("Morris",5,CarType.SEDAN);BMWCarbwmCar=newBMWCar("BWM",5,CarType.SPORTS,"RED","50w");System.out.println("更新汽车前:"+car.toString());System.out.println("更新前的BWMCar:"+bwmCar.toString());CarMapper.INSTANCE.updateBwmCar(car,bwmCar);System.out.println("更新的汽车:"+car.toString());System.out.println("更新的BWMCar:"+bwmCar.toString());}执行结果:扩展:多个对象映射到一个对象1.准备实体类Benz4SMall.java和Mall4S.java@NoArgsConstructor@AllArgsConstructor@DatapublicclassMall4S{privateStringaddress;privateStringmobile;}@Data@NoArgsConstructor@AllArgsConstructorpublicclassBenz4SMall{privateStringaddress;privateStringmobile;privateStringmake;privateintnumberOfSeats;}2.mapper创建转换方法并检查生成的实现类Benz4SMallmallCarToBenzMall(Carcar,Mall4Small4S);null&&mall4S==null){returnnull;}else{Benz4SMallbenz4SMall=newBenz4SMall();if(car!=null){benz4SMall.setMake(car.getMake());benz4SMall.setNumberOfSeats(car.getNumberOfSeats());}if(mall4S!=null){benz4SMall.setAddress(mall4S.getAddress());benz4SMall.setMobile(mall4S.getMobile());}returnbenz4SMall;}}3.客户@TestpublicvoidmallCarToBenzMall(){Carcar=newCar("Morris",5,CarType.SEDAN);Mall4Small4S=newMall4S("北京","135XXXX4503");Benz4SMallbenz4SMall=CarMapper.INSTANCE.mallCarToBenzMall(car,mall4S);System.out.println(benz4SMall.toString());}执行结果:深拷贝和浅拷贝深拷贝和浅拷贝最根本的区别在于是否真正获取一个对象的拷贝实体,而不是引用它。假设B复制了A,在修改A的时候,看B有没有变:如果B也变了,说明是浅拷贝,你缺人手!(修改堆内存中相同的值);不同的值in)MapStruct是创建一个新的对象,也就是深拷贝。MapStruct与其他拷贝的比较我们在平时的项目中经常会用到拷贝功能。今天我们就对比一下,直接抛出赵营超88的实验结果:输出结果:ManualCopy>Mapstuct>=cglibCopy>springBeanUtils>apachePropertyUtils>apacheBeanUtils可以理解为:manualCopy>cglib>reflection>Dozer。根据测试结果,我们可以得出结论,MapStruct在速度上是最好的,它的执行速度是ApacheBeanUtils的10倍,是SpringBeanUtils的4-5倍,和BeanCopier的速度差不多。总结:在大数据量的情况下,MapStruct和BeanCopier都具有高性能优势,其中MapStruct尤为出色。如果日常只是和少量的对象打交道,选择哪一个其实无所谓,但是当数据量很大时,建议使用MapStruct或者BeanCopier来提升接口性能。