写点什么

MapStruct 使用指南

作者:星期35
  • 2022 年 4 月 29 日
  • 本文字数:7070 字

    阅读完需:约 23 分钟

MapStruct使用指南

MapStruct 官网:https://mapstruct.org

1、快速入门

Maven 依赖

<dependencies>    <dependency>        <groupId>org.mapstruct</groupId>        <artifactId>mapstruct</artifactId>        <version>${org.mapstruct.version}</version>    </dependency></dependencies>
复制代码

注:这个依赖会引入 MapStruct 的核心注释。由于 MapStruct 在编译时工作,并且会集成到像 Maven 和 Gradle 这样的构建工具上,我们还必须在 build 标签中添加插件 maven-compiler-plugin,并在其配置中添加 annotationProcessorPaths,该插件会在构建时生成对应代码。

<build>    <plugins>        <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>            </configuration>        </plugin>    </plugins></build>
复制代码
2、MapStruct 的核心注释
@Mapping@MappingTarget//忽略 categoryId 的转换@Mapping(target = "categoryId",ignore = true),//源数据类中的集合应用 --> 目标类中的数据引用–转换List@Mapping(target = "trees",source = "colors"),//嵌套类的属性简单传递转换@Mapping(target = "run",source = "cart.animal.run"),时间转换并格式化@Mapping(source = "birthday", target = "birthDateFormat", dateFormat = "yyyy-MM-dd HH:mm:ss"),@Mapping:属性映射,若源对象属性与目标对象名字一致,会自动映射对应属性source:源属性target:目标属性dateFormat:String 到 Date 日期之间相互转换,通过 SimpleDateFormat,该值为 SimpleDateFormat的日期格式ignore: 忽略这个字段@Mappings:配置多个@Mapping@MappingTarget 用于更新已有对象@InheritConfiguration 用于继承配置
复制代码
3、基本映射

我们先从一些基本的映射开始。我们会创建一个 Doctor 对象和一个 DoctorDto。为了方便起见,它们的属性字段都使用相同的名称:

public class Doctor {    private int id;    private String name;    // getters and setters or builder}
public class DoctorDto { private int id; private String name; // getters and setters or builder}
复制代码

为了在这两者之间进行映射,我们要创建一个 DoctorMapper 接口。对该接口使用 @Mapper 注解,MapStruct 就会知道这是两个类之间的映射器。

@Mapperpublic interface DoctorMapper {    DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);    DoctorDto toDto(Doctor doctor);}
复制代码

这段代码中创建了一个 DoctorMapper 类型的实例 INSTANCE,在生成对应的实现代码后,这就是我们调用的“入口”

我们在接口中定义了 toDto()方法,该方法接收一个 Doctor 实例为参数,并返回一个 DoctorDto 实例。这足以让 MapStruct 知道我们想把一个 Doctor 实例映射到一个 DoctorDto 实例。

当我们构建/编译应用程序时,MapStruct 注解处理器插件会识别出 DoctorMapper 接口并为其生成一个实现类。

public class DoctorMapperImpl implements DoctorMapper {    @Override    public DoctorDto toDto(Doctor doctor) {        if ( doctor == null ) {            return null;        }        DoctorDtoBuilder doctorDto = DoctorDto.builder();
doctorDto.id(doctor.getId()); doctorDto.name(doctor.getName());
return doctorDto.build(); }}
复制代码

DoctorMapperImpl 类中包含一个 toDto()方法,将我们的 Doctor 属性值映射到 DoctorDto 的属性字段中。如果要将 Doctor 实例映射到一个 DoctorDto 实例,可以这样写:

DoctorDto doctorDto = DoctorMapper.INSTANCE.toDto(doctor);
复制代码

注意:你可能也注意到了上面实现代码中的 DoctorDtoBuilder。因为 builder 代码往往比较长,为了简洁起见,这里省略了 builder 模式的实现代码。如果你的类中包含 Builder,MapStruct 会尝试使用它来创建实例;如果没有的话,MapStruct 将通过 new 关键字进行实例化。

不同字段间映射

我们需要让 DoctorMapper 知道这里的不一致。我们可以使用 @Mapping 注解,并设置其内部的 source 和 target 标记分别指向不一致的两个字段。

@Mapperpublic interface DoctorMapper {    DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);	//将目标对象的 doctor.specialty属性 拷贝给 specialization     @Mapping(source = "doctor.specialty", target = "specialization")    DoctorDto toDto(Doctor doctor);}
复制代码
多个源类 &不同属性名称

我们添加了另一个 @Mapping 注解,并将其 source 设置为 Education 类的 degreeName,将 target 设置为 DoctorDto 类的 degree 字段。

@Mapperpublic interface DoctorMapper {    DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);	/*  	    1:将源对象doctor的specialty赋值给目标对象的specialization属性 	    2:将源对象education的degreeName赋值给目标对象的degree属性	    3: 其他属性默认按同名拷贝	*/    @Mapping(source = "doctor.specialty", target = "specialization")    @Mapping(source = "education.degreeName", target = "degree")    DoctorDto toDto(Doctor doctor, Education education);}
复制代码
子对象映射(可引用其他 Mapper 的方法)

多数情况下,POJO 中不会只包含基本数据类型,其中往往会包含其它类。比如说,一个 Doctor 类中会有多个患者类:

public class Patient {    private int id;    private String name;    // getters and setters or builder}
复制代码

在 Doctor 中添加一个患者列表 List:

public class Doctor {    private int id;    private String name;    private String specialty;    private List<Patient> patientList;    // getters and setters or builder}
复制代码

因为 Patient 需要转换,为其创建一个对应的 DTO:

public class PatientDto {    private int id;    private String name;    // getters and setters or builder}
复制代码

最后,在 DoctorDto 中新增一个存储 PatientDto 的列表:

public class DoctorDto {    private int id;    private String name;    private String degree;    private String specialization;    private List<PatientDto> patientDtoList;    // getters and setters or builder}
复制代码

在修改 DoctorMapper 之前,我们先创建一个支持 Patient 和 PatientDto 转换的映射器接口:

@Mapperpublic interface PatientMapper {    PatientMapper INSTANCE = Mappers.getMapper(PatientMapper.class);    PatientDto toDto(Patient patient);}
复制代码

这是一个基本映射器,只会处理几个基本数据类型。然后,我们再来修改 DoctorMapper 处理一下患者列表:

@Mapper(uses = {PatientMapper.class})public interface DoctorMapper {
DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
@Mapping(source = "doctor.patientList", target = "patientDtoList") @Mapping(source = "doctor.specialty", target = "specialization") DoctorDto toDto(Doctor doctor);}
复制代码

因为我们要处理另一个需要映射的类,所以这里设置了 @Mapper 注解的 uses 标志,这样现在的 @Mapper 就可以使用另一个 @Mapper 映射器。我们这里只加了一个,但你想在这里添加多少 class/mapper 都可以。

我们已经添加了 uses 标志,所以在为 DoctorMapper 接口生成映射器实现时,MapStruct 也会把 Patient 模型转换成 PatientDto ——因为我们已经为这个任务注册了 PatientMapper。

编译查看最新想实现代码:

public class DoctorMapperImpl implements DoctorMapper {    private final PatientMapper patientMapper = Mappers.getMapper( PatientMapper.class );
@Override public DoctorDto toDto(Doctor doctor) { if ( doctor == null ) { return null; }
DoctorDtoBuilder doctorDto = DoctorDto.builder();
doctorDto.patientDtoList( patientListToPatientDtoList(doctor.getPatientList())); doctorDto.specialization( doctor.getSpecialty() ); doctorDto.id( doctor.getId() ); doctorDto.name( doctor.getName() );
return doctorDto.build(); } protected List<PatientDto> patientListToPatientDtoList(List<Patient> list) { if ( list == null ) { return null; }
List<PatientDto> list1 = new ArrayList<PatientDto>( list.size() ); for ( Patient patient : list ) { list1.add( patientMapper.toDto( patient ) ); }
return list1; }}
复制代码

显然,除了 toDto()映射方法外,最终实现中还添加了一个新的映射方法——patientListToPatientDtoList()。这个方法是在没有显式定义的情况下添加的,只是因为我们把 PatientMapper 添加到了 DoctorMapper 中。该方法会遍历一个 Patient 列表,将每个元素转换为 PatientDto,并将转换后的对象添加到 DoctorDto 对象内中的列表中。

更新现有实例

有时,我们希望用 DTO 的最新值更新一个模型中的属性,对目标对象(我们的例子中是 DoctorDto)使用 @MappingTarget 注解,就可以更新现有的实例。

@Mapper(uses = {PatientMapper.class})public interface DoctorMapper {
DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
@Mapping(source = "doctorDto.patientDtoList", target = "patientList") @Mapping(source = "doctorDto.specialization", target = "specialty") void updateModel(DoctorDto doctorDto, @MappingTarget Doctor doctor);}
复制代码

重新生成实现代码,就可以得到 updateModel()方法:

public class DoctorMapperImpl implements DoctorMapper {
@Override public void updateModel(DoctorDto doctorDto, Doctor doctor) { if (doctorDto == null) { return; }
if (doctor.getPatientList() != null) { List<Patient> list = patientDtoListToPatientList(doctorDto.getPatientDtoList()); if (list != null) { doctor.getPatientList().clear(); doctor.getPatientList().addAll(list); } else { doctor.setPatientList(null); } } else { List<Patient> list = patientDtoListToPatientList(doctorDto.getPatientDtoList()); if (list != null) { doctor.setPatientList(list); } } doctor.setSpecialty(doctorDto.getSpecialization()); doctor.setId(doctorDto.getId()); doctor.setName(doctorDto.getName()); }}
复制代码

值得注意的是,由于患者列表是该模型中的子实体,因此患者列表也会进行更新。

数据类型转换

MapStruct 支持 source 和 target 属性之间的数据类型转换。它还提供了基本类型及其相应的包装类之间的自动转换。

自动类型转换适用于:

基本类型及其对应的包装类之间。比如, int 和 Integer, float 和 Float, long 和 Long,boolean 和 Boolean 等。

任意基本类型与任意包装类之间。如 int 和 long, byte 和 Integer 等。

所有基本类型及包装类与 String 之间。如 boolean 和 String, Integer 和 String, float 和 String 等。

枚举和 String 之间。

Java 大数类型(java.math.BigInteger, java.math.BigDecimal) 和 Java 基本类型(包括其包装类)与 String 之间。

其它情况详见 MapStruct 官方文档。

因此,在生成映射器代码的过程中,如果源字段和目标字段之间属于上述任何一种情况,则 MapStrcut 会自行处理类型转换。

日期转换

日期进行转换时,我们也可以使用 dateFormat 设置格式声明。

@Mapperpublic interface PatientMapper {
@Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy") Patient toModel(PatientDto patientDto);}
复制代码

生成的实现代码形式大致如下:

public class PatientMapperImpl implements PatientMapper {
@Override public Patient toModel(PatientDto patientDto) { if (patientDto == null) { return null; }
PatientBuilder patient = Patient.builder();
if (patientDto.getDateOfBirth() != null) { patient.dateOfBirth(DateTimeFormatter.ofPattern("dd/MMM/yyyy") .format(patientDto.getDateOfBirth())); } patient.id(patientDto.getId()); patient.name(patientDto.getName());
return patient.build(); }}
复制代码

可以看到,这里使用了 dateFormat 声明的日期格式。如果我们没有声明格式的话,MapStruct 会使用 LocalDate 的默认格式,大致如下:

if (patientDto.getDateOfBirth() != null) {    patient.dateOfBirth(DateTimeFormatter.ISO_LOCAL_DATE                        .format(patientDto.getDateOfBirth()));}
复制代码
数字格式转换

上面的例子中可以看到,在进行日期转换的时候,可以通过 dateFormat 标志指定日期的格式。

除此之外,对于数字的转换,也可以使用 numberFormat 指定显示格式:

// 数字格式转换示例@Mapping(source = "price", target = "price", numberFormat = "$#.00")
复制代码
枚举映射

枚举映射的工作方式与字段映射相同。MapStruct 会对具有相同名称的枚举进行映射,这一点没有问题。但是,对于具有不同名称的枚举项,我们需要使用 @ValueMapping 注解。同样,这与普通类型的 @Mapping 注解也相似。

我们先创建两个枚举。第一个是 PaymentType:


public enum PaymentType {    CASH,    CHEQUE,    CARD_VISA,    CARD_MASTER,    CARD_CREDIT}
复制代码

比如说,这是一个应用内可用的支付方式,现在我们要根据这些选项创建一个更一般、有限的识图:

public enum PaymentTypeView {    CASH,    CHEQUE,    CARD}
复制代码

现在,我们创建这两个 enum 之间的映射器接口:

@Mapperpublic interface PaymentTypeMapper {
PaymentTypeMapper INSTANCE = Mappers.getMapper(PaymentTypeMapper.class);
@ValueMappings({ @ValueMapping(source = "CARD_VISA", target = "CARD"), @ValueMapping(source = "CARD_MASTER", target = "CARD"), @ValueMapping(source = "CARD_CREDIT", target = "CARD") }) PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType);}
复制代码

这个例子中,我们设置了一般性的 CARD 值,和更具体的 CARD_VISA, CARD_MASTER 和 CARD_CREDIT 。两个枚举间的枚举项数量不匹配—— PaymentType 有 5 个值,而 PaymentTypeView 只有 3 个。

为了在这些枚举项之间建立桥梁,我们可以使用 @ValueMappings 注解,该注解中可以包含多个 @ValueMapping 注解。这里,我们将 source 设置为三个具体枚举项之一,并将 target 设置为 CARD。

MapStruct 自然会处理这些情况:

public class PaymentTypeMapperImpl implements PaymentTypeMapper {
@Override public PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType) { if (paymentType == null) { return null; }
PaymentTypeView paymentTypeView;
switch (paymentType) { case CARD_VISA: paymentTypeView = PaymentTypeView.CARD; break; case CARD_MASTER: paymentTypeView = PaymentTypeView.CARD; break; case CARD_CREDIT: paymentTypeView = PaymentTypeView.CARD; break; case CASH: paymentTypeView = PaymentTypeView.CASH; break; case CHEQUE: paymentTypeView = PaymentTypeView.CHEQUE; break; default: throw new IllegalArgumentException( "Unexpected enum constant: " + paymentType ); } return paymentTypeView; }}
复制代码

CASH 和 CHEQUE 默认转换为对应值,特殊的 CARD 值通过 switch 循环处理。

但是,如果你要将很多值转换为一个更一般的值,这种方式就有些不切实际了。其实我们不必手动分配每一个值,只需要让 MapStruct 将所有剩余的可用枚举项(在目标枚举中找不到相同名称的枚举项),直接转换为对应的另一个枚举项。

可以通过 MappingConstants 实现这一点:

@ValueMapping(source = MappingConstants.ANY_REMAINING, target = "CARD")PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType);
复制代码


用户头像

星期35

关注

还未添加个人签名 2018.05.28 加入

还未添加个人简介

评论

发布
暂无评论
MapStruct使用指南_星期35_InfoQ写作社区