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