为什么需要 MapStruct?

关注者
21
被浏览
40,912

8 个回答

作者发觉MapStruct确实是一个提升系统性能,降低无用代码的神器。但在实践过程中,遇到了些问题,并由此对MapStruct框架有了更深入的理解,以下作者将他的学习收获分享给大家。

(点击头像关注我们,可查看更多阿里工程师干货。)

——————————————————————————————————

MapStruct是什么?

MapStruct is a code generator that greatly simplifies the implementation of mappings between Java bean types based on a convention over configuration approach.—— mapstruct.org/ 

从官方定义来看,MapStruct类似于我们熟悉的BeanUtils, 是一个Bean的转换框架。

In contrast to other mapping frameworks MapStruct generates bean mappings at compile-time which ensures a high performance, allows for fast developer feedback and thorough error checking.—— mapstruct.org/ 

他与BeanUtils最大的不同之处在于,其并不是在程序运行过程中通过反射进行字段复制的,而是在编译期生成用于字段复制的代码(类似于Lombok生成get()和set()方法),这种特性使得该框架在运行时相比于BeanUtils有很大的性能提升。

引入

Maven

由于MapStruct和Lombok都会在编译期生成代码,如果配置不当,则会产生冲突,因此在工程中同时使用这两个包时,应该按照以下方案导入:

  • 当POM中不包含Lombok时
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.5.2.Final</version>
</dependency>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.5.2.Final</version>
</dependency>
  • 当POM中包含Lombok且不包含<annotationProcessorPaths>时
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.24</version>
</dependency>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.5.2.Final</version>
</dependency>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.5.2.Final</version>
</dependency>

注意:引入时,mapstruct-processor必须lombok后面。

  • 当POM中包含Lombok且包含<annotationProcessorPaths>时
<properties>
    <org.mapstruct.version>1.5.2.Final</org.mapstruct.version>
</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.24</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> <!-- depending on your project -->
                <target>1.8</target> <!-- depending on your project -->
                <annotationProcessorPaths>
                    <properties>
                        <org.mapstruct.version>1.5.2.Final</org.mapstruct.version>
                    </properties>
                    <dependencies>
                        <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> <!-- depending on your project -->
                                    <target>1.8</target> <!-- depending on your project -->
                                    <annotationProcessorPaths>
                                            <groupId>org.projectlombok</groupId>
                                            <artifactId>lombok</artifactId>
                                            <version>1.18.24</version>
                                        </path>
                                            <groupId>org.mapstruct</groupId>
                                            <artifactId>mapstruct-processor</artifactId>
                                            <version>${org.mapstruct.version}</version>
                                        </path>
                                        <!-- other annotation processors -->
                                    </annotationProcessorPaths>
                                </configuration>
                            </plugin>
                        </plugins>
                    </build>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${org.mapstruct.version}</version>
                    </path>
                    <!-- other annotation processors -->
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>    

Idea Plugin

搜索MapStruct Support安装即可,可以在使用MapStruct时获得更加丰富代码提示。

基础转换

字段完全一致

  • 待转换的类
@Data
@Builder
public class Source {
    private Long id;
    private Long age;
    private String userNick;
}
  • 转换目标类
@Data
public class Target {
    private Long id;
    private Long age;
    private String userNick;
}
  • 转换器

注意:Mapper是Mapstruct的注解。

@Mapper
public abstract class Converter {
    public static Converter INSTANT = Mappers.getMapper(Converter.class);
    public abstract Target convert(Source source);
}
  • 使用示例
final Source source = Source.builder()
        .id(1L)
        .age(18L)
        .userNick("Nick")
        .build();
final Target target = Converter.INSTANT.convert(source);
System.out.println(target);

输出:

Target(id=1, age=18, userNick=Nick)

一一对应的字段名不一致、类型不一致

  • 待转换的类
@Data
@Builder
public class Source {
    private Long id;
    private Long age;
    private String userNick;
}
  • 转换目标类
@Data
public class Target {
    private Long id;
    private Integer age;
    private String nick;
}

转换目标类修改了age字段的类型,和userNick字段的名字,这两个类的字段仍然是一一对应的。

  • 转换器
@Mapper
public abstract class Converter {
    public static Converter INSTANT = Mappers.getMapper(Converter.class);
    // 字段类型映射修改
    @Mapping(source = "age", target = "age", resultType = Integer.class)
    // 字段名映射修改
    @Mapping(source = "userNick", target = "nick")
    public abstract Target convert(Source source);
}
  • 使用示例
final Source source = Source.builder()
        .id(1L)
        .age(18L)
        .userNick("Nick")
        .build();
final Target target = Converter.INSTANT.convert(source);
System.out.println(target);

输出:

Target(id=1, age=18, nick=Nick)
  • 源码解析

《告别BeanUtils,Mapstruct入门到精通》

高级转换

我们在业务代码中经常会用到以下转换方法,因此将示例代码写在下面。

一对多字段互转

在业务代码中,常出现需要将一个类中的一些字段转换为另一个类的JSON字段的情况,以下是一个简单的例子:

  • 互相转换的类

VO:前端渲染内容。

@Data
@Builder
public class VO {
    private Long id;
    private Long age;
    private String userNick;
}

DTO:传输内容,其中仅包含id,其余字段均存放在extra字段中。

@Data
public class DTO {
    private Long id;
    private String extra;
}
  • 多个字段转换为一个字段

常用于将多个字段转为JSON字段,在以下示例中,为了避免引入第三方包(如FastJson),仅使用字符串拼接两个字段,Json方式同理。

@Mapper
public abstract class Converter {
    public static Converter INSTANT = Mappers.getMapper(Converter.class);
    @Mapping(target = "extra", source = "vo", qualifiedByName = "convertToExtra")
    public abstract DTO convert(VO vo);
    @Named("convertToExtra")
    public String convertToExtra(VO vo) {
        return String.format("%s,%s", vo.getAge(), vo.getUserNick());
}

将多个字段转换为一个字段,需要以下几个步骤:

  1. 创建自定义转换方法(本例为convertToExtra()):
    方法入参类型为被转换的类(本例为VO),出参为转换好的字段(本例为extra);
    为方法加上@Named注解,并自定义该方法在mapStruct中的名字(本例中为convertToExtra)。
  2. 在转换方法上增加Mapping注解,其中:
    source字段必须与转换方法入参名字相同(本例中均为vo);
    target字段为目标字段(本例中为extra);
    qualifiedByName字段为上述自定义的方法名字。
  • 将一个字段转换为多个字段

该方法常用于从JSON字段中取出数据。

原理与上述方法类似,定义两个自定义转换方法,用于转换extra字段。

@Mapper
public abstract class Converter {
    public static Converter INSTANT = Mappers.getMapper(Converter.class);
    @Mapping(target = "age", source = "extra", qualifiedByName = "extractAge")
    @Mapping(target = "userNick", source = "extra", qualifiedByName = "extractUserNick")
    public abstract VO convertToVO(DTO dto);
    @Named("extractAge")
    public Long extractAge(String extra) {
        // 从extra中提取第一个值
        return Long.valueOf(extra.split(",")[0]);
    @Named("extractUserNick")
    public String extractUserNick(String extra) {
        // 从extra中提取第二个值
        return extra.split(",")[1];
}
  • 使用示例
final VO vo = VO.builder()
        .id(1L)
        .age(18L)
        .userNick("Nick")
        .build();
// 转为DTO
final DTO dto = Converter.INSTANT.convertToDTO(vo);
System.out.println(dto);
// 转回VO
final VO newVo = Converter.INSTANT.convertToVO(dto);
System.out.println(newVo);

输出:

DTO(id=1, extra=18,Nick)
VO(id=1, age=18, userNick=18)
  • 为转换加缓存

在上述的两个方法(extractAge和extractUserNick)中,进行了重复的String#split()操作,如果该操作更加复杂(如从JSON串中提取内容),则会造成资源的浪费。

为此,可以给当前的converter加一个缓存字段extraFieldBufferLocal,如下例所示。在例子中,每次解析extra字段前,先判断buffer是否存在,如果存在则使用缓存内容。

:Mapstruct中使用xxx.INSTANT获得的转换器是 单例 的,因此,如果要在多线程环境中转换时加入缓存,其缓存必须声明为ThreadLocal类型。

@Mapper
public abstract class Converter { 
    public static Converter INSTANT = Mappers.getMapper(Converter.class);
     * extra字段解析后的buffer,避免多次重复解析
    private final ThreadLocal<String[]> extraFieldBufferLocal = new ThreadLocal<>();
    @Mapping(target = "age", source = "extra", qualifiedByName = "extractAge")
    @Mapping(target = "userNick", source = "extra", qualifiedByName = "extractUserNick")
    public abstract VO convertToVO(DTO dto);
    @Named("extractAge")
    public Long extractAge(String extra) {
        if (extraFieldBufferLocal.get() == null) {
            extraFieldBufferLocal.set(extractExtraField(extra));
        return Long.valueOf(extraFieldBufferLocal.get()[0]);
    @Named("extractUserNick")
    public String extractUserNick(String extra) {
        if (extraFieldBufferLocal.get() == null) {
            extraFieldBufferLocal.set(extractExtraField(extra));
        return extraFieldBufferLocal.get()[1];
     * 提取extra字段
     * @param extra extra字段
     * @return extra字段的提取结果
    public String[] extractExtraField(final String extra) {
        return extra.split(",");
}

子类字段互转

常用于平铺类和嵌套类之间的转换,例如,前端需要将类中的所有字段打平,就可以参考以下示例代码。

  • 互相转换的类

VO:

@Data
@Builder
public class VO {
    private Long id;
    private Date gmtCreate;
    private Long age;
    private String userNick;
}

DTO:

@Data
public class DTO {
    private Long id;
    private Date gmtCreate;
    private Config config;
    @Data
    public static class Config{
        private String age;
        private String userNick;
}

在DTO中,VO的age和userNick字段被放到了子类Config中。此时也可以使用上一节展示的自定义转换函数法进行转换,不过MapStruct提供了一种更加直观简单的转换方法:

  • 子类字段互转
@Mapper
public abstract class Converter {
    public static Converter INSTANT = Mappers.getMapper(Converter.class);
    @Mapping(target = "config.age", source = "age")
    @Mapping(target = "config.userNick", source = "userNick")
    abstract DTO convertToDTO(VO source);
    @Mapping(target = "age", source = "config.age")
    @Mapping(target = "userNick", source = "config.userNick")
    abstract VO convertToVO(DTO dto);
}
  • 使用示例
final VO vo = VO.builder()
        .id(1L)
        .age(10L)
        .gmtCreate(new Date())
        .userNick("nick")
        .build();
final DTO dto = Converter.INSTANT.convertToDTO(vo);
System.out.println(dto);
final VO newVo = Converter.INSTANT.convertToVO(dto);
System.out.println(newVo);

输出:

DTO(id=1, gmtCreate=Fri Sep 16 00:09:05 CST 2022, config=DTO.Config(age=10, userNick=nick))
VO(id=1, gmtCreate=Fri Sep 16 00:09:05 CST 2022, age=10, userNick=nick)

利用Spring进行依赖注入

本文以上示例代码中,都是使用Converter.INSTANT来获得Convert实例,这在业务代码中可能显得有些突兀,而MapStruct提供了依赖注入的机制,让我们能够在Spring的环境下,更优雅的获得Converter,以下是一个例子:

  • 转换器

该转换器转换字段完全相同的两个类:

@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public abstract class Converter {
    public abstract Target convert(Source source);
}

与原有Converter相比,我们删除了丑陋的INSTANT声明:

public static Converter INSTANT = Mappers.getMapper(Converter.class);

并修改了Mapper注解为:

@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
  • 使用示例

在Spring上下文中,可以直接使用依赖注入注解(Autowired、Resource)获得对应的Converter

@Controller
public class MainController {
    @Resource
    private Converter convert;
    @GetMapping("/")
    @ResponseBody
    public boolean test() {
        final Source source = Source.builder()
                .id(1L)
                .age(18L)
                .userNick("nick")
                .build();
        final Target result = convert.convert(source);
        System.out.println(result);
        return true;
}

输出:

Target(id=1, age=18, userNick=Nick)

探究引入顺序

本文在第一章提到,引入MapStruct时,必须要注意Lombok包与MapStruct包的顺序,关于这一点,网上很少有相关文章提及。

问题来源

在复现 《告别BeanUtils,Mapstruct入门到精通》 代码时,文中提到的引入顺序是这样的:

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.5.0.Final</version>
</dependency>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.5.0.Final</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.12</version>
</dependency>

实践发现,在一个空工程中,如果按照上述写法引入MapStruct,其并不能正常工作。

而当修改引入顺序为以下方案时,则MapStruct可以正常使用。

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.12</version>
</dependency>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.5.0.Final</version>
</dependency>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.5.0.Final</version>
</dependency>

进一步测试发现, 只有mapstruct-processor包在lombok包下面时,MapStruct才能够正常使用。

MapStruct基本原理

为了探究上述问题产生的原因,我们首先要理解MapStruct的基本原理。

MapStruct与其他Bean映射库最大的不同就是,其在编译期间生成转换代码,而不是在运行时通过反射生成代码。

为了更直观的理解这一点,可以从target中找到MapStruct自动生成的对应的ConveterImpl类,如下图所示:

即MapStruct为我们编写的Convert抽象类自动生成了一个实现。

而Lombok也是在编译时自动生成代码,那么问题大概率就出现在这里了。

MapStruct是如何与Lombok共存的?

查阅MapStruct官方文档可以发现这样一段内容:

其中提到,MapStruct的annotation processor必须在Lombok的annotation processor生成完代码之后,才可以正常运行。

所以,这应该就是在导入dependencies时,必须先导入Lombok包,再导入MapStruct-processor包才可以正常运行的原因了。不过还有个问题没有解决:

Maven到底在哪里规定了 annotation processor 的载入顺序?难道每次创建工程时,必须记住这些包导入顺序么?

MapStruct官方推荐的导入流程

在进一步查看MapStruct官网时发现,其并没有将MapStruct-processor放在dependencies中,而是放在了annotationProcessorPaths层级下:

 mapstruct.org/documenta

...
<properties>
    <org.mapstruct.version>1.5.2.Final</org.mapstruct.version>
</properties>
<dependencies>
    <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> <!-- depending on your project -->
                <target>1.8</target> <!-- depending on your project -->
                <annotationProcessorPaths>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${org.mapstruct.version}</version>
                    </path>
                    <!-- other annotation processors -->
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

这又是为什么呢?

查阅Maven官方文档,对于<annotationProcessorPaths>有这样一段描述:If specified, the compiler will detect annotation processors only in those classpath elements. If omitted, the default classpath is used to detect annotation processors. The detection itself depends on the configuration ofannotationProcessors.即如果有<annotationProcessorPaths>层级,则使用这个层级声明注解处理器的顺序执行,如果没有,则按照默认classpath的顺序来使用注解处理器。

<annotationProcessorPaths>地址: maven.apache.org/plugin
<annotationProcessorPaths>地址: maven.apache.org/plugin

我们接下来以下命令来获取当前Maven项目中的classpath:

mvn dependency:build-classpath -Dmdep.outputFile=classPath.txt

从导出内容可以看出,classPath中的Jar包顺序就是与dependencies中导入的顺序是相同的。

自此,关于MapStruct导入顺序的所有问题均已经被解决,总结如下:

  1. 在POM中没有annotationProcessorPaths时,Maven使用的classPath作为注解处理器执行的顺序,而classPath的顺序正是dependencies中导入的顺序。
  2. 当MapStruct依赖在Lombok依赖前面时,在执行注解处理器期间, 由于Lombok还未生成get、set代码,因此在MapStruct看来,这些类并没有公开的成员变量,也就无从生成用于转换的方法。
  3. 在使用annotationProcessorPaths后,其强制规定了注解处理器的顺序,dependencies中的顺序就被忽略了,Maven一定会先运行Lombok再运行MapStruct,代码即可正常运行。

(本文作者:寥天)
——————————————————————————————————————————
阿里巴巴集团 大淘宝技术官方账号。 点击下方主页关注我们,你将收获更多来自阿里一线工程师的技术实战技巧&成长经历心得。另,不定期更新最新岗位 招聘信息 和简历内推通道,欢迎各位以最短路径加入我们。

[版权申明]非商业目的注明出处可自由转载
出自:shusheng007


概述

由于现代程序在追求扩展和维护性时很多采用分层的设计结构,所以在写程序时候需要在各种实体之间互相转换,而他们之间很多时候在业务或者技术架构上区别较大,在具体的属性上差别却很小。

例如将 Programer 转换为 ProgramerDto 就很普遍,如下所示:

public class Programer {
    private String name;
    private String proLang;
}

转换为:

public class ProgramerDto {
    private String name;
    private String proLang;
}

由于这些是繁琐易错且没有技术含量的编码工作,所以聪明的程序员就会寻求不断简化它的方法,MapStruct就是其中的一个利器。

MapStruct

简介:

MapStruct is a Java annotation processor for the generation of type-safe and performant mappers for Java bean classes

大意就是: MapStruct 是一个用于Java的Bean的映射器,是它是基于注解的,而且是编译时APT(annotation processor tool)。不像其他APT是运行时,例如Spring里面的注解处理方式,是在运行时通过反射的方式处理的。

详细介绍可以到其官网查看: MapStruct源码 ,下面是官方给出的选择MapStruc的理由,你看看是否说服了你去使用它:

  • Fast execution by using plain method invocations instead of reflection
  • Compile-time type safety . Only objects and attributes mapping to each other can be mapped, so there's no accidental mapping of an order entity into a customer DTO, etc.
  • Self-contained code —no runtime dependencies
  • Clear error reports at build time if:
  • mappings are incomplete (not all target properties are mapped)
  • mappings are incorrect (cannot find a proper mapping method or type conversion)
  • Easily debuggable mapping code (or editable by hand—e.g. in case of a bug in the generator)

使用

如何配置

从前面的介绍我们得知,MapStruct是通过在编译时通过注解来生成代码的方式工作的,所以需要配置APT。此处我们还想使用lombok,所以也会顺便配置其与lombok结合的配置。

<properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <mapstruct.version>1.5.3.Final</mapstruct.version>
        <lombok.version>1.18.20</lombok.version>
        <lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
            <version>${mapstruct.version}</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.8.1</version>
                    <configuration>
                        <source>${maven.compiler.source}</source>
                        <target>${maven.compiler.target}</target>
                        <annotationProcessorPaths>
                                <groupId>org.mapstruct</groupId>
                                <artifactId>mapstruct-processor</artifactId>
                                <version>${mapstruct.version}</version>
                            </path>
                                <groupId>org.projectlombok</groupId>
                                <artifactId>lombok</artifactId>
                                <version>${lombok.version}</version>
                            </path>
                                <groupId>org.projectlombok</groupId>
                                <artifactId>lombok-mapstruct-binding</artifactId>
                                <version>${lombok-mapstruct-binding.version}</version>
                            </path>
                        </annotationProcessorPaths>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>

如上所示,主要配置了注解处理器: <annotationProcessorPaths> 。如果不使用lombok的话,去掉相应的配置即可。

应用

当完成了配置就就需要写代码了,主要是一些注解的使用,MapStruc提供的功能是很强大的,但是入门很容易的。

假设我们有如下两个需要转换的类:

@Data
public class Programer {
    private String name;
    private String lang;
    private Double height;
    private Date beDate;
    private Address address;
    private String girlName;
    private String girlDes;
@Data
public class ProgramerDto {
    private String name;
    private String proLang;
    private String height;
    private String beDate;
    private AddressDto address;
    private GirlFriendDto girlFriend;
}

第一步 : 定义一个 interface ,使用 @Mapper 标记

@Mapper
public interface ProgramerConvetor {
}

第二步 :构建一个实例属性用于访问里面的方法。

@Mapper
public interface ProgramerConvetor {
    ProgramerConvetor INSTANCE = Mappers.getMapper(ProgramerConvetor.class);
}

第三步 :提供转换方法申明,必要时使用 @Mapping 注解

@Mapper
public interface ProgramerConvetor {
    ProgramerConvetor INSTANCE = Mappers.getMapper(ProgramerConvetor.class);
    @Mapping(target = "lang", source = "proLang")
    ProgramerDto toProgramerDto(Programer programer);
}

MapStruc默认会将两个bean的名称相同的属性进行映射,如果source与target的属性名称不一致则需要借助 @Mapping 注解。

简单的转换就只需要以上3步就可以了,编译程序后就会在 \target\generated-sources\annotations 下产生实现类了。

下面的代码是MapStruc自动生成的:

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2023-01-08T16:51:05+0800",
    comments = "version: 1.5.3.Final, compiler: javac, environment: Java 11.0.16.1 (Oracle Corporation)"
public class ProgramerConvetorImpl implements ProgramerConvetor {
    @Override
    public ProgramerDto toProgramerDto(Programer programer) {
        if ( programer == null ) {
            return null;
        ProgramerDto programerDto = new ProgramerDto();
        programerDto.setLang( programer.getProLang() );
        programerDto.setName( programer.getName() );
        return programerDto;
}

是不是和你手写的也差不多,那有了生成类我们就可以在代码中使用了:

public void runMap(){
    Programer programer = new Programer();
    programer.setName("shusheng007");
    ProgramerDto programerDto = ProgramerConvetor.INSTANCE.toProgramerDto(programer);
    log.info("dto: {}",programerDto);
}

可见,可以通过转换器接口里面的那个 INSTANCE 实例属性来调用其方法。看是不是比你手写方便多了呢?特别是属性比较多,而其名称又有很多一致的情况下就更方便了。

进阶

前面那个是最基础的使用,MapStruc提供了非常灵活的映射方式,要完全掌握既没有必要又是不可能的,下面我们挑几个常用的以应对 80% 的日常工作。

装插件

工欲善其事必先利其器,咱给Idea装上一个插件 MapStruct Support ,各种代码智能提示走起来...

调用方式

前面我们使用在接口中定义一个实例属性的方式来访问生成的方法,这有点不Spring,在Spring中我们习惯将bean交给Spring容器管理,MapSturc也支持。

@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface ProgramerConvetor {
}

通过修改 @Mapper componentModel 的属性为spring即可。下面是生成的代码,发现已经添加了 @Component 注解。

...
@Component
public class ProgramerConvetorImpl implements ProgramerConveto{
}

自定义映射

当source与target里的属性名称不一致时需要显示指定映射关系

@Mapping(target = "lang", source = "proLang")
ProgramerDto toProgramerDto(Programer programer);

生成代码:

ProgramerDto programerDto = new ProgramerDto();
programerDto.setLang( programer.getProLang() );

忽略映射

如果不想给 ProgramerDto proLang 赋值可以忽略它。

@Mapping(target = "proLang", ignore = true)
ProgramerDto toProgramerDto(Programer programer);

设置默认值

如果想实现在source值为null时给一个默认值也是可以了。

@Mapping(target = "proLang", defaultValue = "java")
ProgramerDto toProgramerDto(Programer programer);

生成代码:

ProgramerDto programerDto = new ProgramerDto();
if ( programer.getProLang() != null ) {
    programerDto.setProLang( programer.getProLang() );
else {
    programerDto.setProLang( "java" );
}

其实默认值不仅可以是一个具体的值,还可以是一个表达式,表达式一会我们再说。

设置常量

给source的某个属性赋值为常量

@Mapping(target = "proLang", constant = "kotlin")
ProgramerDto toProgramerDto(Programer programer);

生成代码:

programerDto.setProLang( "kotlin" );

数据类型转换

我们在进行bean映射的时候,有时会遇到数据类型不一致的情况。例如对于一个日期,source的数据类型是 Date ,而target的数据类型是 String ,这些情况怎么处理呢?

public class Programer {
    private Double height;
    private Date beDate;
public class ProgramerDto {
    private String height;
    private String beDate;
}

从上面的代码可以看到,我们的两个bean的数据类型是不一致的,但是MapStruct却可以帮我们自动转换

@Mapping(target = "height", source = "height")
ProgramerDto toProgramerDto(Programer programer);

生成的代码:

if ( programer.getHeight() != null ) {
    programerDto.setHeight( String.valueOf( programer.getHeight() ) );
}

生成的代码已经将 Double 帮我们转换成 String 了。不仅如此,我们还可以对生成的字符串的格式进行设置,例如将身高数据保留两位小数

@Mapping(target = "height", source = "height" ,numberFormat = "#.00")

生成的代码:

if ( programer.getHeight() != null ) {
    programerDto.setHeight( new DecimalFormat( "#.00" ).format( programer.getHeight() ) );
}

下面看一个日期相关的转换

@Mapping(target = "beDate", dateFormat = "yyyy-MM-dd HH:mm:ss")
ProgramerDto toProgramerDto(Programer programer);

生成的代码:

if ( programer.getBeDate() != null ) {
    programerDto.setBeDate( new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss" ).format( programer.getBeDate() ) );
}

可见在日期转换中我们可以控制器转换后的格式。

表达式

这个就比较厉害了,你就认为是方法调用即可,我喜欢。有时我们会遇到在做映射的时候不是简单的赋值,而是要进行计算,那这个功能就可以使用表达式来完成。

例如我们要实现将程序员名称变成大写个功能,就可以使用 expression 这个属性进行配置。表达式的形式如下: java(代码调用)

@Mapping(target = "name", expression = "java(programer.getName().toUpperCase())")
ProgramerDto toProgramerDto(Programer programer);

生成代码:

programerDto.setName( programer.getName().toUpperCase() );

表达式里可以进行方法调用,例如上面的代码我们可以换一种方式写,将转换代码写成一个default函数。

注意这个defalut方法的签名一定要符合你的需求,因为MS会为每一个映射尝试这个方法,一旦符合了就会被使用,例如你写一个String 到 String的转换那就麻烦了,每个符合这个得属性转换都会用上...

@Mapping(target = "name", expression = "java(nameToUp(programer))")
ProgramerDto toProgramerDto(Programer programer);
default String nameToUp(Programer programer){
    return Optional.ofNullable(programer)
            .filter(Objects::nonNull)
            .map(p->p.getName())
            .orElse(null)
            .toUpperCase();
}

嵌套映射

我们经常会遇到bean里面套着bean的映射。

{
    "name":"shusheng007",
    "address":{
        "country":"China",
        "city":"TianJin"
}

对于这样的映射,我们只需要在mapper中提供一个嵌套bean的转换关系即可。

@Mapping(target = "address", source = "address")
ProgramerDto toProgramerDto(Programer programer);
//嵌套bean的转换关系
AddressDto toAddressDto(Address addr);

生成代码:

programerDto.setAddress( toAddressDto( programer.getAddress() ) );
@Override
public AddressDto toAddressDto(Address addr) {
    if ( addr == null ) {
        return null;
    AddressDto addressDto = new AddressDto();
    addressDto.setCountry( addr.getCountry() );
    addressDto.setCity( addr.getCity() );
    return addressDto;
}

其实MapStruct非常智能的,即使你不提供它也会尝试进行映射的。

集合映射

只需要提供集合元素类型的映射即可。

AddressDto toAddressDto(Address addr);
List<AddressDto> toAddressList(List<Address> addrList);

生成代码:

@Override
public AddressDto toAddressDto(Address addr) {
@Override
public List<AddressDto> toAddressList(List<Address> addrList) {
    if ( addrList == null ) {
        return null;
    List<AddressDto> list = new ArrayList<AddressDto>( addrList.size() );
    for ( Address address : addrList ) {
        list.add( toAddressDto( address ) );
    return list;
}

外部引用

上面我们介绍了表达式,通过它我们可以写代码逻辑,但是当转换关系需要调用外部类的方法时怎么办呢?我们有两种方法,下面看一下。

例如我们有如下要被引用的类:

@Component
public class GirlFriendMapper {
    public GirlFriendDto toGirlFriendDto(Programer programer) {
        GirlFriendDto girlFriendDto = new GirlFriendDto();
        girlFriendDto.setName(programer.getName());
        girlFriendDto.setDescription(programer.getGirlDes());
        return girlFriendDto;
}
  • 使用抽象类代替接口来做Mapper
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public abstract class ClzProgramerConvertor {
    @Autowired
    protected GirlFriendMapper girlFriendMapper;
    @Mapping(target = "girlFriend", expression = "java(girlFriendMapper.toGirlFriendDto(programer))")
    public abstract ProgramerDto toProgramerDto(Programer programer);
}

使用了抽象类后,你发现熟悉的味道回来了,可以使用 @Autowired 随便往里面注入实例了,然后在 expression 里面调用就好了,是不是很爽?

生成代码:

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2023-01-08T21:03:05+0800",
    comments = "version: 1.5.3.Final, compiler: javac, environment: Java 11.0.16.1 (Oracle Corporation)"
@Component
public class ClzProgramerConvertorImpl extends ClzProgramerConvertor {
    @Override
    public ProgramerDto toProgramerDto(Programer programer) {
        ProgramerDto programerDto = new ProgramerDto();
        programerDto.setGirlFriend( girlFriendMapper.toGirlFriendDto(programer) );
        return programerDto;
    protected AddressDto addressToAddressDto(Address address) {
}

那个 girlFriendMapper 就是我们在父类中注入的。

  • 使用 @Mapper 注解的import和use属性

import 属性就和java中的 import 是一样的,导入后在expression中就可以不使用类的全限定名称了。例如你的转换用到了一个静态工具类,那么如果不在import中导入此工具类,那么使用的时候就要全限定名了。

@Mapping(target = "name", expression = "java(top.ss007.Util.toUpper(programer.getName()))")

当使用的映射方法在其他非静态类里时,就可以使用 use 属性。

@Mapper(componentModel = MappingConstants.ComponentModel.SPRING,
        uses = {
            GirlFriendMapper.class
public interface ProgramerConvetor {
    @Mapping(target = "girlFriend", source = "programer")
    ProgramerDto toProgramerDto(Programer programer);
}

我们使用 @Mapper use 属性将GirlFriendMapper引入进来。

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2023-01-08T21:03:17+0800",
    comments = "version: 1.5.3.Final, compiler: javac, environment: Java 11.0.16.1 (Oracle Corporation)"
@Component
public class ProgramerConvetorImpl implements ProgramerConvetor {
     @Autowired
    private GirlFriendMapper girlFriendMapper;
    @Override
    public ProgramerDto toProgramerDto(Programer programer) {
        ProgramerDto programerDto = new ProgramerDto();
        programerDto.setGirlFriend( girlFriendMapper.toGirlFriendDto( programer ) );
        return programerDto;
 }

可见,引入的类的实例在实现类中被注入了。我们还可以通过 injectionStrategy = InjectionStrategy.CONSTRUCTOR 指定通过构造函数来注入实例,如果不指定默认使用属性注入。

多个数据源

有时我们会遇到多个bean转一个bean的情况,需显示指定参数名称

@Mapping(target = "name", source = "programer.name")
@Mapping(target = "girlFriendName", source = "girl.name")
ProgramerDto toProgramerDto(Programer programer, Gir girl);

切面操作

MapStruct 还提供了两个注解 @BeforeMapping, @AfterMapping 用来实现在mapping前后的统一操作,这一般比较少用,但是在使用多态的时候还是很有作用的。

需求 :我们有一个Human父类,有男人和女人两个子类,然后我们要将这两个子类型mapping成HumanDto。HumanDto中有个性别的属性,需要根据具体的类型决定。在mapping完成后,我们还要将名称修饰一下。

public class Human {
    private String name;
public class Man extends Human{
public class Woman extends Human{
public class HumanDto {
    private String name;
    private GenderType genderType;
public enum GenderType {
    MAN,WOMAN
}

当然我们可以写两个转换方法即可,一个Man到HumanDto,一个Woman到HumanDto,但是在使用的时候就比较麻烦了,需要传入具体的类型,代码也有重复。这种场景下我们就可以使用这两个注解完美的解决问题。

@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public abstract class HumanConvertor {
    @BeforeMapping
    protected void humanDtoWithGender(Human human, @MappingTarget HumanDto humanDto) {
        if (human instanceof Man) {
            humanDto.setGenderType(GenderType.MAN);
        } else if (human instanceof Woman) {
            humanDto.setGenderType(GenderType.WOMAN);
    @AfterMapping
    protected void decorateName(@MappingTarget HumanDto humanDto) {
        humanDto.setName(String.format("【%s】", humanDto.getName()));
    public abstract HumanDto toHumanDto(Human human);
}

生成的代码:

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2023-01-09T23:45:44+0800",
    comments = "version: 1.5.3.Final, compiler: javac, environment: Java 11.0.16.1 (Oracle Corporation)"
@Component
public class HumanConvertorImpl extends HumanConvertor {
    @Override
    public HumanDto toHumanDto(Human human) {
        HumanDto humanDto = new HumanDto();
        //mapping前执行
        humanDtoWithGender( human, humanDto );
        humanDto.setName( human.getName() );
        //mapping后执行
        decorateName( humanDto );
        return humanDto;
}

使用:

@Autowired
private HumanConvertor humanConvertor;
public void runHumanDemo(){
    Human man = new Man();
    man.setName("王二狗");