封面来源:本文封面来源于 MapStruct 官网,如有侵权,请联系删除。
源码仓库:mofan212/mapstruct-demo
0. 阅读前言
本文内容基于 MapStruct 官方文档进行翻译并拓展,力求保证文章准确性,但博主英语水平堪忧,在某些处理上仍有很多不足,还望各位看官不吝赐教。
翻译工作极其耗时,本文禁止转载! 🙏
官网与推荐
MapStruct 官网:MapStruct
MapStruct 官方文档:Reference Guide
不错的 MapStruct 博客系列:属性映射工具——MapStruct(一)
1. MapStruct 概述
1.1 什么是 MapStruct
MapStruct 是一个Java注释处理器,用于生成类型安全的、高性能的、无依赖的 Bean 映射类。
在实际开发中经常会将各种对象相互转换,比如 DTO 转 BO,DTO 转 VO 等等,最简单的做法就是 new
对象,然后对属性值进行一个个地 GET/SET,但这样既繁琐又无聊。
如果有一个专门用来解决转换问题的工具就很棒了,MapStruct 就是这样的一个属性映射工具,只需要定义一个 Mapper 接口,MapStruct 就会自动实现这个映射接口,避免了复杂繁琐的映射实现。
补充与拓展
关于 Java 中的各种 O,可以参考下面这两篇文章:
java中各种O的含义(PO,DO,VO,TO,QO,BO,DAO,DTO,POJO)
浅析DTO、VO、DO、PO的概念
1.2 MapStruct 配置选项
MapStruct 代码生成器可以使用注释处理器选项进行配置。
在直接调用 javac 时,这些选项以 -Akey = value
的形式传递给编译器。 通过 Maven 使用 MapStruct 时,任何处理器选项都可以在 Maven 处理器插件的配置中使用 compilerArgs
传递,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 ... <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-compiler-plugin</artifactId > <version > 3.5.1</version > <configuration > <source > 1.8</source > <target > 1.8</target > <annotationProcessorPaths > <path > <groupId > org.mapstruct</groupId > <artifactId > mapstruct-processor</artifactId > <version > ${org.mapstruct.version}</version > </path > </annotationProcessorPaths > <showWarnings > true</showWarnings > <compilerArgs > <arg > -Amapstruct.suppressGeneratorTimestamp=true </arg > <arg > -Amapstruct.suppressGeneratorVersionInfoComment=true </arg > <arg > -Amapstruct.verbose=true </arg > </compilerArgs > </configuration > </plugin > ...
关于存在的选项,可以参看官方文档 Configuration options ,本文会在使用到的地方再具体讲解。
2. 定义映射器
2.1 Hello MapStruct
创建一个 Maven 项目,在 pom.xml
文件中添加以下配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 <properties > <org.mapstruct.version > 1.4.2.Final</org.mapstruct.version > <maven.compiler.source > 1.8</maven.compiler.source > <maven.compiler.target > 1.8</maven.compiler.target > </properties > <dependencies > <dependency > <groupId > org.mapstruct</groupId > <artifactId > mapstruct</artifactId > <version > ${org.mapstruct.version}</version > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > 1.18.12</version > <scope > provided</scope > </dependency > <dependency > <groupId > junit</groupId > <artifactId > junit</artifactId > <version > 4.13.1</version > <scope > test</scope > </dependency > </dependencies > <build > <plugins > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-compiler-plugin</artifactId > <version > 3.8.1</version > <configuration > <source > 1.8</source > <target > 1.8</target > <annotationProcessorPaths > <path > <groupId > org.mapstruct</groupId > <artifactId > mapstruct-processor</artifactId > <version > ${org.mapstruct.version}</version > </path > <path > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > 1.18.12</version > </path > </annotationProcessorPaths > </configuration > </plugin > </plugins > </build >
注意: Maven 插件要使用 3.6.0 版本及以上、Lombok 使用 1.16.16 版本及以上。不然就会出现以下错误:
java: No property named "numberOfSeats" exists in source parameter(s). Did you mean "null"?
如果 Lombok 版本为 1.18.16 及以上,请再添加上:
1 2 3 4 5 <path > <groupId > org.projectlombok</groupId > <artifactId > lombok-mapstruct-binding</artifactId > <version > 0.1.0</version > </path >
创建一个 Car
实体类:
1 2 3 4 5 6 7 8 9 10 11 12 @Getter @Setter @NoArgsConstructor @AllArgsConstructor public class Car { private String make; private int numberOfSeats; private String type; }
再创建一个 CarDto
:
1 2 3 4 5 6 7 8 9 10 11 12 @Getter @Setter @NoArgsConstructor @AllArgsConstructor public class CarDto { private String make; private int seatCount; private String type; }
还需要创建一个接口,用于对实体进行转换:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Mapper public interface CarMapper { CarMapper INSTANCE = Mappers.getMapper(CarMapper.class); @Mapping(source = "numberOfSeats", target = "seatCount") CarDto carToCarDto (Car car) ; }
最后创建测试类测试一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class CarMapperTest { @Test public void shouldMapCarToDto () { Car car = new Car ( "Morris" , 5 , "Sedan" ); CarDto carDto = CarMapper.INSTANCE.carToCarDto(car); assertNotNull(carDto); assertEquals("Morris" , carDto.getMake()); assertEquals(5 , carDto.getSeatCount()); assertEquals("Sedan" , carDto.getType()); } }
运行测试方法,测试通过! 🎉
为什么写一个接口后就能够帮我们实现实体的转换呢?
其实 Java 实体映射工具有两种:
1、在运行期,使用反射调用 Setter/Getter 方法或者是直接对成员变量赋值,这种方式通过 invoke()
执行赋值。一般会使用 Beanutil、Javassist 等开源库进行实现,代表产品有 Dozer 和 ModelMaper。
2、在编译期,动态生成含有逐一赋值的 class
文件 ,在运行时直接调用该 class
文件,这类的代表有 MapStruct、Selma、Orika。
如果不使用任何类库,手动实现实体的转换,可以使用反射(对应第一种方式),也可以对调用每个属性的 Get/Set 方法进行赋值(对应第二种方式)。
还是有点懵?
查看刚刚项目的 target 目录,可以看到:
自动生成了 CarMapperImpl
实现类,其内部仍然使用的是逐一复制的方式,只不过不用手动去写了。
正因如此,原实体和目标实体最好都编写各属性对应的 Getter 和 Setter 方法,以及它们各自的无参构造器。除此之外,别瞎使用 Lombok 中的 @Accessors
注解。
由于这个 class 文件是在编译期生成的,相比使用反射来实现实体映射,这种方式的效率更高。
更多 Bean 映射工具的性能对比,可以参考:5种常见Bean映射工具的性能比对
注意: 由于这些实现类会在编译期生成并存放在 target 目录中,因此当更改实体信息或映射逻辑时,需要重新编译或删除 target 目录再执行程序,否则更改可能不会生效。
注解介绍
在上述 Demo 中,使用到两个注解:
@Mapper
:作用于接口上,告诉 MapStruct 需要编译期生成哪个接口的实现类。
在生成的实现类中,当原实体的属性与其目标实体对应的属性名称 相同 时,就直接被 隐式映射 。如果名称不一样,就需要使用到 @Mapping
注解。
@Mapping
:常作用于方法上,用于原实体与目标实体属性名称不相同时指定映射关系。
2.2 关闭隐式映射
MapStruct 存在默认的隐式映射,也可以关闭它,只需在接口对应的方法上添加注解:
1 @BeanMapping(ignoreByDefault = true)
还是针对最开始的 Demo,在接口中的唯一方法上添加这个注解:
1 2 3 4 @BeanMapping(ignoreByDefault = true) @Mapping(source = "numberOfSeats", target = "seatCount") @Mapping(source = "make", target = "make") CarDto carToCarDto (Car car) ;
在修改一下测试方法,查看打印结果:
1 2 3 4 5 6 7 8 @Test public void testBeanMapping () { Car car = new Car ( "Morris" , 5 , "Sedan" ); CarDto carDto = CarMapper.INSTANCE.carToCarDto(car); System.out.println(carDto.getMake()); System.out.println(carDto.getSeatCount()); System.out.println(carDto.getType()); }
Morris
5
null
由于没有使用 @Mapping
注解为 type
指定映射关系,因此 type
的属性值是 null
。
但一般来说,没人会闲着没事把隐式映射关闭。🤨
2.3 @Mapping 注解
@Mapping
注解常作用于方法上,其实它也可以作用于注解之上,允许 @Mapping
用于其他(用户自定义的)注释以实现重用。
具体使用
新建 UserInfo
实体类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Getter @Setter @NoArgsConstructor @AllArgsConstructor public class UserInfo { private String id; private String username; private String pwd; private String dept; private Integer age; private Date createTime; }
再新建 StudentDto
类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Getter @Setter @NoArgsConstructor @AllArgsConstructor public class StudentDto { private String id; private String username; private String password; private String department; private String age; private Date createTime; }
要实现 UserInfo
向 StudentDto
转换。
再创建一个 FromUserInfo
注解,在这个注解之上使用 @Mapping
注解:
1 2 3 4 5 6 7 8 9 @Retention(RetentionPolicy.CLASS) @Mapping(target = "id", ignore = true) @Mapping(target = "createTime", expression = "java(new java.util.Date())") @Mapping(source = "dept", target = "department") public @interface FromUserInfo {}
创建 UserInfoMapper
接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Mapper public interface UserInfoMapper { UserInfoMapper INSTANCE = Mappers.getMapper(UserInfoMapper.class); @FromUserInfo @Mapping(source = "pwd", target = "password") StudentDto toStudent (UserInfo userInfo) ; }
最后来个测试类测试一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class UserInfoMapperTest { @Test public void testToStudent () { UserInfo userInfo = new UserInfo ("1" , "mofan" , "123456" , "开发部" , 19 , new Date (1604325269401L )); StudentDto studentDto = UserInfoMapper.INSTANCE.toStudent(userInfo); System.out.println(studentDto.getId()); System.out.println(studentDto.getUsername()); System.out.println(studentDto.getPassword()); System.out.println(studentDto.getDepartment()); System.out.println(studentDto.getAge()); System.out.println(studentDto.getCreateTime()); } }
运行测试类后,控制台输出:
null
mofan
123456
开发部
19
Tue Feb 02 18:33:11 CST 2021
@Mapping
属性讲解
上述案例中又使用了 @Mapping
注解的两个属性:ignore
和 expression
。
在实体转换时,如果不想让某个属性值从源实体赋值到新实体,可以使用 @Mapping
注解的 target
指定新实体中不想被赋值的属性,同时将 ignore
设置为 true
。
expression
的含义就是表达式,使用这个属性,可以为新实体中某个属性指定成表达式设置的值。表达式格式为:java(<EXPRESSION>)
,其实就是一句 Java 代码。比如想让 username 字段设置为 默烦 ,可以这些写:
1 @Mapping(target = "username", expression = "java(\"默烦\")")
需要注意的是: source
属性和 expression
属性不能同时存在。使用 expression
时,其值的 Java 代码所使用的类最好使用全类名,但如果这个 Java 代码会抛异常就不要使用 expression
了,而是需要自定义方法并在 qualifyByName
中引用。
像上述案例中,createTime
的值是指 2020-11-02 21:54:29
,为其指定表达式后,原本的值发生了改变。
如果仔细观察,还能发现 MapStruct 会对数据进行自动转换。比如在原实体中的 age 属性是 Integer
类型的,而 目标实体中的 age 属性是 String
类型的,即便如此,仍然能够映射成功。 👍
@Mapping 其他属性
@Mapping
注解还有其他的属性:
dateFormat:原属性是 Date
,转化为 String
numberFormat:数值类型与 String
类型之间的转化
constant:不管源属性值,直接将目标属性设置为指定的常量
qualifiedByName:根据自定义的方法进行赋值
defaultValue:默认值。如果原属性值为 null
,那么设置目标属性值为指定的默认值
这种方式有什么用?
如果有若干个 DTO 都由同一实体转换而来,这些 DTO 有些属性名称是相同的,但有些又不一样,那么我们就可以编写一个注解,在这个注解上使用 @Mapping
注解,以减少重复代码。
使用 @Mapping
注解作用与其他注解上并使用其他注解时,可以让若干个 DTO 都具备相同的特性。在官网上称这种观点为 “duck-typing”(鸭子类型),当一个东西叫声像鸭子,走路像鸭子,还会游泳,那么它很有可能就是鸭子 🦆。简单来说就是特性决定了类型,而不是类型决定了特性。鸭子类型在动态语言中经常使用,比如 Python,这使得 Python 不像 Java 那样专门去弄一大堆的设计模式。🤪
好了,说远了,回到 MapStruct 上。
但是官网又说,这个功能尚不成熟,用户应当谨慎使用,尤其是在不确定何时始终存在属性时:
the method on which the problem occurs is displayed, as well as the concerned values in the @Mapping annotation. However, the composition aspect is not visible.
2.4 默认方法与抽象类
在 JDK 8 及其更高的版本中,接口中也可以有方法的实现,但是这些方法必须被 static
或 default
关键词修饰。
使用 MapStruct 可以实现简单类型的映射,但是没办法完成一些带有逻辑的映射。比如在原实体中有一个名为 age
的属性,在映射到新实体时,想让 age
的值加一,这对于 MapStruct 来说是无法做到的。
此时可以在接口中定义默认方法,只要参数是原实体类型,返回值是新实体类型,那么使用 MapStruct 生成接口的实现类时将调用这个默认方法。
除此之外,还可以使用抽象类来定义映射器,而不仅仅是接口。 在这种情况下,MapStruct 会生成抽象类的子类,并实现所有的抽象方法。而对于非抽象方法来说,只要参数和返回值类型匹配,MapStruct 也会子类中调用这些方法。与声明默认方法相比,使用抽象类来定义映射器可以在映射器类中声明额外的字段。
2.5 多个源参数
MapStruct 支持带有多个源参数的映射方法。 比如,可以将实体 A 中的部分属性值和实体 B 中的部分属性值映射到同一 DTO 中:
1 2 3 4 5 6 @Mapper public interface AddressMapper { @Mapping(source = "person.description", target = "description") @Mapping(source = "address.houseNo", target = "houseNumber") DeliveryAddressDto personAndAddressToDeliveryAddressDto (Person person, Address address) ; }
与单参数映射方法一样,属性是按名称映射的。
如果多个源参数中定义了具有相同名称的属性,那么必须使用 @Mapping
注释来指定要从哪个源参数中的同名属性映射到目标实体,否则就会报错。对于仅在给定源参数中出现一次的属性,由于 隐式映射 的关系,因此可以选择性地指定名称。
注意: 具有多个源参数的映射方法将在 所有 源参数为空的情况下才返回 null
。否则,目标实体将被实例化,同时将所有来自所提供参数的属性值映射到目标实体中。
MapStruct 还支持直接引用源参数。 比如源参数中有一个 Integer
类型的参数,可以直接将这个参数映射到目标实体中:
1 2 3 4 5 6 @Mapper public interface AddressMapper { @Mapping(source = "person.description", target = "description") @Mapping(source = "hn", target = "houseNumber") DeliveryAddressDto personAndAddressToDeliveryAddressDto (Person person, Integer hn) ; }
2.6 多个嵌套属性映射
如果在实体 A 中存在某一属性 B,其类型是一个实体,现在想将实体 A 中的 B 对象映射到某一不含任何复杂对象类型属性的 DTO 中,那么需要取出属性 B 的各个属性进行一一映射。
如果实体 A 存在多个复杂对象类型的属性,依次进行映射会十分繁琐。
MapStruct 支持使用 .
作为 @Mapping
注释中 target
的属性值。这告诉 MapStruct 将原实体中所有复杂对象类型的属性中的所有属性映射到目标实体。
1 2 3 4 5 6 7 @Mapper public interface CustomerMapper { @Mapping(target = "name", source = "record.name") @Mapping(target = ".", source = "record") @Mapping(target = ".", source = "account") Customer customerDtoToCustomer (CustomerDto customerDto) ; }
上述代码将 customerDto
对象中的 record
对象和 account
对象的各个属性都映射到目标实体中,但由于 record
和 account
都有 name
属性,因此需要显式指定以消除歧义。
2.7 更新存在的实例
在某些情况下,可能并不需要创建一个新的目标实体,而是更新一个现有的实例,这个时候就需要用到 @MappingTarget
注解。
简单来说:一个原实体 A 的属性数量比目标实体 B 少,因此将 A 映射为 B 时,B 中有些属性值会是空。B 中的这些属性是存在于实体 C 中的,那么可以使用 C 来更新 B。
具体使用
原实体 UserInfo
:
1 2 3 4 5 6 7 8 9 10 11 12 @Getter @Setter @NoArgsConstructor @AllArgsConstructor public class UserInfo { private String id; private String username; private String pwd; private String dept; private Integer age; private Date createTime; }
目标对象 TeacherDto
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Getter @Setter @AllArgsConstructor @NoArgsConstructor public class TeacherDto { private String id; private String username; private String password; private String department; private String age; private Date createTime; private String firstName; private String lastName; private Integer sex; private String idCardNum; }
另一实体 PersonInfo
:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Getter @Setter @AllArgsConstructor @NoArgsConstructor public class PersonInfo { private String firstName; private String lastName; private Integer gender; private String idCardNum; }
转换接口中的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Mapping(source = "pwd", target = "password") @Mapping(source = "dept", target = "department") TeacherDto toTeacherDto (UserInfo userInfo) ; @Mapping(source = "gender", target = "sex") void updateTeacherDto (PersonInfo personInfo, @MappingTarget TeacherDto teacherDto) ;
测试方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Test public void testToTeacherDto () { UserInfo userInfo = new UserInfo ("1" , "mofan" , "123456" , "开发部" , 19 , new Date (1604325269401L )); TeacherDto teacherDto = UserInfoMapper.INSTANCE.toTeacherDto(userInfo); assertEquals("123456" , teacherDto.getPassword()); assertNull(teacherDto.getFirstName()); PersonInfo personInfo = new PersonInfo (); personInfo.setFirstName("默" ); personInfo.setLastName("烦" ); personInfo.setGender(1 ); personInfo.setIdCardNum("123456788914725896" ); UserInfoMapper.INSTANCE.updateTeacherDto(personInfo, teacherDto); assertEquals("默" , teacherDto.getFirstName()); assertEquals(1 , teacherDto.getSex().intValue()); }
在更新实例时,如果存在字段名不一样的情况,仍然可以使用 @Mapping
注解进行指定。
在更新对象的方法中,除了使用 void
作为返回值外,还可以使用目标参数类型作为返回值,这表示在更新目标参数后又返回它。比如:
1 2 @Mapping(source = "gender", target = "sex") TeacherDto updateTeacherDto (PersonInfo personInfo, @MappingTarget TeacherDto teacherDto) ;
2.8 反向映射
上述实例中,都是用实体转换为 DTO,那么怎么将 DTO 转换回实体呢?
当然可以使用 @Mapping
注解一一指定,但这样也太麻烦了,有没有什么简单的方法呢?
MapStruct 提供了一个名为 @InheritInverseConfiguration
的注解用于反向映射。该注解只有一个属性name
,表示原映射名,即原映射方法名。
具体使用
新实体 PeopleInfo
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Getter @Setter @NoArgsConstructor @AllArgsConstructor public class PeopleInfo { private Long id; private String name; private String age; private Date createTime; private Double weight; private String bloodType; private GenderEnum gender; }
打工人 WorkerDto
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Setter @Getter @NoArgsConstructor @AllArgsConstructor public class WorkerDto { private Long id; private String name; private Integer age; private String createTime; private String weight; private String bloodType; private Integer gender; }
一个用于转换实体的接口 PeopleInfoMapper
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 @Mapper public interface PeopleInfoMapper { PeopleInfoMapper INSTANCE = Mappers.getMapper(PeopleInfoMapper.class); @Mappings({ @Mapping(target = "name", defaultValue = "默烦"), @Mapping(target = "age", expression = "java(java.lang.Integer.valueOf(peopleInfo.getAge()) + 1)"), @Mapping(target = "createTime", dateFormat = "yyyy-MM-dd"), @Mapping(target = "weight", numberFormat = "0.00"), @Mapping(target = "bloodType", constant = "A 型血"), @Mapping(source = "gender", target = "gender", qualifiedByName = "getGenderEnumCode") }) WorkerDto toWorkerDto (PeopleInfo peopleInfo) ; @InheritInverseConfiguration(name = "toWorkerDto") @Mappings({ @Mapping(target = "age", expression = "java(java.lang.String.valueOf(workerDto.getAge()))"), @Mapping(target = "bloodType", constant = "B 型血"), @Mapping(target = "gender", qualifiedByName = "getGenderEnum") }) PeopleInfo fromPersonInfo (WorkerDto workerDto) ; @Named("getGenderEnum") default GenderEnum getGenderEnum (Integer code) { for (GenderEnum value : GenderEnum.values()) { if (value.getCode().equals(code)) { return value; } } return null ; } @Named("getGenderEnumCode") default Integer getGenderEnumCode (GenderEnum genderEnum) { if (genderEnum == null ) { return null ; } return genderEnum.getCode(); } }
编写一手测试方法: 😎
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @Test public void testInheritInverseConfiguration () { PeopleInfo peopleInfo = new PeopleInfo (); peopleInfo.setId(233L ); peopleInfo.setAge("18" ); peopleInfo.setCreateTime(new Date (1604325269401L )); peopleInfo.setWeight(122.2345 ); peopleInfo.setBloodType("AB 型血" ); peopleInfo.setGender(GenderEnum.MAN); WorkerDto workerDto = PeopleInfoMapper.INSTANCE.toWorkerDto(peopleInfo); assertEquals("默烦" , workerDto.getName()); assertEquals(1 , workerDto.getGender().intValue()); assertEquals(19 , workerDto.getAge().intValue()); System.out.println(workerDto.getCreateTime()); System.out.println(workerDto.getWeight()); System.out.println(workerDto.getBloodType()); PeopleInfo person = PeopleInfoMapper.INSTANCE.fromPersonInfo(workerDto); assertEquals("B 型血" , person.getBloodType()); assertEquals("19" , person.getAge()); assertEquals(GenderEnum.MAN, person.getGender()); }
运行测试方法后,测试通过,控制台打印:
2020-11-02
122.23
A 型血
总结
由于 age 的转化中使用了 expression
、bloodType 转化的过程中使用了 constant
,这两个属性对应的 @Mapping
注释都没有指定 source
。所以在使用 @InheritInverseConfiguration
注解进行反向映射时,需要使用 @Mapping
对这两个属性进行单独的映射,不然就会报错。
在 PeopleInfo
中 gender 属性类型是 GenderEnum
枚举类型,而 WorkerDto
中的 gender 属性是 Integer
类型的,因此在进行属性映射时,需要指定映射方式,这里涉及到 @Named
注解和 @Mapping
注解中 qualifiedByName
属性的使用。
2.9 直接使用字段映射
在前面的案例中,实现与实体之间的映射时都需要实体中有 Getter 和 Setter 方法,但 MapStruct 也支持没有 Getter 和 Setter 的公共字段映射。如果 MapStruct 为属性找不到合适的 Getter 和 Setter 方法,它会把字段作为读 / 写访问器。
如果一个字段被 public
或 public final
修饰,那么它会被认为是一个读访问器。如果一个字段被 static
修饰,它不会被认为是一个读访问器。
当一个字段 仅仅只 被 public
修饰时,它才会被认为是一个写访问器。如果一个字段被 final
或 static
修饰,它不会被认为是一个写访问器。
比如,这样两个实体是能够被相互映射的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Setter @Getter @NoArgsConstructor @AllArgsConstructor public class Customer { private Long id; private String name; } public class CustomerDto { public Long id; public String customerName; }
由于 name
属性和 customerName
属性名称不一样,在进行映射时,需要使用 @Mapping
指定规则。
2.10 使用构建器
在 MapStruct 中,支持通过构建器对不可变类型进行映射。
当进行映射时,MapStruct 会检查被映射的类中是否有构建器。这是通过 BuilderProvider 完成的,如果这个类存在构建器,那么该构建器将被用于映射。
BuilderProvider
的默认实现假设如下:
该类型拥有返回 Builder
对象的公共无参静态方法(该方法称为 构建器创建方法 )。比如 Person
类中有一个公共静态方法,它返回 PersonBuilder
。
Builder
类型有一个公共无参方法(该方法称为 构建方法 ),它返回正在构建的对象类型。比如 PersonBuilder
中有一个 build()
方法,该方法的返回值类型是 Person
。
在有多个 构建方法 的情况下,MapStruct 会寻找一个叫做 build()
的方法,如果存在这样的方法,将使用这个方法,否则编译错误。
可以在 @BeanMapping
、@Mapper
或 @MapperConfig
等注解中使用 @Builder
来定义特定的 构建方法 。
如果有多个 构建器创建方法 满足上述条件,DefaultBuilderProvider
会抛出 MoreThanOneBuilderCreationMethodException
异常。如果发生 MoreThanOneBuilderCreationMethodException
,MapStruct 将在编译期间写入警告,并且不使用任何生成器。
如果在被映射的类中存在上述的方法和构建器,为了完成映射,MapStruct 会调用构建器的构建方法并生成代码。
还可以通过 MapStruct 的 @Builder
的 disableBuilder
属性来禁用构建器检测。MapStruct 将在构建器被禁用的情况下回到常规的 Getter / Setter 来生成代码。还可以在 Maven 处理器插件的配置中使用 <compilerArgs>
添加 <arg>-Amapstruct.disableBuilders=true</arg>
来禁用构建器的检测。
对象工厂也被认为是构建器。例如,如果 PersonBuilder
中存在一个对象工厂,MapStruct 将使用对象工厂而不是 构建器创建方法 。
构建器的检测会影响 @BeforeMapping
和 @AfterMapping
注解的行为,这在后文会介绍。
具体使用
原实体 Animal
:
1 2 3 4 5 6 7 8 9 10 11 @Setter @Getter @NoArgsConstructor @AllArgsConstructor public class Animal { private String name; private String type; }
目标实体与构造器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 @Getter @Setter public class DogDto { private String name; private final String type; protected DogDto (DogDto.Builder builder) { this .type = builder.type; this .name = builder.name; } public static DogDto.Builder builder () { return new DogDto .Builder(); } public static class Builder { private String type; private String name; public Builder type (String type) { this .type = type; return this ; } public Builder name (String name) { this .name = name; return this ; } public DogDto create () { return new DogDto (this ); } } }
转换接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Mapper public interface AnimalMapper { AnimalMapper INSTANCE = Mappers.getMapper(AnimalMapper.class); DogDto toDogDto (Animal animal) ; }
测试方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class AnimalMapperTest { @Test public void testToDogDto () { Animal animal = new Animal (); animal.setName("小黑" ); animal.setType("dog" ); DogDto dogDto = AnimalMapper.INSTANCE.toDogDto(animal); assertEquals("小黑" , dogDto.getName()); assertEquals("dog" , dogDto.getTYPE()); } }
运行测试方法后,测试通过。查看 target 目录,可以看到生成的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class AnimalMapperImpl implements AnimalMapper { public AnimalMapperImpl () { } public DogDto toDogDto (Animal animal) { if (animal == null ) { return null ; } else { Builder dogDto = DogDto.builder(); dogDto.type(animal.getType()); dogDto.name(animal.getName()); return dogDto.create(); } } }
总结
这其实使用了一种设计模式 —— 构建者模式,或者说建造者模式。
在《Effective Java》一书中有提到,一个类的构造函数参数个数有多个时,而且这些参数有些是可选的参数 ,要考虑使用构建器,即使用构建者模式。
但上述代码中并没有体现可选参数的概念,要想体现也很简单,只需要在构建器中编写编写一个构造方法,构造方法的参数就是需要必填的参数。
每次使用构建者模式都会书写许多样板代码,Lombok 提供了一个 @Builder
注解,使用这个注解可以很快地实现构建者模式。使用 @Builder
生成的构造方法是共有的,不符合构建者模式的思想,因此还需要指定构造方法的访问修饰符。比如指定成 protected
:
1 2 3 4 5 6 7 8 9 10 11 @Getter @Setter @Builder @AllArgsConstructor(access = AccessLevel.PROTECTED) public class DogDto { private String name; private final String type; }
将 DogDto 修改成以上形式后,运行测试方法也是可以通过的。 👊
2.11 使用构造函数
在前文中,对象映射使用 Getter / Setter 完成。MapStruct 也支持使用构造方法来映射目标类型。
在进行映射时,MapStruct 会检查被映射的实体中是否存在构建器。如果没有构建器,MapStruct 会寻找一个可访问的构造方法。
构造方法的选择
当有多个构造函数时,MapStruct 按照以下优先级进行选择:
被 @Default
(该注解可以来自任何包!😲)注释所标注的公共构造方法(最高优先级);
如果 只有一个公共 的构造方法,那么它将被用于构造对象,而其他非公共的构造方法会被忽略;
如果存在一个无参构造方法,那么它将被用于构造对象,而其他构造方法会被忽略;
如果有多个符合条件的构造方法,会因为构造方法指代不清而造成编译错误。在这个时候,可以使用 @Default
注解来指定用于构造对象的构造方法。
官网给出的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class Vehicle { protected Vehicle () { } public Vehicle (String color) { } } public class Car { public Car () { } public Car (String make, String color) { } } public class Truck { public Truck () { } @Default public Truck (String make, String color) { } } public class Van { public Van (String make) { } public Van (String make, String color) { } }
在使用构造方法时,将使用构造方法的参数名并与目标属性进行匹配。当构造函数被 @ConstructorProperties
(该注解也可以来自任何包,只不过通常使用 java.beans.ConstructorProperties
)标注时,该注解将被用来获取参数的名称。
当目标实体中存在对象工厂方法或用 @ObjectFactory
标记的方法时,这些方法将优于目标中任何构造方法。在这种情况下,目标实体中的构造方法不会被使用。
具体使用
自己编写的 @Default
注解:
1 2 3 4 5 6 7 @Target(ElementType.CONSTRUCTOR) @Retention(RetentionPolicy.CLASS) public @interface Default {}
创建一个 Book
实体:
1 2 3 4 5 6 7 8 9 10 @Setter @Getter public class Book { private String bookName; private Double price; private String author; }
再创建一个 NovelDto
,后续会将 Book
对象转换为 NovelDto
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Getter public class NovelDto { private final String bookName; private final Double price; private String author; @Default @java .beans.ConstructorProperties({"bookName" , "price" , "author" }) public NovelDto (String book, Double price, String author) { this .bookName = book; this .price = price; this .author = "author" ; } public NovelDto (String bookName, Double price) { this .bookName = bookName; this .price = price; } }
一个用于实体映射的接口:
1 2 3 4 5 @Mapper public interface BookMapper { BookMapper INSTANCE = Mappers.getMapper(BookMapper.class); NovelDto toNovelDto (Book book) ; }
测试方法:
1 2 3 4 5 6 7 8 9 10 11 @Test public void testToNovel () { Book book = new Book (); book.setBookName("Effective Java" ); book.setPrice(78.0 ); NovelDto novelDto = BookMapper.INSTANCE.toNovelDto(book); assertEquals("Effective Java" , novelDto.getBookName()); assertEquals("author" , novelDto.getAuthor()); assertEquals(78.0 , novelDto.getPrice(), 0.0 ); }
运行测试方法后,测试通过!
查看生成的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public NovelDto toNovelDto (Book book) { if (book == null ) { return null ; } else { String bookName = null ; Double price = null ; String author = null ; bookName = book.getBookName(); price = book.getPrice(); author = book.getAuthor(); NovelDto novelDto = new NovelDto (bookName, price, author); return novelDto; } }
总结
通过生成代码来看,使用到了原目标实体的 Getter 方法,用到了目标实体的构造方法,因此在编码时需要保证存在这些方法 才能使用构造函数进行映射 。
使用 MapStruct 将 Book
实体转换成 NovelDto
时,在 NovelDto
中没有 Setter 方法,但存在多个构造方法,最终会选取合适的构造方法进行映射。
在 NovelDto
实体类中存在两个构造方法,且都是公共有参构造方法,因此需要使用 @Default
进行指明要使用哪个构造方法进行映射,如果不指明就会在编译阶段报错。
但是在 MapStruct 中并没有提供 @Default
注解,那怎么办?🤔
根据官方文档 14.1. Non-shipped annotations 的说话,只要使用任意一个能够作用在构造方法且名为 @Default
的注解就行,也就是任意第三方的、能作用于构造方法上的 @Default
注解都能够被 MapStruct 识别,并将注解标记的构造方法作为映射的构造方法。那问题来了,在没有引入第三方依赖,且当前项目中没有符合条件的 @Default
注解咋办?
自己写一个就行,MapStruct 也能识别。具体参见上述提供的代码。😁
使用目标实体的构造方法时,会使用该构造方法的参数名并与 来源实体 的属性进行匹配。务必保证这些名称是一样的,否则无法映射成功。 当然,也不是说名称不一样就不能映射,这时候可以使用 @ConstructorProperties
注解,使用这个注解指定构造方法的第几个参数对应来源对象的哪个 Getter 方法,从而完成参数名与属性的匹配。
那如果目标实体的属性名和原实体的属性名不一致怎么办?这就需要用到前面说的 @Mapping
注解了。😏
备注: 这一块或许比较难理解,建议使用上述代码进行小改然后一一理解。😔
2.12 将 Map 映射到对象
在某些情况下,可能需要将 Map<String, Object>
映射到某个对象,MapStruct 也是支持这种需求的,此时 @Mapping
中的 source
属性值为 Map
中的 key(把 Map
当成一个没有特定类型的对象)。比如:
1 2 3 4 5 6 7 8 9 10 11 12 @Setter @Getter public class Customer { private Long id; private String name; } @Mapper public interface CustomerMapper { @Mapping(target = "name", source = "customerName") Customer toCustomer (Map<String, String> map) ; }
生成的代码如下:
1 2 3 4 5 6 7 8 9 10 public class CustomerMapperImpl implements CustomerMapper { @Override public Customer toCustomer (Map<String, String> map) { if ( map.containsKey( "customerName" ) ) { customer.setName( map.get( "customerName" ) ); } } }
当使用 raw map(没有指定泛型的 Map
) 或没有字符串作为键的 Map
时,将生成警告。如果 Map
本身直接映射到其他目标属性,则不会生成警告。
3. 获取映射器
3.1 映射器工厂
当没有使用依赖注入框架(比如 Spring)时,可以使用 org.mapstruct.factory.Mappers
类来获取一个映射器(Mapper)实例。
使用方法: 调用该类的 Mappers#getMapper()
方法,传入要返回的 Mapper 接口类型。
通常来说,习惯于将映射器实例命名为 INSTANCE
。
在前文中,大部分都是使用接口来定义映射器,而在抽象类中应该这样定义映射器,比如:
1 2 3 4 5 @Mapper public abstract class CarMapper { public static final CarMapper INSTANCE = Mappers.getMapper(CarMapper.class); CarDto carToCarDto (Car car) ; }
这种模式使得客户端可以非常容易地使用映射器对象,而不需要重复实例化新的实例。
MapStruct 生成的映射器是无状态和线程安全的,在多线程环境下可以放心使用。
3.2 使用依赖注入
在实际项目中,一般都会使用依赖注入框架(比如:Spring),这个时候就需要通过依赖注入而不是通过 Mappers#getMapper()
来获取映射器对象。可以通过 @Mapper
的 componentModel
属性或使用配置选项中描述的 Configuration options 指定生成的映射器类应基于的组件模型。
目前,MapStruct 支持 CDI 和 Spring 。在这两种情况下,所需的注解将被添加到生成的映射器实现类中,以便使其同样受到依赖注入的影响。
@Mapper#componentModel
属性或 mapstruct.defaultComponentModel
配置选项支持的值:
值
注入方式
default
默认方式,映射器不使用组件模型,使用 Mappers.getMapper(Class)
来进行获取映射器实例
cdi
生成的映射器是一个应用程序范围的 CDI Bean,使用 @Inject
注入
spring
生成的映射器是一个单例的 Spring Bean,使用 @Autowired
注入
jsr330
生成的映射器中被 @Named
注解标记,使用 @Inject
注入
jakarta
生成的映射器中被 @Named
注解标记,使用 jakarta.inject.@Inject
注入(since 1.5.0.Final)
从 MapStruct 1.5.0.Final 开始,这些值在 org.mapstruct.MappingConstants.ComponentModel
类中以常量的形式存在。
当 mapstruct.defaultComponentModel
配置选项和 @Mapper#componentModel
同时指定了值,以 @Mapper
注解的为准。
使用其他映射器类的映射器将使用配置的组件模型获得这些映射器。比如,映射器 A 是一个 Spring Bean,映射器 A 中注入了映射器 B,那么映射器 B 也必须是一个可注入的 Spring Bean。
3.3 注入策略
使用依赖注入时,可以指定注入策略。
可以通过 @Mapper
和 @MapperConfig
注解的 injectionStrategy
属性或 mapstruct.defaultInjectionStrategy
来指定注入策略。注入策略有两种:构造方法和属性注入, 默认是属性注入。
针对 @Mapper
的 injectionStrategy
属性来说,其值是一个枚举:
1 2 3 4 5 6 public enum InjectionStrategy { FIELD, CONSTRUCTOR }
使用属性注入时,依赖关系将被注入到字段中。
使用构造函数注入时,将生成构造函数,依赖关系将通过构造函数注入。
如果 MapStruct 检测到 需要使用 @Mapper#uses
属性的实例进行映射,则生成的映射器将注入在 uses
属性中定义的类。注意 需要使用 几个字,如果参与映射字段不需要使用指定类型对应的实例进行映射,那么生成的映射器不会注入 uses
属性中定义的类。
对于抽象类或装饰器应使用属性注入,因为它们不能被实例化。
具体使用
使用最开始的 Car
和 CarDto
两个实体进行测试,省略实体代码。
导入 Spring 所需要的依赖:
1 2 3 4 5 6 7 8 9 10 11 12 <dependency > <groupId > org.springframework</groupId > <artifactId > spring-context</artifactId > <version > 5.2.7.RELEASE</version > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-test</artifactId > <version > 5.2.7.RELEASE</version > <scope > test</scope > </dependency >
编写一个 SpringConfig
配置类,主要用于扫描包:
1 2 3 4 5 6 7 @Configuration @ComponentScan("indi.mofan.mapper") public class SpringConfig {}
编写映射器,使用 Spring 的依赖注入:
1 2 3 4 5 6 7 8 9 @Mapper(componentModel = "spring") public interface CarInjectMapper { @Mapping(source = "numberOfSeats", target = "seatCount") CarDto carToCarDto (Car car) ; }
测试类与测试方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @RunWith(SpringRunner.class) @ContextConfiguration(classes = SpringConfig.class) public class DependencyInjectionTest { @Autowired private CarInjectMapper carInjectMapper; @Test public void shouldMapCarToDto () { Car car = new Car ("Morris" , 5 , "Sedan" ); CarDto carDto = carInjectMapper.carToCarDto(car); assertEquals("Morris" , carDto.getMake()); assertEquals(5 , carDto.getSeatCount()); assertEquals("Sedan" , carDto.getType()); } }
查看生成的实现类 CarInjectMapperImpl
,可以看到这个类被 Spring 的 @Component
注解标记。
4. 数据类型转换
在许多情况下,MapStruct 会自动处理类型转换。
例如,一个属性在源 Bean 中是 int
类型,但在目标 Bean 中是 String
类型,通过生成的代码可以看到分别调用 String.valueOf(int)
和 Integer.parseInt(String)
两个方法进行了类型的转换。
又比如在 Car
类型中有一个 Person
类型的 driver
属性,当对 Car
进行映射时,应当把 driver
映射为 PersonDto
类型。
4.1 隐式类型转换
在此列举一些常见的自动类型转换:
Java 基本数据类型和其对应的包装类之间。把包装类转换成基本数据类型时,会执行 null
值检查。
Java 基本数据类型和封装类型之间。例如 int
和 long
之间或 byte
和 Integer
之间。
注意: 从精度较大类型的数据转换为精度较小的数据时,可能会造成值或精度缺失(例如从 long
转换为 int
)。@Mapper
和 @MapperConfig
注解有一个属性 typeConversionPolicy
来控制警告或错误,其默认值为 ReportingPolicy.IGNORE
。
基本数据类型及其包装类和 String
之间。可以在 @Mapping
的 numberFormat
属性中指定 java.text.DecimalFormat
能理解的格式化字符串对数值格式化。
1 2 3 4 5 6 7 8 9 @Mapper public interface CarMapper { @Mapping(source = "price", numberFormat = "$#.00") CarDto carToCarDto (Car car) ; @IterableMapping(numberFormat = "$#.00") List<String> prices (List<Integer> prices) ; }
1 2 3 4 5 6 7 @Mapper public interface CarMapper { @Mapping(source = "power", numberFormat = "#.##E0") CarDto carToCarDto (Car car) ; }
JAXBElement<T>
和 T
之间,List<JAXBElement<T>>
和 List<T>
之间。
java.util.Calendar
或 java.util.Date
和 JAXB 的 XMLGregorianCalendar
之间。
java.util.Date
日期类型或 XMLGregorianCalendar
和 String
之间。可以在 @Mapping
的 dateFormat
属性中指定 java.text.SimpleDateFormat
能理解的格式化字符串对日期字符串格式化。
1 2 3 4 5 6 7 8 9 @Mapper public interface CarMapper { @Mapping(source = "manufacturingDate", dateFormat = "dd.MM.yyyy") CarDto carToCarDto (Car car) ; @IterableMapping(dateFormat = "dd.MM.yyyy") List<String> stringListToDateList (List<Date> dates) ; }
Jodas 中的日期时间类型(DateTime
、LocalDateTime
、LocalDate
、LocalTime
)和 String
之间。可以使用 @Mapping
的 dateFormat
属性对数值进行格式化。
Jodas 的 DateTime
类型和 javax.xml.datatype.XMLGregorianCalendar
、java.util.Calendar
之间。
Jodas 的 LocalDateTime
、LocalDate
和 javax.xml.datatype.XMLGregorianCalendar
、java.util.Date
之间。
java.time.LocalDate
、java.time.LocalDateTime
和 javax.xml.datatype.XMLGregorianCalendar
之间。
Java 8 日期时间类型(ZonedDateTime
、LocalDateTime
、LocalDate
、LocalTime
)和 String
之间。可以使用 @Mapping
的 dateFormat
属性对数值进行格式化。
Java 8 日期类型(Instant
、Duration
、Period
)和 String
之间。
Java 8 的 ZonedDateTime
类型和 Date
之间,当从给定的 Date
转换成 ZonedDateTime
时,使用系统默认时区。
Java 8 的 LocalDateTime
类型和 Date
之间,使用 UTC 作为时区。
Java 8 的 LocalDate
类型和 java.util.Date
、java.sql.Date
之间,使用 UTC 作为时区。
Java 8 的 Instant
类型和 Date
之间。
Java 8 的 ZonedDateTime
类型和 Calendar
之间。
java.sql.Date
、java.sql.Time
、java.sql.Timestamp
和 java.util.Date
之间。
当从一个字符串转换时,如果省略 @Mapping
注解的 dateFormat
,会使用默认的格式和符号来转换,但 XmlGregorianCalendar
是个例外,将根据 XML Schema 1.0 Part 2, Section 3.2.7-14.1, Lexical Representation
java.util.Currency
和 String
之间。当从一个字符串转换时,其值应符合 ISO-4217 规范,否则会抛出 IllegalArgumentException
异常。
java.util.UUID
和 String
之间。当从一个字符串转换时,其值应该是一个有效的 UUID,否则会抛出 IllegalArgumentException
异常。
String
和 StringBuilder
之间。
java.net.URL
和 String
之间。当从一个字符串转换时,其值应该是一个有效的 URL,否则会抛出 MalformedURLException
异常。
4.2 映射对象引用
通常,一个对象不仅具有基本数据类型的属性,还引用其他对象。比如,一个 Cage
实体内部含有 Animal
类型的属性,表示笼子里关着的动物。
在这种情况下,也只需为引用的对象类型定义一个映射方法。
具体使用
创建一个新的 Cage
实体:
1 2 3 4 5 6 7 8 9 10 @Setter @Getter public class Cage { private String name; private String size; private Animal animal; }
将映射成的 CageDto
实体:
1 2 3 4 5 6 7 8 9 10 @Getter @Setter public class CageDto { private String name; private String size; private DogDto dog; }
用于实体映射的接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Mapper public interface CageMapper { CageMapper INSTANCE = Mappers.getMapper(CageMapper.class); @Mapping(source = "animal", target = "dog") CageDto toCageDto (Cage cage) ; DogDto toDogDto (Animal animal) ; }
测试方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Test public void testToCageDto () { Cage cage = new Cage (); Animal animal = new Animal ("小黑" , "Dog" ); cage.setAnimal(animal); cage.setName("小黑的家" ); cage.setSize("70*50*60" ); CageDto cageDto = CageMapper.INSTANCE.toCageDto(cage); Assert.assertEquals("小黑的家" , cageDto.getName()); Assert.assertEquals("70*50*60" , cageDto.getSize()); Assert.assertEquals("小黑" , cageDto.getDog().getName()); Assert.assertEquals("Dog" , cageDto.getDog().getType()); }
运行测试方法后,测试通过。
查看生成的实现类 CageMapperImpl
接口实现类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class CageMapperImpl implements CageMapper { @Override public CageDto toCageDto (Cage cage) { if ( cage == null ) { return null ; } CageDto cageDto = new CageDto (); cageDto.setDog( toDogDto( cage.getAnimal() ) ); cageDto.setName( cage.getName() ); cageDto.setSize( cage.getSize() ); return cageDto; } @Override public DogDto toDogDto (Animal animal) { if ( animal == null ) { return null ; } DogDtoBuilder dogDto = DogDto.builder(); dogDto.name( animal.getName() ); dogDto.type( animal.getType() ); return dogDto.build(); } }
MapStruct 对源对象与目标对象属性的映射规则
如果源属性和目标属性类型相同,值将直接从源复制到目标。如果属性是一个集合(如 List
),集合的副本(新建一个集合,然后把源数据一个个地塞进去)将被设置到目标属性中。
如果源属性和目标属性类型不同,将在当前类检查是否存在将源属性的类型作为参数类型,将目标属性的类型作为返回类型的另一种映射方法。如果存在这样的方法,它将在生成的实现方法中被调用。
如果不存在这样的方法,MapStruct 将查看源属性类型和目标属性类型是否存在内置转换。如果存在,生成的代码将采用这种转换。
如果不存在这样的方法,MapStruct 将采用复杂的转换:
映射方法,通过映射方法映射结果,比如:target = method1(method2(source))
内置转换,通过映射方法映射结果,比如:target = method(conversion(source))
映射方法,通过内置转换映射结果,比如:target = conversion(method(source))
如果没有找到这样的方法,MapStruct 将尝试生成一个自动的 sub-mapping
方法,使用方法进行源属性和目标属性之间的映射。
如果 MapStruct 无法创建基于名称的映射方法,则会在编译时引发错误,显示不可映射的属性及其路径。
可以在以下四个注解上设置 mappingControl
(映射控件),优先级依次增加:
@MapperConfig
@Mapper
@BeanMapping
@Mapping
比如 @Mapper(mappingControl = NoComplexMapping.class)
的优先级高于 @MapperConfig(mappingControl = DeepClone.class)
。
@IterableMapping
和 @MapMapping
的工作原理与 @Mapping
类似。
映射控件是在 MapStruct 1.4 中引入的实验性功能。
映射控件的类型由枚举 org.mapstruct.control.MappingControl.Use
表示:
BUILT_IN_CONVERSION
COMPLEX_MAPPING
DIRECT
MAPPING_METHOD
枚举 Use
的存在使得用户可以对 MapStruct 的映射细节进行控制,当缺失某种枚举时,表示关闭对应的映射方式,默认情况下 mappingControl = MappingControl.class
,表示启用所有映射选项。
属性间应用拓展
如果不想让 MapStruct 生成自动的 sub-mapping
方法,可以使用:
1 @Mapper(disableSubMappingMethodsGeneration = true)
用户可以通过元注解来控制映射。提供了一些方便的注解,比如 @DeepClone
只允许直接映射。使用该注解后,如果源类型和目标类型相同,MapStruct 将对源类型进行深拷贝。sub-mappings-methods
必须被允许(默认情况下)。
在自动的 sub-mapping
方法生成过程中,共享配置将不会被考虑在内。
目标对象的构造函数属性也被视为目标属性。
4.3 控制嵌套对象映射
MapStruct 会使用源属性名和目标属性名生成映射方法,但不幸的是,这两个名称通常不会相等。在这这种情况下,需要使用 @Mapping
注解来指定属性之间的映射。
比如:
1 2 3 4 5 6 7 8 9 10 @Mapper public interface FishTankMapper { @Mapping(target = "fish.kind", source = "fish.type") @Mapping(target = "fish.name", ignore = true) @Mapping(target = "ornament", source = "interior.ornament") @Mapping(target = "material.materialType", source = "material") @Mapping(target = "quality.report.organisation.name", source = "quality.report.organisationName") FishTankDto map ( FishTank source ) ; }
再或者:
1 2 3 4 5 6 7 8 9 10 11 @Mapper public interface FishTankMapperWithDocument { @Mapping(target = "fish.kind", source = "fish.type") @Mapping(target = "fish.name", expression = "java(\"Jaws\")") @Mapping(target = "plant", ignore = true ) @Mapping(target = "ornament", ignore = true ) @Mapping(target = "material", ignore = true) @Mapping(target = "quality.document", source = "quality.report") @Mapping(target = "quality.document.organisation.name", constant = "NoIdeaInc" ) FishTankWithNestedDocumentDto map ( FishTank source ) ; }
MapStruct 将对源对象中的每个嵌套属性执行 null
检查。
MapStruct 鼓励用户编写明确的 嵌套属性映射方法 ,而不是通过 @Mapping
配置来解决一切,这样还可以实现代码复用,更加优雅~ 👊
在某些情况下,用于生成嵌套方法的 ReportingPolicy
可能是 IGNORE
,这意味着 MapStruct 不会显示嵌套映射中未映射的目标属性。
4.4 执行自定义映射方法
有时映射并不简单,有些字段可能需要自定义逻辑。
比如将 FishTank
中的 length
、width
和 height
属性映射到 FishTankWithVolumeDto
中的 volume
属性,这个映射也不是一个简单的映射,需要计算鱼缸 FishTank
的体积,然后输出描述信息。
参与转换的类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 @Getter @Setter public class FishTank { private Fish fish; private int length; private int width; private int height; } @Getter @Setter @AllArgsConstructor public class VolumeDto { private int volume; private String description; } @Getter @Setter public class FishTankWithVolumeDto { private FishDto fish; private VolumeDto volume; } @Getter @Setter public class Fish { private String type; } @Getter @Setter public class FishDto { private String kind; private String name; }
用于实体映射的抽象类(换一种方法,不要接口):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Mapper public abstract class FishTankMapperWithVolume { public static FishTankMapperWithVolume INSTANCE = Mappers.getMapper(FishTankMapperWithVolume.class); @Mapping(target = "fish.kind", source = "source.fish.type") @Mapping(target = "volume", source = "source") public abstract FishTankWithVolumeDto map (FishTank source) ; protected VolumeDto mapVolume (FishTank source) { int volume = source.getLength() * source.getWidth() * source.getHeight(); String desc = volume < 100 ? "Small" : "Large" ; return new VolumeDto (volume, desc); } }
@Mapping
注解的 source 属性值是 map()
方法的参数 source
,而不是 FishTank
中名为 source
的属性。
测试方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class FishTankMapperWithVolumeTest { @Test public void testToFishTankWithVolumeDto () { FishTank fishTank = new FishTank (); fishTank.setLength(70 ); fishTank.setWidth(50 ); fishTank.setHeight(60 ); Fish fish = new Fish (); fish.setType("Carp" ); fishTank.setFish(fish); FishTankWithVolumeDto dto = FishTankMapperWithVolume.INSTANCE.map(fishTank); Assert.assertEquals(210000 , dto.getVolume().getVolume()); Assert.assertEquals("Large" , dto.getVolume().getDescription()); Assert.assertEquals("Carp" , dto.getFish().getKind()); } }
运行测试方法后,测试通过。
查看生成的实现类 FishTankMapperWithVolumeImpl
接口实现类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class FishTankMapperWithVolumeImpl extends FishTankMapperWithVolume { @Override public FishTankWithVolumeDto map (FishTank source) { if ( source == null ) { return null ; } FishTankWithVolumeDto fishTankWithVolumeDto = new FishTankWithVolumeDto (); fishTankWithVolumeDto.setFish( fishToFishDto( source.getFish() ) ); fishTankWithVolumeDto.setVolume( mapVolume( source ) ); return fishTankWithVolumeDto; } protected FishDto fishToFishDto (Fish fish) { if ( fish == null ) { return null ; } FishDto fishDto = new FishDto (); fishDto.setKind( fish.getType() ); return fishDto; } }
4.5 调用其他映射器
MapStruct 还可以调用其他类中定义的映射方法,这些映射方法可以是 MapStruct 生成的也可以是手动编写的。这对于在多个类中编写映射代码,或者想提供无法由 MapStruct 生成的自定义映射逻辑,是非常有用的。
调用其他映射器时,只需在 @Mapper#uses
配置想使用的映射器类即可,可以指定多个。
具体使用
假设有一属性 manufacturingDate
在源实体中的类型为 Date
,而在目标实体中类型为 String
,在转换过程中,对日期值按需求进行格式化,这时候就需要使用自己编写的映射器进行映射处理。
源实体:
1 2 3 4 5 6 7 @Getter @Setter @NoArgsConstructor @AllArgsConstructor public class Car { private Date manufacturingDate; }
目标实体:
1 2 3 4 5 6 7 @Getter @Setter @NoArgsConstructor @AllArgsConstructor public class CarDto { private String manufacturingDate; }
自定义的映射方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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
上指定映射方法所在的类。
映射器接口:
1 2 3 4 5 6 7 8 9 10 11 12 @Mapper(uses = DateMapper.class) public interface CarMapper { CarMapper INSTANCE = Mappers.getMapper(CarMapper.class); CarDto carToCarDtoUseDate (Car car) ; }
测试方法:
1 2 3 4 5 6 7 @Test public void testUseDate () { Car car = new Car (new Date (1612800000000L )); CarDto carDto = CarMapper.INSTANCE.carToCarDtoUseDate(car); assertEquals("2021-02-09" , carDto.getManufacturingDate()); }
运行测试方法后,测试通过。
查看生成的实现类 CarMapperImpl
接口实现类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class CarMapperImpl implements CarMapper { private final DateMapper dateMapper = new DateMapper (); public CarMapperImpl () { } public CarDto carToCarDtoUseDate (Car car) { if (car == null ) { return null ; } else { CarDto carDto = new CarDto (); carDto.setManufacturingDate(this .dateMapper .asString(car.getManufacturingDate())); return carDto; } } }
在为实现 carToCarDto()
方法生成代码时,MapStruct 将寻找一个将 Date
对象映射为 String
的方法,最终在 DateMapper
类中找到它,使用其中的 asString()
方法来映射不同类型的 manufacturingDate
属性。
总结思考
生成的映射器使用为其配置的组件模型(使用 @Mapper#componentModel
配置组件模型)获取引用的映射器。
在示例代码生成的映射器实现类中,使用了 new
关键词 调用无参构造方法 创建了一个包含自定义映射方法的对象。
因此,默认情况下, 在自定义映射方法的所在类中,一定要含有公共无参构造方法,以便能够被实例化。
在使用了依赖注入时,自定义映射方法的所在类也要是一个可注入的 Bean。比如使用了 Spring 框架时,需要让自定义映射方法的所在类也被 Spring 管理。
4.6 将目标类型传递给自定义映射器
使用 @Mapper#uses()
将自定义映射器与生成的映射器挂钩时,可以在自定义映射方法中定义 Class
类型(或其超类)的附加参数,以便针对特定的目标对象类型执行常规映射任务。该属性必须使用 @TargetType
进行标注,以便 MapStruct 生成与目标对象中属性对应类型一致的 Class
实例。
例如,CarDto
中有一个 Reference
类型的 owner
属性,它包含了 Person
实体的主键。现在可以创建通用的自定义映射器,将任何 Reference
对象解析为其对应的、由 JPA 管理的实体实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @ApplicationScoped public class ReferenceMapper { @PersistenceContext private EntityManager entityManager; public <T extends BaseEntity > T resolve (Reference reference, @TargetType Class<T> entityClass) { return reference != null ? entityManager.find(entityClass, reference.getPk()) : null ; } public Reference toReference (BaseEntity entity) { return entity != null ? new Reference ( entity.getPk() ) : null ; } } @Mapper(componentModel = "cdi", uses = ReferenceMapper.class ) public interface CarMapper { Car carDtoToCar (CarDto carDto) ; }
生成的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @ApplicationScoped public class CarMapperImpl implements CarMapper { @Inject private ReferenceMapper referenceMapper; @Override public Car carDtoToCar (CarDto carDto) { if ( carDto == null ) { return null ; } Car car = new Car (); car.setOwner( referenceMapper.resolve( carDto.getOwner(), Owner.class ) ); return car; } }
4.7 传递上下文或状态对象给自定义方法
可以通过生成的映射方法将额外上下文或状态信息传递给具有 @Context
参数的自定义方法。
这样的参数会传递给其他映射方法、适用的情况下的 @ObjectFactory
方法或 @BeforeMapping
、@AfterMapping
方法,因此可以在自定义代码中使用。
在 @ObjectFactory
方法中搜索被 @Context
标记的参数,如果适用,将根据提供的上下文参数值调用这些方法。
还会在 @BeforeMapping
、@AfterMapping
方法中搜索被 @Context
标记的参数,如果适用,将根据提供的上下文参数值调用这些方法。
注意:在调用传入上下文参数的映射方法之前,不会进行 空检查 。在这种情况下,调用方需要确保没有传入 null
。
为了在生成的代码中能够调用含有 @Context
参数的自定义方法,正在生成的映射方法的声明中也需要包含这些(或子类)@Context
参数。生成的代码中不会为缺少的 @Context
参数创建新的实例,也不会传递一个空值代替。
具体使用
使用 @Context
参数将数据传递给我们自定义的属性映射方法:
1 2 3 4 5 6 public abstract CarDto toCar (Car car, @Context Locale translationLocale) ;protected OwnerManualDto translateOwnerManual (OwnerManual ownerManual, @Context Locale locale) { }
生成的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 public CarDto toCar (Car car, Locale translationLocale) { if ( car == null ) { return null ; } CarDto carDto = new CarDto (); carDto.setOwnerManual( translateOwnerManual( car.getOwnerManual(), translationLocale ); return carDto; }
4.8 映射方法的解析
将属性从一种类型映射到另一种类型时,MapStruct 寻找将源类型映射到目标类型的最明确的方法。该方法可以是在同一个映射器接口中声明的,也可以是通过 @Mapper#uses()
注册的另一个映射器中声明的。这同样适用于工厂方法。
寻找映射方法或工厂方法的算法与 Java 的方法解析算法很类似,尤其是具有更明确源类型的方法都将被优先获取(假如有两个方法,一个方法映射寻找的源类型,另一个映射相同的父类型)。如果有多个详细程度相当的方法时,则会引发错误。
在使用 JAXB 时,比如将 String
转换为对应的 JAXBElement<String>
时,MapStruct 在寻找映射方法时将考虑 @XmlElementDecl
注解的 scope
和 name
属性值,这将确保创建的 JAXBElement
实例具有正确的 QNAME
值。
4.9 基于限定符的映射方法选择
在很多情况下需要具有相同方法签名(除了名称)但具有不同行为的映射方法。
MapStruct 提供 @Qualifier
(org.mapstruct.Qualifier)注解来处理这种情况。一个“qualifier”(可译为“限定符”)可以是用户自定义的一个注解,这个注解作用在一个能被使用的映射器中的映射方法上,这个映射方法可以在对象属性映射、iterable 映射或 map 映射中被引用。一个方法和映射器上可以有多个限定符。
具体使用
假设有一个手写的映射器 Titles
,其内部有多个参数类型是 String
,返回值类型也是 String
的自定义映射方法:
1 2 3 4 5 6 7 8 9 10 public class Titles { public String translateTitleEG (String title) { } public String translateTitleGE (String title) { } }
在 Mapper 接口中需要使用到前文中手写的映射器:
1 2 3 4 @Mapper(uses = Titles.class) public interface MovieMapper { GermanRelease toGerman (OriginalRelease movies) ; }
但这样是有问题的,因为在手写的映射器中有多个符合条件的映射方法,MapStruct 无法区分。
这个时候需要使用到 @Qualifier
注解。
编写一个注解,这个注解作用于 手写的映射器 上,将 @Qualifier
作为元注解使用:
1 2 3 4 5 6 7 import org.mapstruct.Qualifier;@Qualifier @Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS) public @interface TitleTranslator {}
由于存在多个符合条件的自定义映射方法,这些映射方法也需要使用“限定符”,再编写一些注解,这些注解作用于 自定义映射方法 上:
1 2 3 4 5 6 7 import org.mapstruct.Qualifier;@Qualifier @Target(ElementType.METHOD) @Retention(RetentionPolicy.CLASS) public @interface EnglishToGerman {}
1 2 3 4 5 6 7 import org.mapstruct.Qualifier;@Qualifier @Target(ElementType.METHOD) @Retention(RetentionPolicy.CLASS) public @interface GermanToEnglish {}
然后在手写的映射器及其内部自定义映射方法上使用这些注解:
1 2 3 4 5 6 7 8 9 10 11 12 13 @TitleTranslator public class Titles { @EnglishToGerman public String translateTitleEG (String title) { } @GermanToEnglish public String translateTitleGE (String title) { } }
最终,Mapper 会变成这样:
1 2 3 4 5 6 @Mapper(uses = Titles.class) public interface MovieMapper { @Mapping(target = "title", qualifiedBy = {TitleTranslator.class, EnglishToGerman.class}) GermanRelease toGerman ( OriginalRelease movies ) ; }
注意:
编写的注解的生命周期必须是 @Retention(RetentionPolicy.CLASS)
的,这表示注解被编译器编译进 class 文件,但运行时会忽略。
用限定符标记的类或方法将不再适用于没有使用 qualifiedBy
属性的映射逻辑。
同样的机制也存在于 Bean 映射上,@BeanMapping#qualifiedBy
将选择用指定限定符标记的工厂方法。
使用 @Named
注解选择映射方法
在大多数情况下,声明一个新注解来完成映射方法的选择有些大材小用。对于这些情况,MapStruct 提供了 @Named
的注解,使用这个注解也可以达到 @Qualifier
的效果。👍
@Named
注解是预定义的限定符(它也被 @Qualifier
注解标记),@Named
可用于命名映射器,或者更直接地通过其 value
值命名映射方法:
1 2 3 4 5 6 @Target({ ElementType.TYPE, ElementType.METHOD}) @Retention( RetentionPolicy.CLASS ) @Qualifier public @interface Named { String value () ; }
在手写的映射器及其内部自定义映射方法上使用 @Named
注解:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Named("TitleTranslator") public class Titles { @Named("EnglishToGerman") public String translateTitleEG (String title) { } @Named("GermanToEnglish") public String translateTitleGE (String title) { } }
而 Mapper 接口中不再使用 @Mapping#qualifiedBy
,而是使用 @Mapping#qualifiedByName
:
1 2 3 4 5 @Mapper( uses = Titles.class ) public interface MovieMapper { @Mapping(target = "title", qualifiedByName = {"TitleTranslator", "EnglishToGerman"}) GermanRelease toGerman ( OriginalRelease movies ) ; }
使用 @Named
需要定义一个限定名称并使用,而使用 @Qualifier
则比较麻烦,需要自定义注解。虽然两者都能达到相同的效果,但使用 @Named
时应当更加小心,因为在使用 @Named
注解时,IDE 并不会提示名称是否书写正确,也不会在重构限定符名称时像重构自定义注解的名称那样巧妙地重构代码中所有使用到的地方。
4.10 结合限定符与默认值
请注意,@Mapping#defaultValue
本质上是一个 String
,需要转换为 @Mapping#target
。使用 @Mapping#qualifiedByName
或 @Mapping#qualifiedBy
时将强制 MapStruct 使用指定的方法。如果想要 @Mapping#defaultValue
有不同的行为,请提供一个适当的映射方法。这个方法需要将 String
转换成 @Mapping#target
想要的类型,同时还要被限定符标记,以便 @Mapping#qualifiedByName
或 @Mapping#qualifiedBy
能够找到。
1 2 3 4 5 6 7 8 9 10 11 12 13 @Mapper public interface MovieMapper { @Mapping(target = "category", qualifiedByName = "CategoryToString", defaultValue = "DEFAULT") GermanRelease toGerman (OriginalRelease movies) ; @Named("CategoryToString") default String defaultValueForQualifier (Category cat) { } }
上述示例 cat
来源中的 category
如果是 null
,CategoryToString( Enum.valueOf( Category.class, "DEFAULT" ) )
方法将被调用,其返回结果被设置给目标的 category
字段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Mapper public interface MovieMapper { @Mapping(target = "category", qualifiedByName = "CategoryToString", defaultValue = "Unknown") GermanRelease toGerman (OriginalRelease movies) ; @Named("CategoryToString") default String defaultValueForQualifier (Category cat) { } @Named("CategoryToString") default String defaultValueForQualifier (String value) { return value; } }
上述示例 cat
来源中的 category
如果是 null
,defaultValueForQualifier( "Unknown" )
方法将被调用(参数为 String
的方法),其返回结果被设置给目标的 category
字段。
如果上面提到的方法不起作用,可以使用 defaultExpression
来设置默认值。
1 2 3 4 5 6 7 8 9 @Mapper public interface MovieMapper { @Mapping(target = "category", qualifiedByName = "CategoryToString", defaultExpression = "java(\"Unknown\")") GermanRelease toGerman ( OriginalRelease movies ) ; }
5. 映射集合
5.1 映射 Set 与 List
MapStruct 也支持 Java 中各种集合类型(比如 Set、List)的映射,包括 Java Collection Framework 中各种 可迭代 类型,其使用与映射对象类型一样。🐮
生成的代码中将包含一个循环,在循环中遍历源集合,转换每个元素并将它们放入目标集合中。
如果在给定的映射器或它使用的映射器中找到集合中元素类型的映射方法,则调用此方法执行元素转换。再或者,如果存在源元素类型和目标元素类型的隐式转换,则调用那个隐式转换。
具体使用
1 2 3 4 5 6 7 8 9 @Mapper public interface CarMapper { Set<String> integerSetToStringSet (Set<Integer> integers) ; List<CarDto> carsToCarDtos (List<Car> cars) ; CarDto carToCarDto (Car car) ; }
重写的 integerSetToStringSet()
方法为每个元素执行从 Integer
到 String
的转换,而生成的 carsToCarDtos()
方法为每个包含的元素调用 carToCarDto()
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @Override public Set<String> integerSetToStringSet (Set<Integer> integers) { if ( integers == null ) { return null ; } Set<String> set = new HashSet <String>(); for ( Integer integer : integers ) { set.add( String.valueOf( integer ) ); } return set; } @Override public List<CarDto> carsToCarDtos (List<Car> cars) { if ( cars == null ) { return null ; } List<CarDto> list = new ArrayList <CarDto>(); for ( Car car : cars ) { list.add( carToCarDto( car ) ); } return list; }
当映射集合类型的属性时,MapStruct 将查找具有匹配的参数和返回类型的集合映射方法。
例如从 Car#passengers
(List<Person>
类型)到 CarDto#passengers
(List<PersonDto>
类型):
1 2 3 carDto.setPassengers( personsToPersonDtos( car.getPassengers() ) ); ...
一些框架和库只暴露了 JavaBeans 的 Getter,而不暴露集合类型属性的 Setter。使用 JAXB 从 XML 生成的类型默认遵循这种模式。在这种情况下,用于映射这种属性的生成代码将调用其 Getter 并添加所有映射元素:
1 2 3 carDto.getPassengers().addAll( personsToPersonDtos( car.getPassengers() ) ); ...
注意: 不允许使用可迭代的源和不可迭代的目标来声明映射方法(或者相反),否则会报错。
5.2 映射 Map
MapStruct 也支持映射 Map。比如:
1 2 3 4 5 public interface SourceTargetMapper { @MapMapping(valueDateFormat = "dd.MM.yyyy") Map<String, String> longDateMapToStringStringMap (Map<Long, Date> source) ; }
与 【5.1 映射 Set 与 List】类似,在生成的代码将遍历源 Map,转换每个 key 和 value(通过隐式转换或调用另一个映射方法),并将它们放入目标 Map 中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Override public Map<Long, Date> stringStringMapToLongDateMap (Map<String, String> source) { if (source == null ) { return null ; } Map<Long, Date> map = new HashMap <Long, Date>(); for (Map.Entry<String, String> entry : source.entrySet()) { Long key = Long.parseLong(entry.getKey()); Date value; try { value = new SimpleDateFormat ("dd.MM.yyyy" ).parse( entry.getValue()); } catch (ParseException e) { throw new RuntimeException (e); } map.put(key, value); } return map; }
5.3 集合映射策略
参考博文:MapStruct文档(五)——集合映射
在 @Mapper
注解中有一个属性 CollectionMappingStrategy
,表示集合映射策略,这个属性是个枚举类型,它有四个枚举值:
ACCESSOR_ONLY
SETTER_PREFERRED
ADDER_PREFERRED
TARGET_IMMUTABLE
目标对象中的 set、get 和 add 等方法的存在在不同场景下有不同的情况。下表介绍了目标对象在不同的集合映射策略下对 set、get 和 add 方法的应用:
值
只有 set 方法
只有 add 方法
set、add 方法都有
set、add 方法都没有
已经存在的集合对象
ACCESSOR_ONLY
直接 set 集合
先 get 获取集合再a ddAll
直接 set 集合
先 get 获取集合再 addAll
先 get 获取集合再 addAll
SETTER_PREFERRED
直接 set 集合
add 集合的一项
直接 set 集合
先 get 获取集合再 addAll
先 get 获取集合再 addAll
ADDER_PREFERRED
直接 set 集合
add 集合的一项
add 集合的一项
先 get 获取集合再 addAll
先 get 获取集合再 addAll
TARGET_IMMUTABLE
直接 set 集合
异常
直接 set 集合
异常
直接 set 集合
在生成 JPA 实体的情况下,通常使用 adder
方法将单个元素(实体)添加到集合中。在父级(执行 adder
方法的实体)与子级(集合中的元素,这些元素也是实体)之间通过调用 adder
方法来建立父子关系。为了找到合适的 adder
,MapStruct 将尝试在集合的泛型参数类型和候选的 adder
的单个参数之间进行匹配。当有更多的候选项时,复数形式的 Setter/Getter 名称将转换为单数,并用于匹配。
不应该显式地使用 DEFAULT
选项。它用于区分用户希望显式地重写 @MapperConfig
中的默认值与 @Mapper
中 MapStruct 的隐式选择。DEFAULT
选项与 ACCESSOR_ONLY
同义。
当使用 adder
方法和 JPA 实体时,MapStruct 假设目标集合是使用该集合类型对应的某种实现(比如 List 与 ArrayList)来初始化的。可以使用工厂创建带有已被初始化的集合的新目标实体,而不是 MapStruct 通过构造方法来创建目标实体。
5.4 集合映射的实现类
当迭代器映射方法或 Map 映射方法使用一个接口类型作为返回类型时,它的一个实现类型将在生成的代码中被实例化。具体接口类型与对应的实现类型如下表:
Interface type
Implementation type
Iterable
ArrayList
Collection
ArrayList
List
ArrayList
Set
LinkedHashSet
SortedSet
TreeSet
NavigableSet
TreeSet
Map
LinkedHashMap
SortedMap
TreeMap
NavigableMap
TreeMap
ConcurrentMap
ConcurrentHashMap
ConcurrentNavigableMap
ConcurrentSkipListMap
6. 映射 Stream
java.util.Stream
的映射和集合的映射是类似的,即通过在映射器接口中定义具有所需源和目标类型的映射方法。
生成的代码将包含从所提供的迭代器或数组来创建一个 Stream
,或者将所提供的 Stream
收集到一个迭代器或数组中。如果存在源元素类型和目标元素类型的映射方法或隐式转换,那么这种转换将在 Stream#map()
中完成。
具体使用
1 2 3 4 5 6 7 8 9 @Mapper public interface CarMapper { Set<String> integerStreamToStringSet (Stream<Integer> integers) ; List<CarDto> carsToCarDtos (Stream<Car> cars) ; CarDto carToCarDto (Car car) ; }
生成的 integerStreamToStringSet()
方法的实现为每个元素执行从 Integer 到 String 的转换,而生成的 carsToCarDtos()
方法的实现为每个包含的元素执行 carToCarDto()
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Override public Set<String> integerStreamToStringSet (Stream<Integer> integers) { if (integers == null ) { return null ; } return integers.map(integer -> String.valueOf(integer)) .collect(Collectors.toCollection(HashSet<String>::new )); } @Override public List<CarDto> carsToCarDtos (Stream<Car> cars) { if (cars == null ) { return null ; } return cars.map(car -> carToCarDto(car)) .collect(Collectors.toCollection(ArrayList<CarDto>::new )); }
注意: 如果进行了从 Stream
到迭代器或数组的映射,那么所传递的 Stream
将被消费,并且不能再消费它。因为 Java 中的 Stream
只允许被消费(使用)一次。
在进行 Stream 到迭代器的映射时,迭代器的具体实现类型与【5.4 集合映射的实现类】中一致。
7. 映射值
7.1 映射枚举
MapStruct 支持将一种 Java 枚举类型映射到另一种 Java 枚举类型的方法的生成。
默认情况下,源枚举中的每个枚举项都会被映射到目标枚举类型中同名的枚举项。
如果需要,可以使用 @ValueMapping
注解(而非 @Mapping
)将源枚举中的某个枚举项映射到另一个名称的枚举项。源枚举中的多个枚举项可以映射到目标枚举中的同一枚举项。
具体使用
1 2 3 4 5 6 7 8 9 10 11 12 @Mapper public interface OrderMapper { OrderMapper INSTANCE = Mappers.getMapper( OrderMapper.class ); @ValueMappings({ @ValueMapping(source = "EXTRA", target = "SPECIAL"), @ValueMapping(source = "STANDARD", target = "DEFAULT"), @ValueMapping(source = "NORMAL", target = "DEFAULT") }) ExternalOrderType orderTypeToExternalOrderType (OrderType orderType) ; }
生成的实现类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class OrderMapperImpl implements OrderMapper { @Override public ExternalOrderType orderTypeToExternalOrderType (OrderType orderType) { if (orderType == null ) { return null ; } ExternalOrderType externalOrderType_; switch (orderType) { case EXTRA: externalOrderType_ = ExternalOrderType.SPECIAL; break ; case STANDARD: externalOrderType_ = ExternalOrderType.DEFAULT; break ; case NORMAL: externalOrderType_ = ExternalOrderType.DEFAULT; break ; case RETAIL: externalOrderType_ = ExternalOrderType.RETAIL; break ; case B2B: externalOrderType_ = ExternalOrderType.B2B; break ; default : throw new IllegalArgumentException ( "Unexpected enum constant: " + orderType ); } return externalOrderType_; } }
默认情况下,如果源枚举类型的枚举项在目标枚举中没有同名的枚举项,并且也未通过 @ValueMapping
映射到其他枚举值,那么 MapStruct 将产生错误。这确保了所有枚举项都以一种安全且可预测的方式进行映射。如果由于某种原因出现了无法识别的源枚举项,生成的映射方法将抛出 IllegalStateException。
MapStruct 还具有将任何剩余(未指定的)的映射映射到默认映射的机制。这只能在同一组值映射中使用一次,并且仅适用于源。它有两种策略:<ANY_REMAINING>
和 <ANY_UNMAPPED>
。它们不能同时使用。 比如:
1 2 @ValueMapping( source = MappingConstants.ANY_REMAINING, target = "SPECIAL" ) ExternalOrderType orderTypeToExternalOrderType (OrderType orderType) ;
<ANY_REMAINING>
:只能用在 source
上。表示源枚举中除了同名自动映射和显式指定的映射外,其余所有枚举项都映射到指定的目标枚举项上(相当于 switch
中的 default
)。
<ANY_UNMAPPED>
:只能用在 source
上,不能和 <ANY_REMAINING>
同时使用,会将源枚举中除 显式 指定的映射外(此时同名自动映射将失效)的其他剩余枚举项映射到目标枚举项上。也就是说,没有经过 @ValueMapping
注解 显式 指定的枚举项都会映射到目标枚举项上。
MapStruct 可以通过 <NULL>
处理来源的 null
值和目标的 null
值。设置在 source 上表示源枚举是 null
时,被映射到指定的目标枚举;设置在 target 上表示给定的来源枚举项映射到 null
。
这三个值在 MappingConstants
类中以常量的形式存在:
1 2 3 4 5 6 7 8 9 10 11 12 13 public final class MappingConstants { private MappingConstants () { } public static final String NULL = "<NULL>" ; public static final String ANY_REMAINING = "<ANY_REMAINING>" ; public static final String ANY_UNMAPPED = "<ANY_UNMAPPED>" ; }
@InheritInverseConfiguration
和 @InheritConfiguration
可以与 @ValueMappings
组合使用。这种情况下,<ANY_REMAINING>
和 <ANY_UNMAPPED>
会被忽略。
具体使用
1 2 3 4 5 6 7 8 9 10 11 12 @Mapper public interface SpecialOrderMapper { SpecialOrderMapper INSTANCE = Mappers.getMapper(SpecialOrderMapper.class); @ValueMappings({ @ValueMapping(source = MappingConstants.NULL, target = "DEFAULT"), @ValueMapping(source = "STANDARD", target = MappingConstants.NULL), @ValueMapping(source = MappingConstants.ANY_REMAINING, target = "SPECIAL") }) ExternalOrderType orderTypeToExternalOrderType (OrderType orderType) ; }
生成的实现类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class SpecialOrderMapperImpl implements SpecialOrderMapper { @Override public ExternalOrderType orderTypeToExternalOrderType (OrderType orderType) { if ( orderType == null ) { return ExternalOrderType.DEFAULT; } ExternalOrderType externalOrderType_; switch ( orderType ) { case STANDARD: externalOrderType_ = null ; break ; case RETAIL: externalOrderType_ = ExternalOrderType.RETAIL; break ; case B2B: externalOrderType_ = ExternalOrderType.B2B; break ; default : externalOrderType_ = ExternalOrderType.SPECIAL; } return externalOrderType_; } }
使用 <ANY_UNMAPPED>
时,MapStruct 将不会自动映射 RETAIL
和 B2B
。
不建议 使用 @Mapping
注解将枚举映射到枚举。使用 @Mapping
注解来映射枚举的方式将在未来的 MapStruct 版本中删除。
7.2 枚举与字符串的相互映射
MapStruct 支持枚举与字符串间的相互映射,这与枚举与枚举之间的映射很类似,但也存在着部分差异。
枚举映射到字符串
相同:在未显式定义映射方式的情况下,每个源枚举项被映射为具有相同名称的 String
值。
相同:<ANY_UNMAPPED>
在处理定义的映射后停止,并继续执行 switch 中 default 子句值。
差异:不支持 <ANY_REMAINING>
,使用这个值将导致错误。它的作用前提是源枚举项和目标枚举项之间存在名称相似性,而这对于 String
类型没有意义。
差异:鉴于 1.
和 3.
因此永远不会有未映射的值。
字符串映射到枚举
相同:在未显式定义映射方式的情况下,给定的 String
将被映射到与目标枚举项名称匹配的值。
相同:<ANY_UNMAPPED>
在处理定义的映射后停止,并继续执行 switch 中 default 子句值。
相同:<ANY_REMAINING>
将为每个目标枚举项创建一个映射,然后继续执行 switch 中 default 子句值。
差异:需要提供 switch 中 default 子句才能得出确定的结果(枚举值集有限,String
选项不限)。不指定 <ANY_REMAINING>
或 <ANY_UNMAPPED>
将出现警告。
具体使用
1 2 3 4 5 6 7 8 9 public enum ExternalOrderType { DEFAULT, RETAIL, B2B, SPECIAL, }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Mapper public interface EnumStringMapper { EnumStringMapper INSTANCE = Mappers.getMapper(EnumStringMapper.class); @ValueMappings({ @ValueMapping(source = MappingConstants.NULL, target = "DEFAULT"), @ValueMapping(source = "RETAIL", target = MappingConstants.NULL), @ValueMapping(source = MappingConstants.ANY_UNMAPPED, target = "SPECIAL") }) String enumToString (ExternalOrderType type) ; @ValueMappings({ @ValueMapping(source = MappingConstants.NULL, target = "DEFAULT"), @ValueMapping(source = "STANDARD", target = MappingConstants.NULL), @ValueMapping(source = MappingConstants.ANY_UNMAPPED, target = "SPECIAL") }) ExternalOrderType stringToEnumWithUnmapped (String str) ; @ValueMappings({ @ValueMapping(source = MappingConstants.NULL, target = "DEFAULT"), @ValueMapping(source = "STANDARD", target = MappingConstants.NULL), @ValueMapping(source = MappingConstants.ANY_REMAINING, target = "SPECIAL") }) ExternalOrderType stringToEnumWithRemaining (String str) ; }
生成的实现类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 public class EnumStringMapperImpl implements EnumStringMapper { @Override public String enumToString (ExternalOrderType type) { if ( type == null ) { return "DEFAULT" ; } String string; switch ( type ) { case RETAIL: string = null ; break ; default : string = "SPECIAL" ; } return string; } @Override public ExternalOrderType stringToEnumWithUnmapped (String str) { if ( str == null ) { return ExternalOrderType.DEFAULT; } ExternalOrderType externalOrderType; switch ( str ) { case "STANDARD" : externalOrderType = null ; break ; default : externalOrderType = ExternalOrderType.SPECIAL; } return externalOrderType; } @Override public ExternalOrderType stringToEnumWithRemaining (String str) { if ( str == null ) { return ExternalOrderType.DEFAULT; } ExternalOrderType externalOrderType; switch ( str ) { case "STANDARD" : externalOrderType = null ; break ; case "DEFAULT" : externalOrderType = ExternalOrderType.DEFAULT; break ; case "RETAIL" : externalOrderType = ExternalOrderType.RETAIL; break ; case "B2B" : externalOrderType = ExternalOrderType.B2B; break ; case "SPECIAL" : externalOrderType = ExternalOrderType.SPECIAL; break ; default : externalOrderType = ExternalOrderType.SPECIAL; } return externalOrderType; } }
7.3 自定义名称转换
在未使用 @ValueMapping
的情况下,源枚举中的每个枚举项将 默认 映射到目标枚举中具有相同名称的枚举项。
但在某些情况下,源枚举需要在进行映射之前进行转换。比如目标枚举相比源枚举增加了统一的后缀呢,这时候将无法进行同名自动映射。
使用 @ValueMapping
依次指定映射规则固然可以,但相对繁琐。MapStruct 提供了一个名为 @EnumMapping
的注解来解决这个问题。
具体使用
假设有如下两个枚举:
1 2 3 4 5 6 7 8 9 public enum CheeseType { BRIE, ROQUEFORT } public enum CheeseTypeSuffixed { BRIE_TYPE, ROQUEFORT_TYPE }
可以这样定义 Mapper 接口实现两个枚举类之间的转换:
1 2 3 4 5 6 7 8 9 10 11 @Mapper public interface CheeseMapper { CheeseMapper INSTANCE = Mappers.getMapper( CheeseMapper.class ); @EnumMapping(nameTransformationStrategy = "suffix", configuration = "_TYPE") CheeseTypeSuffixed map (CheeseType cheese) ; @InheritInverseConfiguration CheeseType map (CheeseTypeSuffix cheese) ; }
最终生成的实现类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public class CheeseSuffixMapperImpl implements CheeseSuffixMapper { @Override public CheeseTypeSuffixed map (CheeseType cheese) { if (cheese == null ) { return null ; } CheeseTypeSuffixed cheeseTypeSuffixed; switch (cheese) { case BRIE: cheeseTypeSuffixed = CheeseTypeSuffixed.BRIE_TYPE; break ; case ROQUEFORT: cheeseTypeSuffixed = CheeseTypeSuffixed.ROQUEFORT_TYPE; break ; default : throw new IllegalArgumentException ("Unexpected enum constant: " + cheese); } return cheeseTypeSuffixed; } @Override public CheeseType map (CheeseTypeSuffixed cheese) { if (cheese == null ) { return null ; } CheeseType cheeseType; switch (cheese) { case BRIE_TYPE: cheeseType = CheeseType.BRIE; break ; case ROQUEFORT_TYPE: cheeseType = CheeseType.ROQUEFORT; break ; default : throw new IllegalArgumentException ("Unexpected enum constant: " + cheese); } return cheeseType; } }
MapStruct 提供了以下四种开箱即用的枚举名称转换策略:
名称
含义
suffix
为源枚举添加后缀
stripSuffix
从源枚举中删除后缀
prefix
为源枚举添加前缀
stripPrefix
从源枚举中删除前缀
在 MapStruct 1.5.0.Final 中新增转换策略 case
,这种策略将为源枚举进行大小写转换,支持的方式有:
upper
:将源枚举转换为大写
lower
:将源枚举转换为大写
capital
:将源枚举中每个单词的首字母大写,其余字母小写,每个单词之间以下划线分割
也可以注册自定义策略,这会在后文中讲到。
8. Spring 拓展
官方文档:MapStruct Spring Extensions 1.0.0 Reference Guide
MapStruct Spring Extensions 是一个 Java 注释处理器,它扩展了著名的 MapStruct 项目并带有 Spring 框架特性。
在 Spring 有一个名为 Converter
,重写该接口的 convert()
方法后,可以将某个对象转换成另一个对象,这与 MapStruct 的功能不谋而合。使用 MapStruct Spring Extensions 后,在编译期间将生成一个适配器,该适配器允许标准 MapStruct 映射器使用 Spring 的 ConversionService
。
这使开发人员能够在其 uses
属性中仅使用 ConversionService
来定义 MapStruct 映射器,而不必单独导入每个 Mapper,从而使得各个 Mapper 之间解耦。
注意: MapStruct Spring Extensions 至少要在 MapStruct 1.4.0.Final 的版本上运行!
8.1 所选依赖信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 <properties > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > <org.mapstruct.version > 1.4.2.Final</org.mapstruct.version > <org.mapstruct.extensions.spring.version > 0.1.1</org.mapstruct.extensions.spring.version > <maven.compiler.source > 1.8</maven.compiler.source > <maven.compiler.target > 1.8</maven.compiler.target > <lomnok.version > 1.18.12</lomnok.version > </properties > <dependencies > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-context</artifactId > <version > 5.3.25</version > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-test</artifactId > <version > 5.3.25</version > <scope > test</scope > </dependency > <dependency > <groupId > org.mapstruct</groupId > <artifactId > mapstruct</artifactId > <version > ${org.mapstruct.version}</version > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > ${lomnok.version}</version > <scope > provided</scope > </dependency > <dependency > <groupId > junit</groupId > <artifactId > junit</artifactId > <version > 4.13.2</version > <scope > test</scope > </dependency > <dependency > <groupId > org.mapstruct.extensions.spring</groupId > <artifactId > mapstruct-spring-annotations</artifactId > <version > ${org.mapstruct.extensions.spring.version}</version > </dependency > </dependencies > <build > <plugins > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-compiler-plugin</artifactId > <version > 3.8.1</version > <configuration > <source > 1.8</source > <target > 1.8</target > <annotationProcessorPaths > <path > <groupId > org.mapstruct</groupId > <artifactId > mapstruct-processor</artifactId > <version > ${org.mapstruct.version}</version > </path > <path > <groupId > org.mapstruct.extensions.spring</groupId > <artifactId > mapstruct-spring-extensions</artifactId > <version > ${org.mapstruct.extensions.spring.version}</version > </path > <path > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > ${lomnok.version}</version > </path > </annotationProcessorPaths > </configuration > </plugin > </plugins > </build >
8.2 映射器作为转换器
新建 MapStruct 映射器接口,继承 Spring 的 Converter
接口:
1 2 3 4 5 6 7 8 9 10 11 12 package indi.mofan.converter;import org.springframework.core.convert.converter.Converter;@Mapper(componentModel = "spring") public interface CarConverter extends Converter <Car, CarDto> { @Mappings({ @Mapping(target = "seatCount", source = "numberOfSeats"), @Mapping(target = "manufacturingDate", source = "manufacturingDate", dateFormat = "yyyy-MM-dd") }) CarDto convert (Car car) ; }
为这个映射器编写一一段测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @RunWith(SpringRunner.class) @ContextConfiguration(classes = SpringConfig.class) public class SpringExtensionsTest { @Autowired private ConversionService conversionService; @Test public void testSimpleUse () { Date manufacturingDate = new Date (); Car car = new Car ("Morris" , 5 , "Sedan" , manufacturingDate); CarDto carDto = conversionService.convert(car, CarDto.class); Assert.assertNotNull(carDto); Assert.assertEquals("Morris" , carDto.getMake()); Assert.assertEquals(5 , carDto.getSeatCount()); Assert.assertEquals("Sedan" , carDto.getType()); SimpleDateFormat format = new SimpleDateFormat ("yyyy-MM-dd" ); String date = format.format(manufacturingDate); Assert.assertEquals(date, carDto.getManufacturingDate()); } }
运行测试代码后,测试并没有通过,提示 Spring 容器中不存在 ConversionService
的 Bean。
尝试在 Spring 配置类 SpringConfig
中添加 ConversionService
类型的 Bean 后,再次运行,依旧测试失败,提示不存在目标转换器。
因此还需要将定义的转换器注入到 ConversionService
类型的 Bean 中,最终 Spring 配置类中的信息如下:
1 2 3 4 5 6 7 8 9 10 11 @Configuration @ComponentScan({"indi.mofan.mapper", "indi.mofan.converter"}) public class SpringConfig { @Bean public <S, T> DefaultConversionService conversionService (List<Converter<S, T>> converters) { DefaultConversionService service = new DefaultConversionService (); converters.forEach(service::addConverter); return service; } }
其中 @ComponentScan
注解扫描的 indi.mofan.converter
包为转换器所在的包。
最后再次运行测试方法后,测试通过。
8.3 ConversionServiceAdapter
使用 MapStruct Spring Extensions 后,会生成一个包含所有转换方法的适配器类 —— ConversionServiceAdapter
。
当一个 Mapper 调用另一个 Mapper 时,需要在调用 Mapper 的 @Mapper#uses
属性中指定被调用的 Mapper 信息,这会造成两个 Mapper 之间的耦合。
利用生成的适配器类,现在只需要在 @Mapper#uses
指定 ConversionServiceAdapter
即可,而不是具体的 Mapper。
代码示例
被调用的 Mapper:
1 2 3 4 5 @Mapper(componentModel = "spring") public interface AnimalConverter extends Converter <Animal, DogDto> { @Override DogDto convert (Animal source) ; }
调用的 Mapper:
1 2 3 4 5 6 7 8 9 10 11 12 import org.mapstruct.extensions.spring.converter.ConversionServiceAdapter; @Mapper(componentModel = "spring", uses = ConversionServiceAdapter.class) public interface CageConverter extends Converter <Cage, CageDto> { @Override @Mapping(source = "animal", target = "dog") CageDto convert (Cage source) ; }
默认生成的 ConversionServiceAdapter
在 org.mapstruct.extensions.spring.converter
包下,在未编译前,这些代码可能报错,不用担心,大胆运行!
还要在 Spring 的配置类中让 Spring 扫描 ConversionServiceAdapter
所在的包:
1 2 3 4 5 @Configuration @ComponentScan({"indi.mofan.mapper", "indi.mofan.converter", "org.mapstruct.extensions.spring.converter"}) public class SpringConfig { }
测试方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public void testUseAdapter () { Cage cage = new Cage (); Animal animal = new Animal ("小黑" , "Dog" ); cage.setAnimal(animal); cage.setName("小黑的家" ); cage.setSize("70*50*60" ); CageDto cageDto = conversionService.convert(cage, CageDto.class); Assert.assertNotNull(cageDto); Assert.assertEquals("小黑的家" , cageDto.getName()); Assert.assertEquals("70*50*60" , cageDto.getSize()); Assert.assertEquals("小黑" , cageDto.getDog().getName()); Assert.assertEquals("Dog" , cageDto.getDog().getType()); }
运行测试方法后,测试通过!
8.4 自定义名称
默认情况下,生成的适配器名为 ConversionServiceAdapter
,在 org.mapstruct.extensions.spring.converter
包下。
如果想要自定义名称和包名,可以使用 @SpringMapperConfig
注解:
1 2 3 4 5 6 @SpringMapperConfig( conversionServiceAdapterPackage = "indi.mofan.adapter", conversionServiceAdapterClassName = "MyAdapter" ) public interface MapperSpringConfig {}
指定生成的适配器名为 MyAdapter
,所在包路径为 indi.mofan.adapter
。这样 Spring 在扫描包时就不用再扫描 org.mapstruct.extensions.spring.converter
,直接扫描 indi.mofan
即可:
1 2 3 4 5 @Configuration @ComponentScan("indi.mofan") public class SpringConfig { }
使用 @SpringMapperConfig
注解如果没有特别指定 conversionServiceAdapterPackage
,那么适配器类将生成在 @SpringMapperConfig
所标记的类的相同路径下。
8.5 指定 ConversionService Bean 的名称
当 Spring 容器中存在多个 ConversionService
类型的 Bean 时,需要指定 MapStruct Spring Extensions 所用的 ConversionService
类型的 Bean 的名称。
可以使用 @SpringMapperConfig
注解的 conversionServiceBeanName
属性进行指定。
1 2 3 4 5 6 7 @SpringMapperConfig( conversionServiceAdapterPackage = "indi.mofan.adapter", conversionServiceAdapterClassName = "MyAdapter", conversionServiceBeanName = "myConversionService" ) public interface MapperSpringConfig {}
Spring 配置类中注入 ConversionService
类型的 Bean 的方法名发生变化:
1 2 3 4 @Bean public <S, T> DefaultConversionService myConversionService (List<Converter<S, T>> converters) { }
@Bean
注解通过所标记的方法名称来指定 Bean 的名称。
而在依赖注入 ConversionService
的地方,需要使用 @Qualifier
注解指定 Bean 的名称,比如:
1 2 3 @Autowired @Qualifier("myConversionService") private ConversionService conversionService;
8.6 外置转换
Spring 提供了许多的内置转换,比如将 String
类型转换成 Locale
类型,将 Object
类型转换成 Optional
类型。为了使 MapStruct 在运行过程中也能使用这些转换器,可以在 @SpringMapperConfig
注解的 externalConversions
属性中指定使用的外置转换。
在与 Spring 的整合过程中,映射器的 @Mapper
注解都要指定 componentModel
为 spring
,如果还有调用 Mapper 的情况,还需要指定 uses
为生成的适配器类型。当配置更多时,重复的 @Mapper
看着十分恶心,此时可以使用 @MapperConfig
注解来共享配置,而后在 @Mapper
注解中指定 config
属性值为 @MapperConfig
标记的类即可。
1 2 3 4 5 6 7 8 9 10 11 12 @MapperConfig(componentModel = "spring", uses = MyAdapter.class) @SpringMapperConfig( conversionServiceAdapterPackage = "indi.mofan.adapter", conversionServiceAdapterClassName = "MyAdapter", conversionServiceBeanName = "myConversionService", externalConversions = {@ExternalConversion( sourceType = CarDto.class, targetType = Optional.class )} ) public interface MapperSpringConfig {}
新定义的转换器:
1 2 3 4 5 6 @Mapper(config = MapperSpringConfig.class) public interface OptionalConverter extends Converter <NestedDto, OptionalDto> { @Override @Mapping(source = "carDto", target = "optional") OptionalDto convert (NestedDto source) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test public void testExternalConversions () { CarDto carDto = new CarDto (); carDto.setMake("Morris" ); NestedDto nestedDto = new NestedDto (); nestedDto.setCarDto(carDto); OptionalDto convert = conversionService.convert(nestedDto, OptionalDto.class); Assert.assertNotNull(convert); Optional<CarDto> optional = convert.getOptional(); Assert.assertNotNull(optional); Assert.assertTrue(optional.isPresent()); Assert.assertEquals("Morris" , optional.map(CarDto::getMake).orElse("" )); Assert.assertNull(optional.map(CarDto::getType).orElse(null )); }
8.7 转换 List
完成了单个对象的转换,但对于是列表的来源数据,除了可以使用循环、Stream 等操作外,可以编写一个工具类完成转换:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 @Component public class SpringExtensionHelper implements ApplicationContextAware { private static ApplicationContext context; public static final String CONVERSION_SERVICE_BEAN_NAME = "myConversionService" ; private static ConversionService getConversionService () { return context.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class); } public static <T> T convert (Object source, Class<T> targetType) { return getConversionService().convert(source, targetType); } public static Object convert (Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { return getConversionService().convert(source, sourceType, targetType); } @SuppressWarnings("unchecked") public static <T, R> List<R> convertList (List<T> source, Class<R> targetClass) { if (CollectionUtils.isEmpty(source)) { return Collections.emptyList(); } return (List<R>) convert( source, TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(source.get(0 ).getClass())), TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(targetClass)) ); } @Override public void setApplicationContext (ApplicationContext applicationContext) throws BeansException { SpringExtensionHelper.context = applicationContext; } }
1 2 3 4 5 6 7 8 9 10 11 @Test public void testConvertList () { Car car = new Car (); car.setMake("Morris" ); List<CarDto> carDtoList = SpringExtensionHelper.convertList(Collections.singletonList(car), CarDto.class); Assert.assertEquals(1 , carDtoList.size()); CarDto carDto = carDtoList.get(0 ); Assert.assertNotNull(carDto); Assert.assertEquals("Morris" , carDto.getMake()); }
9. 结语
博主精力有限(好吧,就是懒狗 🐶),暂时就翻译这么多,以上内容足以满足日常所需,更高级的特性待使用到再进行补充。
待完善的内容:
对象工厂
高级映射选项
重用映射配置
自定义映射
使用 MapStruct SPI