目录:
你是否在做项目时遇到过以下情况:
- DTO(数据传输对象)与Entity之间的转换:在Java的Web应用中,通常不会直接将数据库中的Entity实体对象返回给前端。而是会创建一个DTO对象,这个DTO对象只包含需要返回给前端的字段。此时,就需要将Entity转换为DTO。
- 复杂对象的映射:当需要映射的对象包含大量的字段,或者字段之间存在复杂的依赖关系时,手动编写映射代码不仅繁琐,而且容易出错。
1.为什么选择MapStruct
1.1.常见的属性映射方法
一般来说,不使用MapStruct框架进行属性映射,常有的方法以下两种:
- Getter/Setter方法手动映射
这种方法最朴素,手动编写代码将源对象的属性存入目标对象,需要注意实体类中嵌套属性的判空操作以防止空指针异常。
- BeanUtils.copyProperties()方法进行映射
BeanUtils
底层使用的是反射机制实现属性的映射。反射是一种在运行时动态获取类信息、调用方法或访问字段的机制,无法利用JVM的优化机制,因此通常比直接方法调用慢得多。
此外,
BeanUtils
只能同属性映射,或者在属性相同的情况下,允许被映射的对象属性少;但当遇到被映射的属性数据类型被修改或者被映射的字段名被修改,则会导致映射失败。
1.2.MapStruct的优势
MapStruct是一个基于注解的Java代码生成器,它通过分析带有@Mapper注解的接口,在编译时自动生成实现该接口的映射器类。这个映射器类包含了用于执行对象之间映射的具体代码。
与常规方法相比,MapStruct具备的优势有:
- 简化代码。对于对象内属性较多的情况,使用MapStruct框架无须手动对每个属性进行get/set和属性判空操作。MapStruct可以通过注解和映射接口来定义映射规则,自动生成映射代码,从而大大简化了这种复杂对象的映射过程。
- 性能优越。相较于反射这种映射方法,MapStruct在编译期生成映射的静态代码,可以充分利用JVM的优化机制,对于企业级的项目应用来说,这种方式能大大提高数据复制的性能。
- 类型安全。由于MapStruct在编译期生成映射代码,这意味着如果源对象和目标对象的映射存在错误,那么可以在编译时就发现错误。相比之下,BeanUtils在运行时使用反射来执行属性复制,这可能会导致类型不匹配的问题在运行时才发现。
- 灵活映射。MapStruct可以轻松处理嵌套对象和集合的映射。对于嵌套对象,MapStruct可以递归地应用映射规则;对于集合,MapStruct可以自动迭代集合中的每个元素并应用相应的映射规则。
有开发者对比过两者的性能差距,如下表。这充分体现了MapStruct性能的强大。
对象转换次数属性个数BeanUtils耗时MapStruct耗时5千万次614秒1秒5千万次1536秒1秒5千万次2555秒1秒
2.MapStruct快速入门
在快速入门中,我们的任务是将dto的数据复制到实体类中。
2.1.导入Maven依赖
<!-- https://mvnrepository.com/artifact/org.mapstruct/mapstruct --><dependency><groupId>org.mapstruct</groupId><artifactId>mapstruct</artifactId><version>1.4.2.Final</version></dependency><!-- https://mvnrepository.com/artifact/org.mapstruct/mapstruct-processor --><dependency><groupId>org.mapstruct</groupId><artifactId>mapstruct-processor</artifactId><version>1.4.2.Final</version></dependency>
2.2.创建相关对象
注意,实体类要具有get/set方法,这里我使用了lombok的@Data注解来实现。
importlombok.Data;/**
* @author modox
* @date 2024/5/5
*/@DatapublicclassHotel{// 酒店名称privateString hotelName;// 酒店地址privateString hotelAddress;// 所在城市privateString hotelCity;// 联系电话privateString hotelPhone;}
dto类我使用了@Builder注解,可以快速为对象赋初始值。
importlombok.Builder;importlombok.Data;/**
* @author modox
* @date 2024/5/5
*/@Data@BuilderpublicclassHotelDTO{// 酒店名称privateString name;// 酒店地址privateString address;// 所在城市privateString city;}
2.3.创建转换器Converter
使用抽象类来定义转换器,只需中@Mapping注解中填写
target
和
source
的字段名,即可实现属性复制。
importorg.mapstruct.Mapper;importorg.mapstruct.Mapping;importorg.mapstruct.Mappings;/**
* @author modox
* @date 2024/5/5
*/@Mapper(componentModel ="spring")publicabstractclassTestConverter{//酒店详情@Mappings({@Mapping(target ="hotelName", source ="name"),@Mapping(target ="hotelAddress", source ="address"),@Mapping(target ="hotelCity", source ="city"),})publicabstractHoteldto2Hotel(HotelDTO hotelDTO);}
2.4.测试
在SpringBoot的测试类中测试,这里我使用DTO类的@Builder注解提供的方法为dto赋初值模拟实际开发,通过调用converter的方法实现属性映射。
@TestpublicvoidTest(){HotelDTO build =HotelDTO.builder().name("五星级酒店").address("中国").city("北京").build();TestConverter converter =newTestConverterImpl();Hotel hotel = converter.dto2Hotel(build);System.out.println(hotel);}
结果如图:
最后,我们可以发现在target包的converter的相同目录下,生成了TestConverter的实现类
里面为我们编写好了映射的代码。
3.MapStruct进阶操作
如果仅是这种简单层级的对象映射,还不足以体现MapStruct的灵活性。下面将介绍MapStruct的进阶技巧。
3.1.嵌套映射
假设我们的Hotel实体类中嵌套了另外一个实体类Master
@DatapublicclassHotel{// 酒店名称privateString hotelName;// 酒店地址privateString hotelAddress;// 所在城市privateString hotelCity;// 联系电话privateString hotelPhone;privateMaster master;@Data@NoArgsConstructor@AllArgsConstructorpublicstaticclassMaster{privateString personName;privateInteger personAge;}}
dto对象为:
@Data@BuilderpublicclassHotelDTO{// 酒店名称privateString name;// 酒店地址privateString address;// 所在城市privateString city;privateString personName;privateInteger personAge;}
我们需要把
personName
和
personAge
映射到Hotel实体类的Master中,怎么做?
很简单,只需要在target属性中加上Hotel实体类嵌套实体类的字段名,加字符
.
,再跟上嵌套类的字段名即可
//酒店详情@Mappings({@Mapping(target ="hotelName", source ="name"),@Mapping(target ="hotelAddress", source ="address"),@Mapping(target ="hotelCity", source ="city"),@Mapping(target ="master.personName", source ="personName"),@Mapping(target ="master.personAge", source ="personAge"),})publicabstractHoteldto2Hotel(HotelDTO hotelDTO);
结果如图:
3.2.集合映射
如果源对象和目标对象的集合的元素类型都是基本数据类型,直接在target和source中填写字段名即可。
若源对象和目标对象的集合元素类型不同,怎么做?
这个案例我们需要把DTO的personList映射到masterList中。
@Data@BuilderpublicclassHotelDTO{// 酒店名称privateString name;// 酒店地址privateString address;privateList<HotelDTO.Person> personList;@Data@NoArgsConstructor@AllArgsConstructorpublicstaticclassPerson{privateString personName;privateInteger personAge;}}
@DatapublicclassHotel{// 酒店名称privateString hotelName;// 酒店地址privateString hotelAddress;privateList<Master> masters;@Data@NoArgsConstructor@AllArgsConstructorpublicstaticclassMaster{privateString name;privateInteger age;}}
编写converter,这次需要进行两层映射。
第一层将person集合映射到master集合上。
第二层将person对象的属性映射到master对象中。
// 酒店详情@Mappings({@Mapping(target ="hotelName", source ="name"),@Mapping(target ="hotelAddress", source ="address"),@Mapping(target ="masters", source ="personList")})publicabstractHoteldto2Hotel(HotelDTO hotelDTO);@Mappings({@Mapping(target ="name", source ="personName"),@Mapping(target ="age", source ="personAge"),})publicabstractHotel.MastertoList(HotelDTO.Person person);
结果如图:
查看target包下的代码,可以发现MapStruct除了两层映射外,还帮你自动生成了迭代集合添加元素的代码,从而实现集合元素的复制。
@ComponentpublicclassTestConverterImplextendsTestConverter{publicTestConverterImpl(){}// 第一层映射publicHoteldto2Hotel(HotelDTO hotelDTO){if(hotelDTO ==null){returnnull;}else{Hotel hotel =newHotel();
hotel.setMasters(this.personListToMasterList(hotelDTO.getPersonList()));
hotel.setHotelAddress(hotelDTO.getAddress());
hotel.setHotelName(hotelDTO.getName());return hotel;}}// 第二层映射publicHotel.MastertoList(HotelDTO.Person person){if(person ==null){returnnull;}else{Hotel.Master master =newHotel.Master();
master.setName(person.getPersonName());
master.setAge(person.getPersonAge());return master;}}// 调用第二层映射,将person集合的元素添加到master中protectedList<Hotel.Master>personListToMasterList(List<HotelDTO.Person> list){if(list ==null){returnnull;}else{List<Hotel.Master> list1 =newArrayList(list.size());Iterator var3 = list.iterator();while(var3.hasNext()){HotelDTO.Person person =(HotelDTO.Person)var3.next();
list1.add(this.toList(person));}return list1;}}}
4.字段的逻辑处理
4.1.复杂逻辑处理(qualifiedByName和@Named)
这次我们需要把dto中的personName和personAge的list集合映射到实体类的masters集合中。常规的集合映射无法处理这种情况,这时需要使用到qualifiedByName和@Named进行特殊处理。
@Data@BuilderpublicclassHotelDTO{// 酒店名称privateString name;// 酒店地址privateString address;privateList<String> personName;privateList<Integer> personAge;}
@DatapublicclassHotel{// 酒店名称privateString hotelName;// 酒店地址privateString hotelAddress;// 主人privateList<Master> masters;@Data@NoArgsConstructor@AllArgsConstructorpublicstaticclassMaster{privateString personName;privateInteger personAge;}}
这就需要拿到两个list的数据,进行手动处理了。在@Mapping注解的qualifiedByName属性指定方法名定位处理逻辑的方法,@Named(“dtoToMasters”)。
利用stream流进行处理。
// 酒店详情@Mappings({@Mapping(target ="hotelName", source ="name"),@Mapping(target ="hotelAddress", source ="address"),@Mapping(target ="masters", source ="hotelDTO", qualifiedByName ="dtoToMasters")})publicabstractHoteldto2Hotel(HotelDTO hotelDTO);@Named("dtoToMasters")List<Hotel.Master>dtoToMasters(HotelDTO hotelDTO){List<String> personNames = hotelDTO.getPersonName();List<Integer> personAges = hotelDTO.getPersonAge();if(personNames !=null&& personAges !=null&& personNames.size()== personAges.size()){returnIntStream.range(0, personNames.size()).mapToObj(i ->newHotel.Master(personNames.get(i), personAges.get(i))).collect(Collectors.toList());}// 如果列表长度不匹配或其他错误情况,可以返回空列表或抛出异常returnCollections.emptyList();}
返回结果:
4.2.额外逻辑处理(ignore和@AfterMapping)
@Mappings的ignore属性,也可以对一个字段(不能是集合)进行额外逻辑处理。通常搭配@AfterMapping注解使用。
这个案例中,我们需要根据DTO的mount属性判断是否大于15,如果大于,则判断hotel实体类的isSuccess为true
@DatapublicclassHotel{// 酒店名称privateString hotelName;// 酒店地址privateString hotelAddress;// 酒店生意是否兴隆privateBoolean isSuccess;}
@Data@BuilderpublicclassHotelDTO{// 酒店名称privateString name;// 酒店地址privateString address;privateInteger mount;}
编写converter,注意@AfterMapping注解下的方法的参数列表,需要使用@MappingTarget注解指明目标对象,
// 酒店详情@Mappings({@Mapping(target ="hotelName", source ="name"),@Mapping(target ="hotelAddress", source ="address"),@Mapping(target ="isSuccess", ignore =true)})publicabstractHoteldto2Hotel(HotelDTO hotelDTO);@AfterMappingvoidisSuccess(HotelDTO hotelDTO,@MappingTargetHotel hotel){if(hotelDTO.getMount()==null){return;}boolean b = hotelDTO.getMount()>15;
hotel.setIsSuccess(b);}
测试方法
@TestpublicvoidTest(){HotelDTO build =HotelDTO.builder().name("五星级酒店").address("中国").mount(18).build();TestConverter converter =newTestConverterImpl();Hotel hotel = converter.dto2Hotel(build);System.out.println(hotel);}
返回结果
4.3.简单逻辑处理(expression)
expression可以在注解中编写简单的处理逻辑
在这个案例中我需要在实体类的nowTime字段获取当前时间。
@DatapublicclassHotel{// 酒店名称privateString hotelName;// 酒店地址privateString hotelAddress;privateLocalDateTime nowTime;}
直接在expression属性中使用方法获取当前时间。
// 酒店详情@Mappings({@Mapping(target ="hotelName", source ="name"),@Mapping(target ="hotelAddress", source ="address"),@Mapping(expression ="java(java.time.LocalDateTime.now())", target ="nowTime")})publicabstractHoteldto2Hotel(HotelDTO hotelDTO);
结果如下
版权归原作者 MoCrane 所有, 如有侵权,请联系我们删除。