MapStruct 快速上手

MapStruct是一款用于对象间高效映射转换的工具,通过编译期注解减少手动编码。本文介绍了其使用场景、配置、核心概念,以及如何处理不同类型映射和复杂逻辑。尤其适合Web开发中大量且相似的映射操作。
摘要由CSDN通过智能技术生成

简介

MapStruct是一款基于编译期注解处理技术,用于对象之间的映射转换工具,如从DTO映射为Entity进行持久化,从Entity映射为VO进行页面渲染。

通常进行映射转换的2个对象大部分属性类型及属性名一致,或者完全一样。

MapStruct之前进行映射工作主要通过以下2种方式:

方式优点缺点使用场景
BeanUtils代码量少使用反射机制,存在性能问题少量映射转换需求且无复杂转换逻辑
手动编码映射灵活、可进行复杂逻辑转换需要编写大量模板代码少量映射转换需求且转换逻辑复杂、待映射转换的2个对象差异较大

MapStruct主要解决以上方式的2个问题:

  1. 减少编码量、减少模板代码
  2. 提升性能的同时保持灵活性

可以这么说,需手动编码进行的映射转换都能通过MapStruct搞定。但并不是说MapStruct适合所有场景,在适合手动编码映射时,盲目引入MapStruct反而会增加复杂性。

最适合使用MapStruct的场景:Web环境中,为保持领域对象独立性,存在与领域数据、行为相关的CommandDTOVO,需要大量的映射转换操作且彼此之间的属性都比较相似。

环境配置

MapStruct依赖包含2部分:核心API依赖、注解处理器

  1. Maven环境下依赖配置如下:

...
<properties>
    <org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
</properties>
...
<dependencies>
    <!-- 定义MapStruct核心依赖 -->
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${org.mapstruct.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>
                        <!-- 在编译插件中配置MapStruct注解处理器 -->
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${org.mapstruct.version}</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>
 

  1. IDE环境

    每个IDE中都有自身的代码语法树解析机制来进行代码快捷跳转、代码关系识别等。通过编译期注解处理器生成的代码无法被IDE识别,在使用时会提示编译错误。

    不同IDE需要安装对应的插件或配置解决该问题。以IDEA为例,需要开启Enable annotation processing.

    image-20220225223247038

快速上手

Mapper定义

DTO定义如下:

public class PersonDTO {
    private String id;
    private String userName;
    // 省略getter/setter
 

Entity定义如下:

public class Person {
    private String id;
    private String name;
    // 省略getter/setter
 

DTOEntity之间存在相同和不同名称的属性,这种属性间的差异符合大部分的情况。

下面定义Mapper来处理这两者之间的映射:

@Mapper
public interface PersonMapper {  
    // 当前PersonMapper实例对象
    PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);
    
    @Mapping(source = "name", target = "userName")
    PersonDTO toDTO(Person person);
 

分析一下上面多个定义的含义:

  1. Mapper为一个接口,通过接口定义一些行为,MapStruct负责在编译期生成实现该接口的代码。可以使用一个抽象类代替。

  2. @Mapper: 该注解用于标识当前对象为一个映射转换器,不可缺省

  3. toDTO方法:方法名无限制,MapStruct负责实现该方法。

  4. @Mapping:对于PersonPersonDTO名称不一致的属性显式指定映射关系。

  5. INSTANCE:该Mapper对应的实例,为保持单例,建议定义在接口内。

Mapper使用

在需要映射转换的位置调用PersonMapper.INSTANCE.toDTO(p)

原理浅析

MapStruct使用编译期注解处理技术JSR-269:Pluggable Annotation Processing API。通过定义一系列注解,在编译时解析这些注解来生成对应Mapper的实现类,真实的转换代码都在编译期间生成。

编译时MapStruct注解处理器会查找被@Mapper修饰的类,这也是为什么@Mapper不可以缺省。

编译会进行多轮,如果在过程中产生新的文件,那么会继续下一轮编译。在IDE开发环境下编译,可以观察到target目录下的子目录generated-sources包含了MapStruct处理器生成的源代码、classes目录下包含了再次编译的类文件。

MapStruct生成的源代码:

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2022-03-02T22:41:17+0800",
    comments = "version: 1.4.2.Final, compiler: javac, environment: Java 1.8.0_151 (Oracle Corporation)"
)
public class PersonMapperImpl implements PersonMapper {
    @Override
    public PersonDTO toDTO(Person person) {
        if ( person == null ) {
            return null;
        }
        PersonDTO personDTO = new PersonDTO();
        personDTO.setUserName( person.getName() );
        personDTO.setId( person.getId() );
        return personDTO;
    }
}

MapStruct为被@Mapper修饰的接口生成了一个实现代码,实现的逻辑与手工编码方式基本无差异,使用原生的getter/setter进行属性拷贝。

MapStructBeanUtils性能好的原因:使用原生的方法,把运行期的做的事情在编译期就做完了,避免通过反射查找带来的性能损耗。

基础使用

上面的示例展示了最简单的使用方法。

真实场景下,情况就变的很复杂,大体需要解答以下几个问题:

  1. 属性类型不一样怎样处理?

  2. 待映射转换的目标对象如何实例化?

  3. 如何处理枚举、集合、日期、内嵌引用类型?

  4. 存在复杂映射逻辑该怎样处理?

默认行为

MapStruct默认实现了很多隐式转换,这些隐式转换让我们在大多数时候可以不进行显式定义映射逻辑即可完成需求。

以下这些类型之间会自动进行映射:

  1. 基本类型及其包装类型之间

    如源属性为int类型,目标属性为Integer类型,则未显式指定映射时,会自动映射。

  2. 基本类型(及其包装类型)与String

    如源属性为String类型,目标属性为Integer类型,则未显式指定映射时,会自动生成映射Integer.parseInt("2")

  3. 枚举类型与String类型之间

  4. 日期类型与String类型之间

    日期类型较为特殊,一般需要指定日期的格式。需要在映射方法上通过@Mapping#dateFormat显式指定日期格式,如@Mapping(source="birthday", target="birthday", dateFormat="yyyy-MM-dd")

集合映射

集合之间的映射与Bean对象之间映射无太大区别,只是在这个基础上循环进行Bean对象映射转换。

List<PersonDTO> toDTOs(List<Person> personList);

生成的代码对persons进行遍历对每个Person映射转换并放到一个新的集合中,以下为生成的代码片段:

@Override
public List<PersonDTO> toDTOs(List<Person> personList) {
    if ( personList == null ) {
        return null;
    }
    List<PersonDTO> list = new ArrayList<PersonDTO>( personList.size() );
    for ( Person person : personList ) {
        list.add( toDTO( person ) );
    }
    return list;
}

Map的处理与集合一样。

可能这里会引申出另一个问题:源对象是个集合,目标对象是个普通Bean(或者相反)该怎么处理? 这里只能使用后面会说过的自定义映射方法。

内嵌引用对象

源对象中包含一个引用对象的属性,MapStruct会自动递归处理这种内嵌的引用对象属性。

但是显式指定映射配置时,需要通过层级属性名来指定内嵌对象的属性名。如Person中包含一个address(Address)对象属性,PersonDTO中包含一个address(AddressDTO)对象属性,通过@Mapping显式指定属性映射:

// source=address.name 为Address中的属性名, target=address.addressName 为AddressDTO中的属性名
@Mapping(source="address.name", target="address.addressName")
PersonDTO toDTO(Person person);

平坦化内嵌对象

当目标对象中的属性为源对象的基本属性及内嵌对象的属性组合时,可以在@Mapping中指定target=.来时平坦化内嵌对象。

Person定义如下:

public class Person {
    private String id;
    private String name;
    private Address address;
    // 省略setter/getter
}

Address定义如下:

public class Address {
    private String id;
    private String city;
    private String detail;
    // 省略setter/getter
}

PersonDTO定义如下:

public class PersonDTO {
    private String id;
    private String name;
    private String city;
    private String detail;
    // 省略setter/getter
}

如上示例,PersonDTO中包含了Person部分基础属性以及Address的属性。

一种方式在Mapper中显式指定出需要映射的内嵌属性,这种方式在属性过多时,较为繁琐。MapStruct提供了另一种较为简便的方式:平坦化内嵌对象。

@Mapping(source = "id", target = "id")
@Mapping(source = "address", target = ".")
// @Mapping(source = "address.city", target = "city")
// @Mapping(source = "address.detail", target = "detail")
PersonDTO toDTO(Person person);

使用@Mapping中指定target=.将源属性address中的所有属性映射到当前目标对象PersonDTO的直接属性上,无需对每个内嵌对象的属性显式指定映射关系。

需要注意的是:如果bean的基础属性与内嵌对象的属性冲突时,需要显式指定映射关系。如Address中存在id属性,Person中也存在id属性,此时MapStruct无法区分出哪个才是真正需要的。

自定义映射方法

以上都是通过一些内置的配置完成映射,MapStruct还允许用户自定义方法来完成一些属性之间的复杂映射逻辑。

复用上例中的PersonAddress,定义PersonDTO:

public class PersonDTO {
    private String id;
    private String name;
    private String address; // address 由Address中的city + detail拼接而成
   // 省略setter/getter
}

实现需求:Address中的citydetail属性值拼接成一个新的字符后才能作为PersonDTO中的address属性值。

PersonDTO toDTO(Person person);
default String addressToString(Address address) {
    return address.getCity() + "-" + address.getDetail();
}

自定义了一个addressToString方法用于拼接逻辑,MapStruct在使用该方法返回的结果对address属性的赋值。

MapStruct是如何识别自定义的映射方法?

源属性的类型作为参数,目标属性的类型作为返回值即可作为该属性对映射的自定义方法。如上例中,需要把一个Address类型的属性映射成一个String类型的属性,与addressToString方法定义一致。

在接口中定义默认方法是JDK8才有的特性,如果JDK版本较低时,可以将Mapper定义为抽象类。

额外的问题

一些特定问题,需参考官网说明,此处抛砖引玉简单介绍一下:

  1. 除了在当前Mapper自定义方法,如果某类属性映射会在多个Mapper中共享时也可以通过@Mapper#uses指定一个外部的类专门进行自定义映射方法。

  2. 一对映射属性找到多个满足要求的方法时,默认行为会抛出异常。可以通过@qualifiedBy qualifiedByName 精确指定方法

更新已有对象属性

上面所有示例都在说明从一个源对象映射转换成一个新对象。在一些场景下需要指定源对象来更新已有的对象属性而不是创建一个新的对象,如在web更新用户信息:前端传递需要修改的信息,后端根据id查找在数据库中持久化的用户数据,将前端的数据更新到当前对象中。

MapStruct通过@MappingTarget来支持这种更新操作。

// 方法的参数返回类型不是必须,可以指定为void
Person toEntity(PersonDTO personDTO, @MappingTarget Person person);

@MappingTarget用来指定待更新的目标对象。

查看一下生成的源代码:

@Override
public void toEntity(PersonDTO personDTO, Person person) {
    if ( personDTO == null ) {
        return;
    }
    person.setId( personDTO.getId() );
    person.setName( personDTO.getName() );
}

Spring容器内使用

MapStruct通过@Mapper#componentModel可以在多种容器内使用。

非容器环境

非容器环境下,需要对Mapper实例化并取得引用,而不应在每个使用的地方重新实例化一次。

@Mapper
public interface PersonMapper {  
    // 当前PersonMapper实例对象
    PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);
    @Mapping(source = "name", target = "userName")
    PersonDTO toDTO(Person person);
}

Spring容器环境

@Mapper(componentModel="spring")
public interface PersonMapper {  
    @Mapping(source = "name", target = "userName")
    PersonDTO toDTO(Person person);
}

通过@Mapper#componentModel指定当前Mapper托管在Spring容器中,需要使用该Mapper时,通过注入的方式引入即可。

@Resource
private PersonMapper mapper;

小结

本文简单介绍MapStruct使用场景及一些日常中大概率会使用的特性。MapStruct足够“聪明”,默认行为足以解决大部分问题,特别适合存在大量映射转换的场景,如Web开发环境下。

整篇都基于几个假设:目标对象存在无参构造器;源对象存在getter方法;目标对象存在setter方法等。实际情况可能与此相反,这些在MapStruct中都提供了对应的解决方案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值