Mapstruct 使用教程(一)
第一章 Mapstruct 学习入门之基本使用
文章目录
前言
MapStruct是一个Java注释处理器,用于生成类型安全的bean映射类。开发者要做的就是定义一个映射器接口,该接口声明必需的映射方法。在编译期间,MapStruct将生成此接口的实现。此实现使用简单的Java方法调用在源对象和目标对象之间进行映射,没有反射或类似内容。与手动编写映射代码相比,MapStruct通过自动生成繁琐且易于出错的代码来节省开发者时间。遵循配置方法上的约定,MapStruct使用合理的默认值,但在配置或实现特殊行为时可以使用开发者自定义的配置。
与动态映射框架相比,MapStruct具有以下优点:
- 通过使用普通方法调用(settter/getter)而不是反射来快速执行
- 编译时类型安全性:只能映射相互映射的对象和属性,不能将order实体意外映射到customer DTO等。
- 如果有如下问题,编译时会抛出异常
3.1 映射不完整(并非所有目标属性都被映射)
3.2 映射不正确(找不到正确的映射方法或类型转换) 可以通过freemarker定制化开发
官方文档详见:https://mapstruct.org/documentation/stable/reference/html/#implicit-type-conversions
1. 设置
MapStruct是基于JSR 269的Java注释处理器,因此可以在命令行构建(javac,Ant,Maven等)以及您的IDE中使用。
它包含以下工件:
- org.mapstruct:mapstruct:包含必需的注释,例如@Mapping
- org.mapstruct:mapstruct-processor:包含注释处理器,该注释处理器生成映射器实现
1.1 Maven
对于基于Maven的项目,将以下内容添加到您的POM文件中以使用MapStruct:
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.4.Final</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</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>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
Lombok依赖:(版本最好在1.16.16以上,否则会出现问题)通常是和lombok一起使用的(非必须!)
<dependency>
<groupid>org.projectlombok</groupid>
<artifactid>lombok</artifactid>
<version>${lombok.version}</version>
// 版本号 1.18.12
</dependency>
下载插件(不是必须的,但是挺好用)
idea中下载 mapstruct support 插件,安装重启Idea:
在参数上,按 ctrl + 鼠标左键 ,能够自动进入参数所在类文件
2. 定义一个映射器
2.1 基本映射
要创建映射器,只需使用所需的映射方法定义一个Java接口,并用注释对其进行org.mapstruct.Mapper注释:
该@Mapper注释将使得MapStruct代码生成器创建的执行PersonMapper 过程中生成时的界面。
在生成的方法实现中,源类型(例如Person)的所有可读属性都将被复制到目标类型(例如PersonDTO)的相应属性中:
- 当一个属性与其目标实体对应的名称相同时,它将被隐式映射。
- 当属性在目标实体中具有不同的名称时,可以通过@Mapping注释指定其名称。
如果不指定@Mapping,默认映射name相同的field
如果映射的对象field name不一样,通过 @Mapping 指定。
忽略字段加@Mapping#ignore() = true
代码如下(示例):
@Data
public class Person {
String describe;
private String id;
private String name;
private int age;
private BigDecimal source;
private double height;
private Date createTime;
}
@Data
public class PersonDTO {
String describe;
private Long id;
private String personName;
private String age;
private String source;
private String height;
}
// mapper
@Mapper
public interface PersonMapper {
PersonMapper INSTANCT = Mappers.getMapper(PersonMapper.class);
@Mapping(target = "name", source = "personName")
@Mapping(target = "id", ignore = true) // 忽略id,不进行映射
PersonDTO conver(Person person);
}
生成的实现类:
public class PersonMapperImpl implements PersonMapper {
public PersonMapperImpl() {
}
public PersonDTO conver(Person person) {
if (person == null) {
return null;
} else {
PersonDTO personDTO = new PersonDTO();
personDTO.setDescribe(person.getDescribe());
if (person.getId() != null) {
personDTO.setId(Long.parseLong(person.getId()));
}
personDTO.setPersonName(person.getName());
personDTO.setAge(String.valueOf(person.getAge()));
if (person.getSource() != null) {
personDTO.setSource(person.getSource().toString());
}
personDTO.setHeight(String.valueOf(person.getHeight()));
return personDTO;
}
}
}
测试:
@Test
public void test(){
Person person = new Person();
person.setDescribe("测试");
person.setAge(18);
person.setName("张三");
person.setHeight(170.5);
person.setSource(new BigDecimal("100"));
PersonDTO dto = PersonMapper.INSTANCT.conver(person);
System.out.println(dto);
// PersonDTO(describe=测试, id=null, personName=张三, age=18, source=100, height=170.5)
}
2.2 指定默认值
在@Mapper接口类里面的转换方法上添加@Mapping注解
target() 必须添加,source()可以不添加,则直接使用defaultValue
@Mapping(target = "describe", source = "describe", defaultValue = "默认值")
PersonDTO conver(Person person);
生成的impl:
...
if (person.getDescribe() != null) {
personDTO.setDescribe(person.getDescribe());
} else {
personDTO.setDescribe("默认值");
}
...
测试:
```java
@Test
public void test(){
Person person = new Person();
//person.setDescribe("测试");
person.setAge(18);
person.setName("张三");
person.setHeight(170.5);
person.setSource(new BigDecimal("100"));
PersonDTO dto = PersonMapper.INSTANCT.conver(person);
System.out.println(dto);
// PersonDTO(describe=默认值, id=null, name=张三, age=18, source=100, height=170.5)
}
2.3 使用表达式
目前java是唯一受支持的语言,达式必须以Java表达式的形式给出
注意: 这个属性不能与source()、defaultValue()、defaultExpression()、qualifiedBy()、qualifiedByName()或constant()一起使用。
@Mapping(target = "describe", source = "describe", defaultValue = "默认值")
@Mapping(target = "createTime",expression = "java(new java.util.Date())")
PersonDTO conver(Person person);
测试:
@Test
public void test(){
Person person = new Person();
//person.setDescribe("测试");
person.setAge(18);
person.setName("张三");
person.setHeight(170.5);
person.setSource(new BigDecimal("100"));
PersonDTO dto = PersonMapper.INSTANCT.conver(person);
System.out.println(dto);
// PersonDTO(describe=默认值, id=null, name=张三, age=18, source=100, height=170.5, createTime=Fri Dec 11 23:21:31 GMT+08:00 2020)
}
默认表达式@Mapping#defaultExpression()是默认值和表达式的组合。仅当source属性为null时才使用它们
2.4 dateFormat
如果属性从字符串映射到日期,则该格式字符串可由SimpleDateFormat处理,反之亦然。当映射枚举常量时,将忽略所有其他属性类型。
....
@Mapping(target = "createTime" ,source = "createTime", dateFormat = "yyyy-MM-dd")
PersonDTO conver(Person person);
...
impl:
try {
if (person.getCreateTime() != null) {
personDTO.setCreateTime((new SimpleDateFormat("yyyy-MM-dd")).parse(person.getCreateTime()));
}
} catch (ParseException var4) {
throw new RuntimeException(var4);
}
2.5 组合映射
2.5.1 多个源对象
@Data
public class BasicEntity {
private Date createTime;
private String createBy;
private Date updateTime;
private String updateBy;
private int _ROW;
}
@Mapper(uses =DateFormtUtil.class)
public interface PersonMapper {
PersonMapper INSTANCT = Mappers.getMapper(PersonMapper.class);
@Mapping(target = "personName",source = "name")
PersonDTO conver(Person person);
@Mapping(target = "createTime",source = "basicEntity.createTime")
PersonDTO combinationConver(Person personC, BasicEntity basicEntity);
}
2.5.2 使用其他的值
...
@Mapping(target = "id", source = "id")
PersonDTO mapTo(Person person, String id);
...
虽然Person和Person有相同的id字段,但是映射器会使用mapTo方法里面的id参数。
2.6 嵌套映射
@Data
public class Person {
...
private Child personChild;
...
}
@Data
public class PersonDTO {
...
private Child child;
...
}
// mapper
@Mapper(uses =DateFormtUtil.class)
public interface PersonMapper {
PersonMapper INSTANCT = Mappers.getMapper(PersonMapper.class);
@Mapping(target = "child", source = "personChild")
PersonDTO conver(Person person);
}
如果field name一样则不需要指定@Mapping
2.7 numberFormat()
如果带注释的方法从数字映射到字符串,则使用DecimalFormat将格式字符串作为可处理的格式。反之亦然。对于所有其他元素类型,将被忽略。
从基本2.1 基本映射可以看出,number类型与字符串直接的转换是通过valueOf(),如果字符串格式不正确会抛出java.lang.NumberFormatException异常,例如:Integer.valueOf(“10.2”)
使用numberFormat()之后DecimalFormat格式转换,还是会抛出NFE异常
// mapper
....
@Mapping(target = "age",source = "age", numberFormat = "#0.00")
PersonDTO conver(Person person);
...
// imppl
personDTO.setAge((new DecimalFormat("#0.00")).format((long)person.getAge()));
2.8 逆映射
在双向映射的情况下,例如从实体到DTO以及从DTO到实体,前向方法和反向方法的映射规则通常是相似的,并且可以通过切换source和来简单地反转target。
使用注释@InheritInverseConfiguration表示方法应继承相应反向方法的反向配置
....
@Mapping(target = "age",source = "age", numberFormat = "#0.00")
PersonDTO conver(Person person);
@InheritInverseConfiguration
Person conver(PersonDTO dto);
...
2.9 继承与共享配置
2.9.1 继承配置
方法级配置注解,例如@Mapping,@BeanMapping,@IterableMapping,等等,都可以继承从一个映射方法的类似使用注释方法@InheritConfiguration:
@Mapper
public interface CarMapper {
@Mapping(target = "numberOfSeats", source = "seatCount")
Car carDtoToCar(CarDto car);
@InheritConfiguration
void carDtoIntoCar(CarDto carDto, @MappingTarget Car car);
}
上面的示例声明了一种carDtoToCar()具有配置的映射方法,该配置定义了应如何映射numberOfSeats类型中的属性Car。在现有Instance实例上执行映射的update方法Car需要相同的配置才能成功映射所有属性。通过声明@InheritConfiguration该方法,MapStruct可以搜索继承候选,以应用继承自该方法的注释。
如果所有类型的A(源类型和结果类型)都可以分配给B的相应类型,则一个方法A可以从另一种方法B继承配置。
如果可以使用多个方法作为继承的源,则必须在注释中指定方法名称:@InheritConfiguration( name = “carDtoToCar” )。
一种方法,可以使用==@InheritConfiguration==和覆盖或通过另外施加修改的配置@Mapping,@BeanMapping等等。
2.9.2 共享配置
MapStruct提供了通过指向带注释的中央接口来定义共享配置的可能性@MapperConfig。为了使映射器使用共享配置,需要在@Mapper#config属性中定义配置接口。
该@MapperConfig注释具有相同的属性@Mapper注释。任何未通过via指定的属性@Mapper都将从共享配置中继承。中指定@Mapper的属性优先于通过引用的配置类指定的属性。列表属性例如uses可以简单组合:
@MapperConfig(unmappedTargetPolicy = ReportingPolicy.ERROR )
public interface CentralConfig {
}
@Mapper(config = CentralConfig.class } )
// Effective configuration:
// @Mapper(uses = { CustomMapperViaMapper.class, CustomMapperViaMapperConfig.class },
// unmappedTargetPolicy = ReportingPolicy.ERROR // )
public interface SourceTargetMapper { ... }
共享配置config,配置一些检查策略
例如:
- unmappedSourcePolicy()、unmappedTargetPolicy() : 源或者目标没有标注映射的属性怎么报告
- typeConversionPolicy() :应该报告如何进行有损(缩小)转换,例如:long到integer的转换。
- collectionMappingStrategy(): 集合类型映射策略
其他的,请阅读源码
3. 使用自定义方法
3.1 自定义类型转换方法
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:
@Mapper(uses=DateMapper.class)
public interface PersonMapper{
PersonMapper INSTANCT = Mappers.getMapper(PersonMapper.class);
PersonDTO conver(Person person);
}
impl:
public class PersonMapperImpl implements PersonMapper {
private final DateMapper dateMapper = new DateMapper();
public PersonMapperImpl() {
}
public PersonDTO conver(Person person) {
....
personDTO.setCreateTime(this.dateMapper.asDate(person.getCreateTime()));
...
return personDTO;
}
}
在进行类型转换的时候直接调用改转换方法
@Mapper#uses可以使用多个类
3.2 使用@Qualifier
在许多情况下,需要mapper具有相同的方法签名(除了名称)但具有不同的行为。MapStruct 有一个方便的机制来处理这种情况:( @Qualifier) org.mapstruct.Qualifier。“限定符”是用户可以编写的自定义注解,“粘贴到”映射方法中,该方法包含为使用的Mapper,并且可以在 bean 属性映射、可迭代映射或映射映射中引用。多个限定符可以“粘贴到”一个方法和映射上。
public class Titles {
public String translateTitleEG(String title) {
// some mapping logic
}
public String translateTitleGE(String title) {
// some mapping logic
}
}
以及使用此手写映射器的映射器,其中source和target具有应映射的属性“标题”:
导致不明确的映射方法错误:如果不使用限定符,这将导致不明确的映射方法错误,因为找到了 2 个限定方法 ( translateTitleEG, translateTitleGE) 并且 MapStruct 不会提示选择哪一个。
@Mapper( uses = Titles.class )
public interface MovieMapper {
GermanRelease toGerman( OriginalRelease movies );
}
输入限定符方法:声明限定符类型
import org.mapstruct.Qualifier;
@Qualifier
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface TitleTranslator {
}
并且,一些限定符指示使用哪个翻译器将源语言映射到目标语言:
为映射方法声明限定符类型
import org.mapstruct.Qualifier;
@Qualifier
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface EnglishToGerman {
}
@Qualifier
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface GermanToEnglish {
}
请注意TitleTranslator类型级别的目标,EnglishToGerman方法GermanToEnglish级别的目标!
然后,使用限定符,映射可能如下所示:使用限定符的映射器
@Mapper( uses = Titles.class )
public interface MovieMapper {
@Mapping( target = "title", qualifiedBy = { TitleTranslator.class, EnglishToGerman.class } )
GermanRelease toGerman( OriginalRelease movies );
}
自定义映射器限定它提供的方法:
@TitleTranslator
public class Titles {
@EnglishToGerman
public String translateTitleEG(String title) {
// some mapping logic
}
@GermanToEnglish
public String translateTitleGE(String title) {
// some mapping logic
}
}
3.3 使用@Named
在许多情况下,声明一个新的注解来帮助选择过程,对于试图实现的目标来说可能太多了。针对该情况,MapStruct 有@Named注解。此注解是预定义的限定符(用@Qualifier自身注释),可用于命名映射器,或者更直接地通过其值命名映射方法。上面的相同示例如下所示:
自定义映射器,注释要通过以下方式限定的方法@Named
@Named("TitleTranslator")
public class Titles {
@Named("EnglishToGerman")
public String translateTitleEG(String title) {
// some mapping logic
}
@Named("GermanToEnglish")
public String translateTitleGE(String title) {
// some mapping logic
}
}
使用命名的映射器
@Mapper( uses = Titles.class )
public interface MovieMapper {
@Mapping( target = "title", qualifiedByName = { "TitleTranslator", "EnglishToGerman" } )
GermanRelease toGerman( OriginalRelease movies );
}
3.3 将限定符与默认值组合
请注意 Mapping#defaultValue本质上是 a String,需要转换为 Mapping#target。提供Mapping#qualifiedByNameorMapping#qualifiedBy将强制 MapStruct 使用该方法。如果您想要 的不同行为Mapping#defaultValue,请提供适当的映射方法。这种映射方法需要将 a String转换成想要的类型,并且还要进行注解,以便通过Mapping#target找到它。Mapping#qualifiedByNameMapping#qualifiedBy
使用 defaultValue 的mapping
@Mapper
public interface MovieMapper {
@Mapping( target = "category", qualifiedByName = "CategoryToString", defaultValue = "DEFAULT" )
GermanRelease toGerman( OriginalRelease movies );
@Named("CategoryToString")
default String defaultValueForQualifier(Category cat) {
// some mapping logic
}
}
在上面的示例中,如果类别为空,CategoryToString( Enum.valueOf( Category.class, “DEFAULT” ) )将调用该方法并将结果设置为类别字段。
使用默认值和默认方法的映射器:
@Mapper
public interface MovieMapper {
@Mapping( target = "category", qualifiedByName = "CategoryToString", defaultValue = "Unknown" )
GermanRelease toGerman( OriginalRelease movies );
@Named("CategoryToString")
default String defaultValueForQualifier(Category cat) {
// some mapping logic
}
@Named("CategoryToString")
default String defaultValueForQualifier(String value) {
return value;
}
}
在上面的示例中,如果类别为空,defaultValueForQualifier( “Unknown” )将调用该方法并将结果设置为类别字段。
如果上述方法不起作用,则可以使用选项defaultExpression来设置默认值。
使用 defaultExpression 的映射器
@Mapper
public interface MovieMapper {
@Mapping( target = "category", qualifiedByName = "CategoryToString", defaultExpression = "java(\"Unknown\")" )
GermanRelease toGerman( OriginalRelease movies );
@Named("CategoryToString")
default String defaultValueForQualifier(Category cat) {
// some mapping logic
}
}
源码详见:
https://github.com/xiaojunzhang09/OpenSource/tree/main/mapstruct_demo
本章主要阐述了mapstruct 的基本用法,一些高级用法,如:SPI运用、FreeMarker生成代码,以及相关生成代码原理都还未涉及,后续会不断更新,敬请期待!