白话JavaBean高性能属性映射工具MapStruct

最近想对业务上遇到的属性/对象拷贝的代码进行优化,以前大多使用的是BeanUtils.copyProperties,然后不匹配名称的字段通过set赋值来弥补;但是,经常会出现两个bean大部分字段不匹配的情况,一大堆的set赋值真的是极其恶心。前几天在交流群里请教了几个大厂的朋友,在他们的极力推荐下得知MapStruct是一款高效易用的框架;那么就撸起袖子直接就开干了,肝出此文,并且编写了demo项目辅助理解。

MapStruct简介

what is MapStruct ?

MapStrutc是一款基于约定优于配置原则诞生的代码生成器,它旨在极大简化Java javabean类型之间的映射的实现。由于它生成代码的方式基于普通方法调用,因此具有快速、类型安全和容易理解的特点。

Why we choose MapStruct?

这是因为,应用程序的多个层之间通常需要针对不同的对象模型进行映射(例如do和dto直接映射)。

编写这种映射代码本身是一件非常乏味且容易出错的事情。而MapStruct正是为了解决这个问题而诞生的,它的设计思路就是尽可能通过自动化简化对象模型映射的复杂度。提升开发者的编码效率。减少手动映射出错的概率。

How to use MapStruct?

MapStruct是通过注解处理器,插入Java编译器, 它支持基于命令行进行构建(Maven, Gradle等等),并且原则上支持任意主流的IDE(IDEA, Eclipse等等)。

MapStruct支持使用合理的默认值, 并且支持用户对默认值进行任意的指定,这些都来自于用户的配置或指定的代码实现。

2分钟急速入门

这种工具没有太多的花样,官方文档已经很详细了,那我们就基于官方文档,扛起键盘开始干吧,mapstruct官网[1]

基于maven进行安装配置

由于MapStruct是基于JSR 269[2] 规范实现的Java注解处理器,因此它可以使用命令行或IDE进行构建,目前IDEA已然成为事实上的主流IDE, 我们就用它来尝鲜吧!

因为我只熟悉maven >.<,所以这里使用maven安装;grade, ant等其他安装方式请自行参考官网哈

首先建立一个Maven工程,引入如下依赖和配置

</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>
                        <path>
                            <groupId>org.mapstruct</groupId>
                            <artifactId>mapstruct-processor</artifactId>
                            <version>${org.mapstruct.version}</version>
                        </path>
                        <!-- other annotation processors -->
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
        </plugins>
    </build>
复制代码

MapStruct的插件下载

MapStruct贴心的提供了IDE的插件,用于快速生成javabean的映射代码。

我们可以直接在idea插件中心下载,也可使用以下方式下载后手动安装

MapStruct官网下载:MapStruct IntelliJ IDEA plugin[3]

jetbrains插件仓库下载:MapStruct support for IntelliJ IDEA[4]

编写第一个案例

配置就绪,我们开始编写第一个案例的代码吧!

首先,我们需要定义两个待映射的javabean (Car.java / CarDto.java)

public class Car {
    private String make;
    private int numberOfSeats;
    private CarType type;

    public Car(String make, int numberOfSeats, CarType type) {
        this.make = make;
        this.numberOfSeats = numberOfSeats;
        this.type = type;
    }
    // getter、setter方法省略...
}

public class CarDto {

    private String make;
    private int seatCount;
    private String type;
}

public enum CarType {
    SEDAN;
}
复制代码

接着我们需要创建一个mapper,这个是映射的重头戏:

@Mapper
public interface CarMapper {

    public static final CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );

    @Mapping(source = "numberOfSeats", target = "seatCount")
    CarDto carToCarDto(Car car);
}
复制代码

我们对代码中的重点稍加解释:

@Mapper:凡是被标注该注解的接口,都被指定了基于这个接口来进行javabean的映射,mapstruct会通过这个接口生成实现类,并在实现类的内部实现代码映射;

@Mapping:有时候对两个javabean进行映射时,难免会出现字段名称不匹配,但是我们又希望两个javabean中不同名的两个字段互相映射,这个时候就需要手动指定映射关系;如上面的代码中,我们显式指定将CarDto中的numberOfSeats映射到Car中的seatCount字段。那么当调用映射方式carToCarDto时,就能够实现将 numberOfSeats 的值copy给seatCount 的目的。

继续看我们的代码,由于我们没有使用DI框架(比如:spring 或者 Guice),因此如果此时想要获取mapper实例,则需要通过调用 org.mapstruct.factory.Mappers 类中的 getMapper 方法来实现。由于CarMapper只做javabean的映射,因此没必要每次进行javabean映射时,都去创建一个mapper实例。所以这里做成单例模式。

测试代码如下

public class MapStructTest {

    @Test
    public void shouldMapCarToDto() {
        //given
        Car car = new Car( "Morris", 5, CarType.SEDAN );

        //when
        CarDto carDto = CarMapper.INSTANCE.carToCarDto( car );

        //then
        Assert.assertNotNull(carDto);
        Assert.assertEquals("Morris", carDto.getMake());
        Assert.assertEquals(5, carDto.getSeatCount());
        Assert.assertEquals("SEDAN", carDto.getType());
    }
}
复制代码

运行结果如下:

好了,到这里基本上对如何使用MapStruct我们就有了一个直观的认识,可以看到通过MapStruct实现将car的字段内容映射到carDto中是一件很简单轻松的事情。

不过,我们好像发现一个问题,在spring的环境下mapper里面每次都创建个单例;正常来说,在spring环境下这种单例javabean完全可以交由spring容器去管理,那么我们应该怎么搞呢?

MapStruct其实针对DI注入提供了三种方式:

cdi:使用 @Inject注解注入

spring:使用 @Autowired注解注入

jsr330:使用 @Inject注解注入

简单的说,MapStruct支持我们对其进行依赖注入,由于我们企业级/互联网后端开发基本都是会依赖spring框架的,所以我们可以编写以下代码:

@Mapper(componentModel = "spring")
public interface CarMapper {

    public static final CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );

    @Mapping(source = "numberOfSeats", target = "seatCount")
    CarDto carToCarDto(Car car);
}
复制代码

这样配置的话,在其他地方想要使用CarMapper的时候,就可以通过 @Autowired注解来注入。

但是,如果每次都写 (componentModel = “spring”),通过在每个mapper上面都进行指定也挺烦的,都是重复的工作;

既然我们能想到了这个问题,MapStruct怎么可能想不到,那么MapStruct是怎么做的呢?

MapStruct在maven中有个配置选项,用于进行全局配置,让MapStruct在编译的时候按照这个全局配置生成javabean的映射代码;那么怎么配置呢?代码如下:

// pom.xml文件中
    <build>
        <plugins>
            <plugin>
                // ... 无关的配置就不贴出来了
                <configuration>
                    <compilerArgs>
                        <compilerArg>
                            -Amapstruct.defaultComponentModel=spring
                        </compilerArg>
                    </compilerArgs>
                </configuration>
            </plugin>
        </plugins>
    </build>
复制代码

完美,到这里我们就实现在项目全局使用统一的属性进行属性注入Mapper的声明, 而不用每次都手动指定 (componentModel = “spring”)了。

继续入门

为什么要继续入门呢?虽然我们已经算是入门了,而且也可以在spring环境中用起来了;但是这还远远不够,单纯的进行单层javabean之间的映射,连企业中最基本的需求都无法满足,那么我们还差哪些呢?

  • javabean的嵌套组合(就是一个javabean引用了另外一个javabean,或另一个javabean的集合)
  • 集合的映射(集合中的javabean引用了另一个javabean的集合)

如果连这两个都能够实现,那么主流的使用场景就覆盖的差不多了。因为如果javabean定义的合理,javabean之间的映射基本就以上这些情况。那么接下来我们继续:

复杂场景:javabean里嵌套了一个普通javabean

待映射的两个javabean均嵌套了其他javabean Person -> Apple / PersonDto -> AppleDto

public class Apple {

    private String color;

    public Apple(String color) {
        this.color = color;
    }

    // getter、setter省略...

    @Override
    public String toString() {
        return "Apple{" +
                "color='" + color + '\'' +
                '}';
    }
}

public class Person {

    private String name;
    private Apple apple;

    // getter、setter省略...

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", apple=" + apple +
                '}';
    }
}

public class AppleDto {

    private String color;

    public AppleDto(String color) {
        this.color = color;
    }

    // gettet、setter省略...

    @Override
    public String toString() {
        return "AppleDto{" +
                "color='" + color + '\'' +
                '}';
    }
}

public class PersonDto {

    private String name;
    private AppleDto appleDto;

    // gettet、setter省略...

    @Override
    public String toString() {
        return "PersonDto{" +
                "name='" + name + '\'' +
                ", appleDto=" + appleDto +
                '}';
    }
}
复制代码

接着我们编写一个嵌套javabean的mapper文件

@Mapper
public interface AppleMapper {
    // AppleDto -> apple
    Apple toApple(AppleDto appleDto);
    // Apple -> AppleDto
    @InheritInverseConfiguration
    AppleDto fromApple(Apple apple);
}

@Mapper(uses = { AppleMapper.class })
public interface PersonMapper {

    PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);
    // PersonDto -> Person
    @Mapping(source = "appleDto", target = "apple")
    Person toPerson(PersonDto personDto);
    // Person -> PersonDto
    @Mapping(source = "apple", target = "appleDto")
    @InheritInverseConfiguration
    PersonDto fromPerson(Person person);
}
复制代码

从以上代码可以看出,一个javabean嵌套了另一个普通javabean的关键点有3个地方:**

  1. 嵌套的普通javabean必须有自己的mapper文件,这样可以保证这个普通javabean的属性都可以被映射,甚至当出现字段不一样,还可以进行手动映射
  2. 因为Apple被嵌套在Person里面,所以在PersonMapper映射文件定义时,需要通过编写代码:@Mapper(uses = {AppleMapper.class}) 指定嵌套关系;
  3. 使用注解 @InheritInverseConfiguration,方便javabean的逆向转换。

javabean里面嵌套一个List集合
这里就不贴代码了,这种映射和上面的几乎一样;只要把Apple修改为List appleList就可以了,代码链接我会附在文章末尾。

同样的,如果javabean里面嵌套一个普通的List strings, 映射方法和上面如出一辙,具体参考文末提供的代码连接

好了,到这里算是真正入门了,对于MapStruct这个框架我们也可以真正在项目中使用起来了,但是肯定还会存在一些其他的坑点,例如:

  1. 有时候在进行javabean映射的时候,可能想对值进行一些特殊处理后再映射过去;例如日期格式转换 Date <—> String
  2. 映射的时候想要针对一些字段进行忽略,禁止映射
  3. 映射的时候,如果某个属性没值,是否可以填充默认值

除了上面提到的几个坑外,可能还存在其他的坑、以及对于使用上是否还存在更加优雅的方式、还有MapStruct更高阶的功能如何使用等等,碍于篇幅我们没能面面俱到,还是需要我们一起继续进行学习和研究。

References

[1] mapstruct官网: mapstruct.org/

[2] JSR 269: www.jcp.org/en/jsr/deta…

[3] MapStruct IntelliJ IDEA plugin: mapstruct.org/news/2017-0…

[4] MapStruct support for IntelliJ IDEA: plugins.jetbrains.com/plugin/1003…

[5] 完整demo地址: github.com/ziyuc69/adv…

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享