MapStruct是一个代码生成库,简化了Java对象到Java对象之间的映射过程。通过注解接口,它可以自动生成转换代码,支持多种映射场景,如属性映射、定制方法、目标对象更新等。此外,还支持数据类型转换、嵌套对象映射和使用上下文对象定制转换。在Spring项目中,可通过Maven/Gradle依赖和配置进行集成。

注:MapStruct 从版本 1.2.0.Beta1 开始支持与 Lombok 整合,利用 Lombok 生成的 getter、setter 和构造函数来生成映射实现。

配置

Maven

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
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>

Gradle

1
2
3
4
5
implementation "org.mapstruct:mapstruct:${mapstructVersion}"
compileOnly "org.projectlombok:lombok"
annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
annotationProcessor "org.projectlombok:lombok-mapstruct-binding:${lombokMapstructBindingVersion}"
annotationProcessor "org.projectlombok:lombok"

创建映射接口(普通使用)

创建一个Java接口,并使用@Mapper注解标记它。例如:

1
2
3
4
5
6
7
@Mapper
public interface MyMapper {
MyMapper INSTANCE = Mappers.getMapper(MyMapper.class);

TargetObject sourceToTarget(SourceObject source);
// 定义其他映射方法
}

上述代码定义了一个映射接口MyMapper,其中有一个映射方法sourceToTarget用于将SourceObject映射为TargetObjectINSTANCE字段用于获取映射器的实例。

此时我们编译项目之后,可以看见生成的MyMapper实现类中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override  
public TargetObject sourceToTarget(SourceObject source) {
if ( source == null ) {
return null;
}

TargetObject targetObject = new TargetObject();

targetObject.setUserName( source.getUserName() );
targetObject.setUserId( source.getUserId() );
targetObject.setSex( source.getSex() );

return targetObject;
}

这样就省去了我们自己手写两个对象之间的字段映射,避免了大量的重复工作,大大增加了开发效率,其次也是最重要的一点就是我们可以很直观的看见两个对象之间的字段映射关系,不像Dozer那样每次基于反射区实现映射,我们无法看见两边的字段的映射,出现问题后不方便排查,功能上不可控。

很重要的一点提示:我们要养成在写完一个映射方法后,要养成一定一定提前编译看一下生成的实现类方法是否正确,同时也看看是否存在字段映射关系设置错误导致编译不通过。

映射接口使用

在业务代码或者其他代码方法中,我们可以直接使用MyConverter.INSTANCE.sourceToTarget(source)进行sourcetarget之间的转换。

1
2
3
TargetObject handleObject(SourceObject source){  
return MyConverter.INSTANCE.sourceToTarget(source);
}

MapStruct常用注解

了解MapStruct的注解及其属性是非常重要的,因为它们定义了映射规则和行为。以下是MapStruct中常用的注解及其属性:

@Mapper

用于标记一个接口或抽象类,用于定义对象之间的映射规则。它有多个属性可以配置映射器的功能。以下是 @Mapper 注解的一些常用属性:

componentModel

指定生成的映射器实例的组件模型,以便与应用框架集成。他有"default"(默认值)、"cdi"、"spring"等可选值(具体参考MappingConstants.ComponentModel)。我们着重介绍一下default以及spring:

  • default:MapStruct的默认组件模型
    在默认模式下,MapStruct 会生成一个无参数的构造函数的映射器实例。映射器实例的创建和管理由 MapStruct自动处理。实例通常通过 Mappers.getMapper(Class)获取。适用于简单的映射场景,无需额外的依赖注入或容器管理。
  • spring:使用Spring Framework的组件模型
    在 Spring 模式下,MapStruct 会生成一个使用 @Component 注解标记的映射器实例,从而允许通过 Spring 的 IoC 容器进行管理和依赖注入。适用于 Spring 框架中的应用,可以利用 Spring 的依赖注入功能。稍后我们会介绍这种模型的使用,也是我们日常使用SpringBoot开发时用的比较多的模型。比如上例中,我们使用spring的模型,则生成的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component  
public class MySpringMapperImpl implements MySpringMapper {

@Override
public TargetObject sourceToTarget(SourceObject source) {
if ( source == null ) {
return null;
}

TargetObject targetObject = new TargetObject();

targetObject.setUserName( source.getUserName() );
targetObject.setUserId( source.getUserId() );
targetObject.setSex( source.getSex() );

return targetObject;
}
}

可以看见实现类中自动加上了@Component,注入到Spring的容器中管理。

  • cdi:使用 Contexts and Dependency Injection (CDI) 的组件模型。
    在 CDI 模式下,MapStruct 会生成一个使用 @Dependent 注解标记的映射器实例,允许通过 CDI 容器进行管理和依赖注入。适用于Java EEJakarta EE中使用 CDI 的应用,可以利用 CDI 容器进行管理。

其余的大家感兴趣的可以去阅读源码,平时使用不多,这里就不过多介绍了。

uses

指定映射器使用的自定义转换器。自定义转换器是在映射过程中调用的方法,用于处理特定类型之间的自定义映射逻辑。如果我们两个对象之间有一个字段的属性值需要特殊处理之后在进行映射,即需要加上一些转换逻辑,我们就可以自定义一个转换器,然后在映射器中使用转换器中的方法。例如:SoureObject中的有一个枚举值,但是转换到TargetObject中时需要转换为具体的说明,那么此时我们就可以使用自定义转换器。

我们自定义一个转换器,并且定义一个转换方法:

1
2
3
4
5
6
7
public class MyConverter {  

@Named("convertSexDesc")
public String convertSexDesc(Integer sex){
return SexEnum.descOfCode(sex);
}
}

然后再映射器MyMapper中使用uses指定转换器,同时使用@Mapping注解指定两个字段的映射规则:

1
2
3
4
5
6
7
8
@Mapper(uses = {MyConverter.class})  
public interface MyMapper {

MyMapper INSTANCE = Mappers.getMapper(MyMapper.class);

@Mapping(target = "sex", source = "sex", qualifiedByName = "convertSexDesc")
TargetObject sourceToTarget(SourceObject source);
}

编译后可以看见实现类中生成的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyMapperImpl implements MyMapper {  

private final MyConverter myConverter = new MyConverter();

@Override
public TargetObject sourceToTarget(SourceObject source) {
if ( source == null ) {
return null;
}

TargetObject targetObject = new TargetObject();

targetObject.setSex( myConverter.convertSexDesc( source.getSex() ) );
targetObject.setUserName( source.getUserName() );
targetObject.setUserId( source.getUserId() );

return targetObject;
}
}

当然假如你的转换器或者转换方法,是你这个映射器独有,其他映射器不会使用这个转换方法,那么你可以直接在MyMapper中定义一个default的转换方法,就不必使用uses引入转换器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Mapper  
public interface MyMapper {

MyMapper INSTANCE = Mappers.getMapper(MyMapper.class);

@Mapping(target = "sex", source = "sex", qualifiedByName = "convertSexDesc")
TargetObject sourceToTarget(SourceObject source);


@Named("convertSexDesc")
default String convertSexDesc(Integer sex){
return SexEnum.descOfCode(sex);
}
}

编译后生成的实现类中,直接可以调用到这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyMapperImpl implements MyMapper {  

@Override
public TargetObject sourceToTarget(SourceObject source) {
if ( source == null ) {
return null;
}

TargetObject targetObject = new TargetObject();

targetObject.setSex( convertSexDesc( source.getSex() ) );
targetObject.setUserName( source.getUserName() );
targetObject.setUserId( source.getUserId() );

return targetObject;
}
}

在Java中,接口可以包含默认方法(Default Methods)。默认方法是在接口中提供一个默认的实现,这样在接口的实现类中就不需要强制性地实现该方法了。默认方法使用关键字 default 进行声明。

imports

导入其他类的全限定名,使其在生成的映射器接口中可见。比如我们可以导入其他的工具类去处理我们的字段,例如:StringUtils, CollectionUtilsMapUtils,或者一些枚举类等。同常运用@Mapping中的expression上。

1
2
3
4
5
6
7
8
@Mapper(imports = {StringUtils.class, SexEnum.class})  
public interface MyMapper {

MyMapper INSTANCE = Mappers.getMapper(MyMapper.class);

@Mapping(target = "sex", expression = "java(SexEnum.descOfCode(source.getSex()))")
TargetObject sourceToTarget(SourceObject source);
}

编译后生成的实现类中直接importimports中定义的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import com.springboot.code.mapstruct.SexEnum;
import org.springframework.util.StringUtils;

public class MyMapperImpl implements MyMapper {

@Override
public TargetObject sourceToTarget(SourceObject source) {
if ( source == null ) {
return null;
}

TargetObject targetObject = new TargetObject();

targetObject.setUserName( source.getUserName() );
targetObject.setUserId( source.getUserId() );

targetObject.setSex( SexEnum.descOfCode(source.getSex()) );

return targetObject;
}
}

当然我们也可以不使用imports去导入其他的类,那我们在使用这些类的方法时,必须写上他们的全路径:

1
2
3
4
5
6
7
8
@Mapper  
public interface MyMapper {

MyMapper INSTANCE = Mappers.getMapper(MyMapper.class);

@Mapping(target = "sex", expression = "java(com.springboot.code.mapstruct.SexEnum.descOfCode(source.getSex()))")
TargetObject sourceToTarget(SourceObject source);
}

编译后生成的实现类中,就不会import类了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyMapperImpl implements MyMapper {  

@Override
public TargetObject sourceToTarget(SourceObject source) {
if ( source == null ) {
return null;
}

TargetObject targetObject = new TargetObject();

targetObject.setUserName( source.getUserName() );
targetObject.setUserId( source.getUserId() );

targetObject.setSex( com.springboot.code.mapstruct.SexEnum.descOfCode(source.getSex()) );

return targetObject;
}
}

config

config 属性允许你指定一个映射器配置类,该配置类用于提供全局的配置选项。通过配置类,你可以定义一些全局行为,例如处理 null 值的策略、映射器名称、映射器组件模型等。

我们使用@MapperConfig定义一个映射器配置类 MyMapperConfig

1
2
3
4
5
6
7
8
@MapperConfig(  
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
componentModel = "default",
uses = MyConverter.class,
unmappedTargetPolicy = org.mapstruct.ReportingPolicy.WARN
)
public interface MyMapperConfig {
}

然后再MyMapper中指定config:

1
2
3
4
5
6
7
8
@Mapper(config = MyMapperConfig.class)  
public interface MyMapper {

MyMapper INSTANCE = Mappers.getMapper(MyMapper.class);

@Mapping(target = "sex", source = "sex", qualifiedByName = "convertSexDesc")
TargetObject sourceToTarget(SourceObject source);
}

我们可以集中管理映射器的一些全局行为,而不需要在每个映射器中重复配置。
在实际应用中,你可以根据项目需求定义不同的映射器配置类,用于管理不同的全局配置选项。这有助于提高代码的组织性和可维护性。

nullValueCheckStrategy

用于指定映射器对源对象字段的null值进行检查的策略。检查策略枚举类NullValueCheckStrategy值如下:

  • ALWAYS:始终对源值进行NULL检查。
    生成的实现类中,都是源值进行判NULL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override  
public TargetObject sourceToTarget(SourceObject source) {
if ( source == null ) {
return null;
}

TargetObject targetObject = new TargetObject();

if ( source.getSex() != null ) {
targetObject.setSex( myConverter.convertSexDesc( source.getSex() ) );
}
if ( source.getUserName() != null ) {
targetObject.setUserName( source.getUserName() );
}
if ( source.getUserId() != null ) {
targetObject.setUserId( source.getUserId() );
}

return targetObject;
}
  • ON_IMPLICIT_CONVERSION:不检查NULL值,直接将源值赋值给目标值

除了上述的属性值之外,还有一些其他的属性值,例如:

  • unmappedSourcePolicy: 未映射源对象字段的处理策略。
  • unmappedTargetPolicy: 未映射目标对象字段的处理策略。
    可选值:ReportingPolicy.IGNORE(忽略未映射字段,默认)、ReportingPolicy.WARN(警告)、ReportingPolicy.ERROR(抛出错误)。

以及其他的一些属性值,如果需要用到的同学,可以看一下源码中的介绍,这里就不过多叙述了。

@MapperConfig

注解用于定义映射器配置类,它允许在一个单独的配置类中集中管理映射器的全局配置选项。可以将一些全局的配置选项集中在一个配置类中,使得映射器的配置更为清晰和可维护。在实际应用中,可以根据需要定义不同的映射器配置类,以便在不同的场景中使用。配置类可以在映射器中通过@Mapperconfig属性引入。它大部分的属性值跟@Mapper一致。

1
2
3
4
5
6
7
8
@MapperConfig(  
nullValueCheckStrategy = NullValueCheckStrategy.ON_IMPLICIT_CONVERSION,
componentModel = "default",
uses = MyConverter.class,
unmappedTargetPolicy = org.mapstruct.ReportingPolicy.WARN
)
public interface MyMapperConfig {
}

然后再MyMapper中指定config:

1
2
3
4
5
6
7
8
@Mapper(config = MyMapperConfig.class)  
public interface MyMapper {

MyMapper INSTANCE = Mappers.getMapper(MyMapper.class);

@Mapping(target = "sex", source = "sex", qualifiedByName = "convertSexDesc")
TargetObject sourceToTarget(SourceObject source);
}

@Mapping

用于自定义映射器方法中的映射规则。它允许你指定源对象和目标对象之间字段的映射关系。

sourcetarget:

  • source 含义: 源对象字段的名称或表达式。
  • target 含义: 目标对象字段的名称。
1
2
@Mapping(target = "sourceField", source = "sourceField")  
TargetObject sourceToTarget(SourceObject source);

或者使用表达式的方式:

1
2
@Mapping(expression = "java(source.getSourceField())", target = "targetField")
TargetObject sourceToTarget(SourceObject source);

qualifiedByNamequalifiedBy:

  • qualifiedByName: 指定使用自定义转换器方法进行映射。

定义一个转换器MyNameConverter:

1
2
3
4
5
6
7
public class MyNameConverter {  

@Named("convertUserName")
public String convertUserName(String userName){
return Optional.ofNullable(userName).map(String::toUpperCase).orElse(userName);
}
}

使用自定义转换器的方法:

1
2
3
4
5
6
7
@Mapper( uses = {MyNameConverter.class}, nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS)  
public interface MyMapper {

MyMapper INSTANCE = Mappers.getMapper(MyMapper.class);

@Mapping(target = "userName", source = "userName", qualifiedByName = "convertUserName")
TargetObject sourceToTarget(SourceObject source);
  • qualifiedBy: 指定使用基于@qualifier注解的转换方法

先定义一个基于@qualifier(mapstruct包下)的作用于转换器类上的注解@StrConverter:

1
2
3
4
5
@Qualifier  
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface StrConverter {
}

再定义一个基于@qualifier(mapstruct包下)的作用于转换器方法上的注解@NameUpper:

1
2
3
4
5
@Qualifier  
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface NameUpper {
}

最后定义一个自定义转换器MyNameConverter:

1
2
3
4
5
6
7
8
9
@StrConverter  
public class MyNameConverter {


@NameUpper
public String convertUserName(String userName){
return Optional.ofNullable(userName).map(String::toUpperCase).orElse(userName);
}
}

然后我们在@Mappinbg中通过使用:

1
2
3
4
5
@Mapper(uses = {MyNameConverter.class} ,nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS)  
public interface MyMapper {
@Mapping(target = "userName", source = "userName", qualifiedBy = NameUpper.class)
TargetObject sourceToTarget(SourceObject source);
}

最终两种方式编译后的结果是一致的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MyMapperImpl implements MyMapper {  

private final MyNameConverter myNameConverter = new MyNameConverter();

@Override
public TargetObject sourceToTarget(SourceObject source) {
if ( source == null ) {
return null;
}

TargetObject targetObject = new TargetObject();

if ( source.getUserName() != null ) {
targetObject.setUserName( myNameConverter.convertUserName( source.getUserName() ) );
}
if ( source.getUserId() != null ) {
targetObject.setUserId( source.getUserId() );
}

return targetObject;
}
}

以上基于qualifiedBy的使用示例参考自@Qualifier源码文档。

ignore

是否忽略某字段的映射。为true时忽略。

1
2
@Mapping(target = "sex", source = "sex", ignore = true)
TargetObject sourceToTarget(SourceObject source);

编译后实现类中不会对这个字段进行赋值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override  
public TargetObject sourceToTarget(SourceObject source) {
if ( source == null ) {
return null;
}

TargetObject targetObject = new TargetObject();

if ( source.getUserName() != null ) {
targetObject.setUserName( source.getUserName() );
}
if ( source.getUserId() != null ) {
targetObject.setUserId( source.getUserId() );
}

return targetObject;
}

defaultExpression

指定默认表达式,当源对象字段为 null 时使用。

1
2
@Mapping(target = "sex", source = "sex", defaultExpression = "java(SexEnum.MAN.desc)")
TargetObject sourceToTarget(SourceObject source);

编译后实现类:

1
2
3
4
5
6
if ( source.getSex() != null ) {  
targetObject.setSex( String.valueOf( source.getSex() ) );
}
else {
targetObject.setSex( SexEnum.MAN.desc );
}

defaultExpression不能与expression,defaultValue,constant一起使用。

defaultValue

指定默认值,当源对象字段为 null 时使用。

1
2
@Mapping(target = "sex", source = "sex", defaultValue = "男人")  
TargetObject sourceToTarget(SourceObject source);

编译后:

1
2
3
4
5
6
if ( source.getSex() != null ) {  
targetObject.setSex( String.valueOf( source.getSex() ) );
}
else {
targetObject.setSex( "男人" );
}

defaultValue不能与expression,defaultExpression,constant一起使用。

constant

将目标对象的字段设置为该常量。不从源对象中映射值。

1
2
@Mapping(target = "source", constant = "API")  
TargetObject sourceToTarget(SourceObject source);

编译后:

1
targetObject.setSource( "API" );

constant不能与defaultExpression,expression,defaultValue,constant, source一起使用。

expression

通过表达式完成映射。要基于该字符串设置指定的目标属性。目前,Java 是唯一受支持的“表达式语言”,表达式必须使用以下格式以 Java 表达式的形式给出:java(<EXPRESSION>)

1
2
@Mapping(target = "sex", expression = "java(SexEnum.descOfCode(source.getSex()))")  
TargetObject sourceToTarget(SourceObject source);

编译后:

1
targetObject.setSex( SexEnum.descOfCode(source.getSex()) );

expression不能与source, defaultValue, defaultExpression, qualifiedBy, qualifiedByName 以及constant 一起使用

dateFormat

指定日期格式化模式,仅适用于日期类型的字段。可以实现String类型时间和Date相互转换,基于SimpleDateFormat实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Data  
public class TargetObject {
private String createTime;

private Date loginDate;
}

@Data
public class SourceObject {
private Date createTime;

private String loginDate;
}


@Mapping(target = "createTime", source = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
@Mapping(target = "loginDate", source = "loginDate", dateFormat = "yyyy-MM-dd")
TargetObject sourceToTarget(SourceObject source);

编译后:

1
2
3
4
5
6
7
8
9
10
11
if ( source.getCreateTime() != null ) {  
targetObject.setCreateTime( new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss" ).format( source.getCreateTime() ) );
}
try {
if ( source.getLoginDate() != null ) {
targetObject.setLoginDate( new SimpleDateFormat( "yyyy-MM-dd" ).parse( source.getLoginDate() ) );
}
}
catch ( ParseException e ) {
throw new RuntimeException( e );
}

numberFormat

指定数值格式化格式,仅适用Number类型的字段。可以实现String类型数值与Number相互转换,基于DecimalFormat实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Data  
public class TargetObject {
private double amountDouble;

private String amountStr;
}

@Data
public class SourceObject {
private String amountStr;

private double amountDouble;
}

@Mapping(target = "amountDouble", source = "amountStr", numberFormat = "#,###.00")
@Mapping(target = "amountStr", source = "amountDouble", numberFormat = "#,###.00")
TargetObject sourceToTarget(SourceObject source);

编译后:

1
2
3
4
5
6
7
8
9
try {  
if ( source.getAmountStr() != null ) {
targetObject.setAmountDouble( new DecimalFormat( "#,###.00" ).parse( source.getAmountStr() ).doubleValue() );
}
}
catch ( ParseException e ) {
throw new RuntimeException( e );
}
targetObject.setAmountStr( new DecimalFormat( "#,###.00" ).format( source.getAmountDouble() ) );

还有其他的属性,这里就不过多叙述了,有兴趣或者需要的可以阅读源码。

@Mappings

包含多个@Mapping注解,将多个字段映射规则组合在一起,使代码更清晰。

1
2
3
4
5
6
7
8
9
@Mappings({  
@Mapping(target = "source", constant = "API"),
@Mapping(target = "sex", expression = "java(SexEnum.descOfCode(source.getSex()))"),
@Mapping(target = "createTime", source = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss"),
@Mapping(target = "loginDate", source = "loginDate", dateFormat = "yyyy-MM-dd"),
@Mapping(target = "amountDouble", source = "amountStr", numberFormat = "#,###.00"),
@Mapping(target = "amountStr", source = "amountDouble", numberFormat = "#,###.00")
})
TargetObject sourceToTarget(SourceObject source);

@Named

用于标记自定义转换器或者映射器中的某个方法的名称。一般配合qualifiedByName 使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 标记映射方法名称
*/
@Named("sourceToTarget")
TargetObject sourceToTarget(SourceObject source);

/**
* 标记转换器方法名称
*/
@Named("convertSexDesc")
default String convertSexDesc(Integer sex){
return SexEnum.descOfCode(sex);
}

我们在定义自己的转换器方法时,最好把方法都加上@Named的注解标记你的方法名称,否则如果后续代码中再写一个同类型的不同方法名的转换方法时编译报错:不明确的映射方法。

@IterableMapping

1
2
3
4
5
6
用于集合映射,定义集合元素的映射规则。其中一些属性例如:`qualifiedByName``qualifiedBy`以及`dateFormat``numberFormat`参考`@Mapping`中的用法。
@Named("sourceToTarget")
TargetObject sourceToTarget(SourceObject source);

@IterableMapping(qualifiedByName = "sourceToTarget")
List<TargetObject> sourceToTargetList(List<SourceObject> sourceObjectList);

编译后的实现类代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override  
public List<TargetObject> sourceToTargetList(List<SourceObject> sourceObjectList) {
if ( sourceObjectList == null ) {
return null;
}

List<TargetObject> list = new ArrayList<TargetObject>( sourceObjectList.size() );
for ( SourceObject sourceObject : sourceObjectList ) {
list.add( sourceToTarget( sourceObject ) );
}

return list;
}

可看出它内部循环调用sourceToTarget的方法完成list的转换。

需要特别注意,在写集合类型的转换时一定要配合IterableMappingqualifiedByNameNamed使用,如果不使用@IterableMapping中显示声明循环使用的方法时,它的内部会重新生成一个映射方法去使用。这样会在开发过程中出现一些莫名其妙的忽然就不好使的错误。。。。。

1
2
3
4
5
6
7
8
    @Named("sourceToTarget")  
TargetObject sourceToTarget(SourceObject source);

@Named("sourceToTarget2")
TargetObject sourceToTarget2(SourceObject source);

// @IterableMapping(qualifiedByName = "sourceToTarget")
List<TargetObject> sourceToTargetList(List<SourceObject> sourceObjectList);

编译后,实现类中代码可以看出并没有使用以上两个方法,而是重新生成的:

@MappingTarget

标记在映射方法的目标对象参数上,允许在映射方法中修改目标对象的属性。当目标对象已经创建了,此时可以将目标对象也当做参数传递到映射器方法中。

1
2
3
4
@Mapping(target = "source", constant = "API")  
@Mapping(target = "sex", expression = "java(SexEnum.descOfCode(source.getSex()))")
@Named("sourceToTarget3")
void sourceToTarget3(@MappingTarget TargetObject targetObject, SourceObject source);

编译后实现类代码:

1
2
3
4
5
6
7
8
9
10
11
12
@Override  
public void sourceToTarget3(TargetObject targetObject, SourceObject source) {
if ( source == null ) {
return;
}

targetObject.setUserName( source.getUserName() );
targetObject.setUserId( source.getUserId() );

targetObject.setSource( "API" );
targetObject.setSex( SexEnum.descOfCode(source.getSex()) );
}

@InheritConfiguration

它用于在映射接口中引用另一个映射方法的配置。主要用于减少代码重复,提高映射方法的可维护性。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Mappings({  
@Mapping(target = "source", constant = "API"),
@Mapping(target = "sex", expression = "java(SexEnum.descOfCode(source.getSex()))")
})
@Named("sourceToTarget")
TargetObject sourceToTarget(SourceObject source);

@InheritConfiguration(name = "sourceToTarget")
@Named("sourceToTarget2")
TargetObject sourceToTarget2(SourceObject source);

@InheritConfiguration(name = "sourceToTarget")
void sourceToTarget4(@MappingTarget TargetObject targetObject, SourceObject source);

sourceToTarget2sourceToTarget4就可以直接继承使用sourceToTarget的规则了。避免了再次定义一份相同的规则。

@InheritInverseConfiguration

双向映射

在双向映射的情况下,例如从实体到 DTO 以及从 DTO 到实体,正向方法和反向方法的映射规则通常是相似的,并且可以通过切换source和target来简单地反转。

使用注解@InheritInverseConfiguration可以标记当前方法应继承反向配置。

案例演示:carDtoToCar()方法是carToDto() 的反向映射方法。carToDto()中任何属性映射会自动反转并复制到带有@InheritInverseConfiguration注解的方法中。

1
2
3
4
5
6
7
8
9
10
@Mapper
public interface CarMapper {

@Mapping(target = "seatCount", source = "numberOfSeats")
CarDto carToDto(Car car);

@InheritInverseConfiguration
@Mapping(target = "numberOfSeats", ignore = true)
Car carDtoToCar(CarDto carDto);
}

来自反转方法的特定映射可以被忽略(ignore)或覆盖,例如:@Mapping(target = "numberOfSeats", ignore=true)

需要在当前映射器(超类/接口)中定义于反向继承的方法。

如果有多个方法符合条件,则需要使用name属性指定继承配置的方法:@InheritInverseConfiguration(name = "carToDto")

发生冲突时@InheritConfiguration优先于@InheritInverseConfiguration。

@BeanMapping

用于配置映射方法级别的注解,它允许在单个映射方法上指定一些特定的配置。例如忽略某些属性、配置映射条件等(开始我们在@Mapper中定义)。它提供了一种在方法级别自定义映射行为的方式。

1
2
3
@BeanMapping(nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS)  
@Named("sourceToTarget2")
TargetObject sourceToTarget2(SourceObject source);

编译后实现的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override  
public TargetObject sourceToTarget2(SourceObject source) {
if ( source == null ) {
return null;
}

TargetObject targetObject = new TargetObject();

if ( source.getUserName() != null ) {
targetObject.setUserName( source.getUserName() );
}
if ( source.getUserId() != null ) {
targetObject.setUserId( source.getUserId() );
}
if ( source.getSex() != null ) {
targetObject.setSex( String.valueOf( source.getSex() ) );
}

return targetObject;
}

校验了源对象值的null

@ValueMapping

用于自定义枚举类型或其他可映射类型的值映射。该注解允许在枚举类型映射时,定义自定义的值映射规则,使得在映射中可以转换不同的枚举值。他只有两个属性值:

  • source:只能取值:枚举值名称,MappingConstants.NULLMappingConstants.ANY_REMAININGMappingConstants.ANY_UNMAPPED
  • target: 只能取值:枚举值名称MappingConstants.NULLMappingConstants.ANY_UNMAPPED
1
2
3
4
5
6
7
8
9
10
11
  public enum OrderType { RETAIL, B2B, C2C, EXTRA, STANDARD, NORMAL }

public enum ExternalOrderType { RETAIL, B2B, SPECIAL, DEFAULT }

@ValueMappings({
@ValueMapping(target = "SPECIAL", source = "EXTRA"),
@ValueMapping(target = "DEFAULT", source = "STANDARD"),
@ValueMapping(target = "DEFAULT", source = "NORMAL"),
@ValueMapping(target = MappingConstants.THROW_EXCEPTION, source = "C2C" )
})
ExternalOrderTypeEnum mapOrderType(OrderTypeEnum 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
@Override  
public ExternalOrderTypeEnum mapOrderType(OrderTypeEnum orderType) {
if ( orderType == null ) {
return null;
}

ExternalOrderTypeEnum externalOrderTypeEnum;

switch ( orderType ) {
case EXTRA: externalOrderTypeEnum = ExternalOrderTypeEnum.SPECIAL;
break;
case STANDARD: externalOrderTypeEnum = ExternalOrderTypeEnum.DEFAULT;
break;
case NORMAL: externalOrderTypeEnum = ExternalOrderTypeEnum.DEFAULT;
break;
case C2C: throw new IllegalArgumentException( "Unexpected enum constant: " + orderType );
case RETAIL: externalOrderTypeEnum = ExternalOrderTypeEnum.RETAIL;
break;
case B2B: externalOrderTypeEnum = ExternalOrderTypeEnum.B2B;
break;
default: throw new IllegalArgumentException( "Unexpected enum constant: " + orderType );
}

return externalOrderTypeEnum;
}

@Context

@Context注解在MapStruct框架中用于标记映射方法的参数,使得这些参数作为映射上下文来处理。被标注为@Context的参数会在适用的情况下传递给其他映射方法、@ObjectFactory方法或者@BeforeMapping@AfterMapping方法,从而可以在自定义代码中使用它们。

具体作用如下:

  • 传递上下文信息: 当MapStruct执行映射操作时,它会将带有@Context注解的参数值向下传递到关联的方法中。这意味着你可以在不同的映射阶段(包括属性映射、对象工厂方法调用以及映射前后的处理方法)共享和利用这些上下文数据。
  • 调用相关方法: MapStruct还会检查带有@Context注解的参数类型上是否声明了@BeforeMapping@AfterMapping方法,并在适用时对提供的上下文参数值调用这些方法。
  • 空值处理: 注意,MapStruct不会在调用与@Context注解参数相关的映射前后方法或对象工厂方法之前进行空值检查。调用者需要确保在这种情况下不传递null值。
  • 生成代码的要求: 为了使生成的代码能够正确调用带有@Context参数的方法,正在生成的映射方法声明必须至少包含那些相同类型(或可赋值类型)的@Context参数。MapStruct不会为缺失的@Context参数创建新实例,也不会以null代替它们传递。

因此,@Context注解提供了一种机制,允许开发者在映射过程中携带并传播额外的状态或配置信息,增强了映射逻辑的灵活性和定制能力。

一个简单的用法示例:

1
2
3
4
5
6
7
8
9
	@Named("sourceToTarget5")  
@Mapping(target = "createTime", constant = "createTime", qualifiedByName = "formatDate")
TargetObject sourceToTarget5(SourceObject source, @Context ContextObject contextObject);

@Named("formatDate")
default String formatDate(LocalDateTime createTime, @Context ContextObject contextObject){
DateTimeFormatter dateTimeFormatter = contextObject.getDateTimeFormatter();
return dateTimeFormatter.format(createTime);
}

生成的实现类代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override  
public TargetObject sourceToTarget5(SourceObject source, ContextObject contextObject) {
if ( source == null ) {
return null;
}

TargetObject targetObject = new TargetObject();

targetObject.setUserName( source.getUserName() );
targetObject.setUserId( source.getUserId() );
if ( source.getSex() != null ) {
targetObject.setSex( String.valueOf( source.getSex() ) );
}

targetObject.setCreateTime( formatDate( LocalDateTime.parse( "createTime" ), contextObject ) );

return targetObject;
}

@BeforeMapping

这个注解可以标注在一个没有返回值的方法上,该方法会在执行实际映射操作前被调用。在此方法中可以通过@Context注入上下文对象,并根据需要对源对象或上下文进行修改或预处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
	@Named("sourceToTarget5")  
@Mapping(target = "createTime", constant = "createTime", qualifiedByName = "formatDate")
TargetObject sourceToTarget5(SourceObject source, @Context ContextObject contextObject);

@Named("formatDate")
default String formatDate(LocalDateTime createTime, @Context ContextObject contextObject){
DateTimeFormatter dateTimeFormatter = contextObject.getDateTimeFormatter();
return dateTimeFormatter.format(createTime);
}

@BeforeMapping
default void beforeFormatDate(@Context ContextObject context) {
// 在映射之前初始化或更新上下文中的信息
context.setDateTimeFormatter(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}

编译后生成的实现类代码中,会发现在sourceToTarget5的方法第一行会调用beforeFormatDate这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override  
public TargetObject sourceToTarget5(SourceObject source, ContextObject contextObject) {
// 第一行调用@BeforeMapping的方法
beforeFormatDate( contextObject );

if ( source == null ) {
return null;
}

TargetObject targetObject = new TargetObject();

targetObject.setUserName( source.getUserName() );
targetObject.setUserId( source.getUserId() );
if ( source.getSex() != null ) {
targetObject.setSex( String.valueOf( source.getSex() ) );
}

targetObject.setCreateTime( formatDate( LocalDateTime.parse( "createTime" ), contextObject ) );

return targetObject;
}

@AfterMapping

这个注解同样可以标注在一个没有返回值的方法上,但它会在完成所有属性映射后被调用。你可以在这里执行一些额外的转换逻辑或者基于映射结果和上下文进行后期处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Named("sourceToTarget5")  
@Mapping(target = "createTime", constant = "createTime", qualifiedByName = "formatDate")
TargetObject sourceToTarget5(SourceObject source, @Context ContextObject contextObject);

@Named("formatDate")
default String formatDate(LocalDateTime createTime, @Context ContextObject contextObject){
DateTimeFormatter dateTimeFormatter = contextObject.getDateTimeFormatter();
return dateTimeFormatter.format(createTime);
}

@BeforeMapping
default void beforeFormatDate(@Context ContextObject context) {
// 在映射之前初始化或更新上下文中的信息
context.setDateTimeFormatter(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}

@AfterMapping
default void afterHandler(SourceObject source, @MappingTarget TargetObject targetObject, @Context ContextObject contextObject){
targetObject.setContext(contextObject.getContext());
}

编译后,可以发现在sourceTarget5的实现方法中的最后会调用afterHandler方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override  
public TargetObject sourceToTarget5(SourceObject source, ContextObject contextObject) {
beforeFormatDate( contextObject );

if ( source == null ) {
return null;
}

TargetObject targetObject = new TargetObject();

targetObject.setUserName( source.getUserName() );
targetObject.setUserId( source.getUserId() );
if ( source.getSex() != null ) {
targetObject.setSex( String.valueOf( source.getSex() ) );
}

targetObject.setCreateTime( formatDate( LocalDateTime.parse( "createTime" ), contextObject ) );

afterHandler( source, targetObject, contextObject );

return targetObject;
}

@BeforeMapping@AfterMapping 注解的方法默认会作用于在同一接口内使用了相同参数类型的映射方法上。如果想要在一个地方定义一个通用的前置或后置处理逻辑,并让它应用于多个映射方法,可以编写一个不带具体映射源和目标参数的方法,并在需要应用这些逻辑的所有映射方法上保持相同的@Context参数类型。

@ObjectFactory

此注解用于声明一个工厂方法,该方法在目标对象实例化阶段被调用。这里也可以通过@Context获取到上下文信息,以便在创建目标对象时就考虑到某些上下文依赖。

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
@Named("sourceToTarget5")  
@Mapping(target = "createTime", constant = "createTime", qualifiedByName = "formatDate")
TargetObject sourceToTarget5(SourceObject source, @Context ContextObject contextObject);

@Named("formatDate")
default String formatDate(LocalDateTime createTime, @Context ContextObject contextObject){
DateTimeFormatter dateTimeFormatter = contextObject.getDateTimeFormatter();
return dateTimeFormatter.format(createTime);
}

@BeforeMapping
default void beforeFormatDate(@Context ContextObject context) {
// 在映射之前初始化或更新上下文中的信息
context.setDateTimeFormatter(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}

@AfterMapping
default void afterHandler(SourceObject source, @MappingTarget TargetObject targetObject, @Context ContextObject contextObject){
targetObject.setContext(contextObject.getContext());
}

@ObjectFactory
default TargetObject createTargetObject(@Context ContextObject contextObject){
TargetObject targetObject = new TargetObject();
// 根据上下文初始化dto的一些属性
targetObject.setContext(contextObject.getContext());
return targetObject;
}

编译后生成的实现类中,会看见TargetObject会通过createTargetObject方法创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override  
public TargetObject sourceToTarget5(SourceObject source, ContextObject contextObject) {
beforeFormatDate( contextObject );

if ( source == null ) {
return null;
}

TargetObject targetObject = createTargetObject( contextObject );

targetObject.setUserName( source.getUserName() );
targetObject.setUserId( source.getUserId() );
if ( source.getSex() != null ) {
targetObject.setSex( String.valueOf( source.getSex() ) );
}

targetObject.setCreateTime( formatDate( LocalDateTime.parse( "createTime" ), contextObject ) );

afterHandler( source, targetObject, contextObject );

return targetObject;
}

@ObjectFactory 标记的方法则更具有针对性,它通常用于为特定的目标对象创建实例。如果你定义了一个@ObjectFactory方法且没有指定具体映射方法,则这个工厂方法会作为默认的实例化方式,在所有未明确提供实例化方法的映射目标对象时被调用。

SpringBoot集成

上面我们说到了@Mapper注解以及他的属性componentModel,将该值设置为Spring也就是MappingConstants.ComponentModel.SPRING值时,这个映射器生成的实现类就可以被Spring容器管理,这样就可以在使用时就可以注入到其他组件中了。

1
2
3
4
5
6
7
8
9
@Mapper(uses = {MyNameConverter.class}, imports = {SexEnum.class}, componentModel = MappingConstants.ComponentModel.SPRING)  
public interface MyMapper {
@Mappings({
@Mapping(target = "source", constant = "API"),
@Mapping(target = "sex", expression = "java(SexEnum.descOfCode(source.getSex()))"),
})
@Named("sourceToTarget")
TargetObject sourceToTarget(SourceObject source);
}

生成的实现类自动加上@Component注解,并将其注册为Spring Bean,:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component  
public class MyMapperImpl implements MyMapper {
@Override
public TargetObject sourceToTarget(SourceObject source) {
if ( source == null ) {
return null;
}

TargetObject targetObject = new TargetObject();

targetObject.setUserName( source.getUserName() );
targetObject.setUserId( source.getUserId() );
if ( source.getCreateTime() != null ) {
targetObject.setCreateTime( DateTimeFormatter.ISO_LOCAL_DATE_TIME.format( source.getCreateTime() ) );
}

targetObject.setSource( "API" );
targetObject.setSex( SexEnum.descOfCode(source.getSex()) );

return targetObject;
}
}

这样就可以在其他组件中注入MyMapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SpringBootTest  
public class SpringbootCodeApplicationTests {
private MyMapper mapper;

@Test
void testMapper(){
TargetObject targetObject = mapper.sourceToTarget(new SourceObject());
System.out.println(targetObject.getSex());
}

@Autowired
public void setMapper(MyMapper mapper) {
this.mapper = mapper;
}