不为有趣之事,何遣有涯之生
不失其所者久,死而不亡者寿

工程师工具箱系列(1) mapstruct简明教程

MapStruct简明教程

芸芸众生

在Java项目开发中,不管你是采用传统的MVC分层模式,还是DDD驱动的微服务模式,都免不了在各层级之间传递对象,在这个过程中会出现许多的对象概念性名词:VO,DTO,DO,Entity,ValueObj等等。我们先不管这些对象在你们各自项目里的作用,有一个共同的工作就是完成他们之间赋值转换。

靠手动赋值来完成对象转换的人毕竟已经很稀缺了,我们一般都知道借助一些工具去简化这部分重复劳动。

目前市面上用的比较常见的可能有下面这几种:

它们之间的性能对比大致如下:

结合性能和吞吐量来看,手动写性能肯定是最高的,省去中间商赚差价嘛,但是社会有分工才能进步,整体效能才能增加,所以我们应该借助工具。

综合分析下来,MapStruct的性能和吞吐量都是最好的,毕竟实现原理上决定了一切,接下来我们就上手下MapStruct。

初窥门径

mapstruct的使用和如何把大象放进冰箱的步骤是一样的:1 引入mapstruct;2 创建转换器与转换方法;3 获取转换实例进行使用

引入POM依赖

Maven

<dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${org.mapstruct.version}</version>
</dependency>
...
<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>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${org.mapstruct.version}</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

Gradle

plugins {
    ...
    id "com.diffplug.eclipse.apt" version "3.26.0" // Only for Eclipse
}
dependencies {
    ...
    compile 'org.mapstruct:mapstruct:1.4.2.Final'
    annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'
    testAnnotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final' // if you are using mapstruct in test code
}

创建转换器与方法

创建之前你肯定已经明确了需要转换的两个类,比如下面的代码示例,是将Car对象转换成一个CarDto对象

@Mapper //指定该类为mapstruct的映射器
public interface CarMapper {
     // 通过ClassLoader加载
    CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );
     //转换方法,自动匹配名称与类型相同的字段,不同的字段需要通过Mapping注解进行指定
     // 这里就指定了将Cat对象的numberOfSeats属性转换赋值到CatDto的seatCount属性
    @Mapping(target = "seatCount", source = "numberOfSeats")
    CarDto carToCarDto(Car car);
}

进行使用

使用的时候就特别简单了,直接获取转换器的实例,调用转换方法,传入对应的参数即可

CarDto carDto = UserMapper.INSTANCE.carToCarDto(car);

好像还蛮简单的,但是实际开发的时候可没怎么简单,实际的业务和不用的开发人员有不同的习惯,有时候面临的场景就会复杂起来:

  • 字段名称相同,但是类型不同怎么处理?mapstruct会帮我们自动转换吗,它怎么知道怎么转换?
  • 对象和字符串之间转换怎么处理?实际开发
  • 列表和列表之间转换怎么处理?难道我也循环遍历吗?
  • 灵活的自定义转换怎么处理?

另外喜欢偷懒的小伙伴可能还会有一个疑问:虽然说用起来只有三步,但是每次都要为两个转换对象创建一个转换器的话,那岂不是会有很多的转换器了?

这些问题都会游刃有余小节中得到解答,该小结中利用了面向对象设计方法,省去了编写大量转换器与方法工作,并利用java8新特性方便实现灵活的自定义转换。

IDEA好基友

为了更好使用mapstruct,如果你使用的是Intellij IDEA编辑器,那么建议你安装个插件,它可以为我们提供一些遍历操作。

安装时候直接在IDEA的插件市场上搜索mapstruct,安装重启即可,插件为我们提供了一下几个便捷操作:

  • 自动填充属性与枚举常量
    自动填充属性与枚举常量

  • 点击可以直达注解使用的声明字段

  • 可以查找使用过的地方

PS:插件地址:https://plugins.jetbrains.com/plugin/10036-mapstruct-support

游刃有余

示例说明

为了更好说明示例,我们定义两个需要转换的对象类,我把它们之间字段的区别也列了出来

  • 相同字段:指的是名词和类型都相同,工具会自动转换
  • 原始类特有:指的是原始类UserE所特有的,可能有3种情况:类型一致但是名称不一致,类型不一致名称也不一致,类型不一致名称一致
  • 目标类特有:指的是目标类UserVO所特有的,它同时也对应上面原始类的三种情况

避免编写重复转换器

要避免编写重复的转换器接口,类似我们要避免编写不同类型的字段进行某种相同计算一样。很自然的就想到使用泛型来解决。

我们可以定义一个基础接口,包含了通用的映射方法,只要是字段类型相同的对象需要转换,这个基础接口就满足了,通过继承基础接口,传入具体的转换类型,无需任何实现与配置。

这里我提供了三种通用转换方法:1 单对象的转换;2 列表对象的转换;3 Stream对象转换,因为每种类型存在互相转换,所以基础接口包含了6个方法

同时,你可以把项目中约定好的一些字段约束加到其中,比如创建日期的格式等等

实现复杂灵活转换

接下来就是解决上面表格中的3种情况,它们的解决方案分别如下:

首先定义个UserMapping接口,继承BaseMapping,传入转换的类型,注意你自己规定的SOURCE和TARGET参数,不要搞混就行

@Mapper(componentModel = "spring")//spring注入方式
public interface UserMapping extends BaseMapping<UserE,UserVO>{

重载接口的方法,比如现在我们把UserE转换成为UserVO,解决类型一致,名称不一致的Mapping示例

@Mappings({
            @Mapping(source = "etest", target = "vtest"),
            @Mapping(source = "sex", target = "gender"),
    })
    @Override
    UserVO sourceToTarget(UserE var1);

去掉@Mappings,直接把多个@Mapping加在方法上面作用是相同的

那怎么解决cteateTime名称一致,类型不一致呢?

从UserE到UserVO是把时间类型转换为String类型,这是一种很常见的转换常见,注意看我们在基础接口中定义了目标字段的cteateTime时间格式,这就给工具提供了自动转换的可能性,主要给出的格式符合这个要求,那么工具会自动帮助我们完成转换

/**
     * 映射同名属性
     */
    @Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
    TARGET sourceToTarget(SOURCE var1);

最后看名称不一致,类型也不一致的字段,UserE中的字符串如何变成UserVO中的一个对象,首先容易想到的一点是我们可以通过@Mapping配置建立二者之间的转换关系,但是工具肯定不知道怎么转换了,所以还我们需要提供如何转换方法。

@Mappings({
            @Mapping(source = "etest", target = "vtest"),
            @Mapping(source = "sex", target = "gender"),
            @Mapping(source = "configE",target = "configs")
    })
    @Override
    UserVO sourceToTarget(UserE var1);

那么如何提供呢?假设我们已经写好一个转换方法,应该如何告知工具去选择使用?我相信你已经想到了,只要指定入参和出参类型,再结合mapping指定映射关系工具应该就能完成转换了。

于是我们再利用java8种接口可以使用默认方法的特性,我们直接在接口里增加

/**
     * 映射string config 到 List<UserVO.UserConfig> list的转换
     * 会被自动调用
     */
    default List<UserVO.UserConfig> strConfigToListUserConfig(String config) {
        return JSONUtil.toList(config,UserVO.UserConfig.class);
    }

这两步加起来就构成完成了类型不一致,名称不一致属性之间的转换

但是其实还是存在一个问题,如果存在多个指定转换关系,入参和出参也一致的情况,那工具就不知道具体采用哪个默认方法了。所以我们还需要知道如何完全自定义转换。
自定义一个转换类

public class CustmMapping {

    public static String convertFiled1(UserVO.UserConfig userConfig){

        return "自定义" + userConfig.getField1();
    }
}

在接口类中导入转换类(1处),在@Mapping中指定目标字段的转换类函数(2处)

@Mapper(componentModel = "spring",imports = CustmMapping.class)//1处
public interface UserMapping extends BaseMapping<UserE,UserVO>{
...
@Mapping(target = "sex", source = "gender")
@Mapping(target = "password", ignore = true)
@Mapping(target = "etest", source = "vtest") 
@Mapping(target="configE",expression="java(CustmMapping.convertFiled1(var1.getConfigs().get(0)))")//2处
@Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
@Override
UserE targetToSource(UserVO var1);
...

对应的测试代码:

@Log4j2
@DisplayName("使用MapStruct进行对象赋值转换")
public class MapStructTest {
    private static UserE userE;
    private static UserVO newUserVO;
    private static UserMapping userMapping;

    @BeforeAll
    public static void init() {
        userE = new UserE()
                .setId(100L)
                .setBirthday(LocalDate.of(1988,02,25))
                .setUsername("临江仙")
                .setCreateTime(LocalDateTime.now())
                .setSex(1)
                .setEtest(Arrays.asList("a","b","c"))
                .setConfigE("[{\"field1\":\"Test Field1\",\"field2\":500}]");

        userMapping = Mappers.getMapper(UserMapping.class);
        List<UserVO.UserConfig> userConfigs = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            UserVO.UserConfig userConfig = new UserVO.UserConfig("字段"+i, i+10);
            userConfigs.add(userConfig);
        }
        newUserVO = new UserVO()
                .setId(200L)
                .setUsername("鹊桥仙")
                .setPassword("123321")
                .setBirthday(LocalDate.of(1988,07,06))
                .setCreateTime("1988-02-25 12:00:00")
                .setGender(2)
                .setVtest(Arrays.asList("备注1","备注2","备注3"))
                .setConfigs(userConfigs);
        log.info("@BeforeAll: init()");
    }
    @DisplayName("准备好UserE和UserVO")
    @Test
    public void testHasUserEandUserVO(){
        System.out.println("准备好的userE:" + userE);
        System.out.println("准备好的newUserVO:" + newUserVO);;
    }
    @DisplayName("将UserE转换成UserVO")
    @Test
    public void testEtoVO() {
        UserVO userVO = userMapping.sourceToTarget(userE);
        System.out.println("转化后得到的userVO: " + userVO);
    }
    @DisplayName("将UserVO转换成UserE")
    @Test
    public void testVOtoE(){
        UserE userE = userMapping.targetToSource(newUserVO);
        System.out.println("转化后得到的userE:" + userE);
    }
}

测试结果:

转化后得到的userVO: UserVO(id=100, username=临江仙, password=null, gender=1, birthday=1988-02-25, createTime=2021年6月1号, vtest=[a, b, c], configs=[UserVO.UserConfig(field1=Test Field1, field2=500)])


转化后得到的userE:UserE(id=200, username=鹊桥仙, password=null, sex=2, birthday=1988-07-06, createTime=1988-02-25T12:00, etest=[备注1, 备注2, 备注3], configE=自定义字段0)

准备好的userE:UserE(id=100, username=临江仙, password=null, sex=1, birthday=1988-02-25, createTime=2021-06-03T13:22:42.203, etest=[a, b, c], configE=[{"field1":"Test Field1","field2":500}])
准备好的newUserVO:UserVO(id=200, username=鹊桥仙, password=123321, gender=2, birthday=1988-07-06, createTime=1988-02-25 12:00:00, vtest=[备注1, 备注2, 备注3], configs=[UserVO.UserConfig(field1=字段0, field2=10), UserVO.UserConfig(field1=字段1, field2=11), UserVO.UserConfig(field1=字段2, field2=12), UserVO.UserConfig(field1=字段3, field2=13), UserVO.UserConfig(field1=字段4, field2=14)])

温故知新

最后我们对mapstruct工具做个小结:

  • 核心特点 :基于 JSR 269 的 Java 注解处理器实现,用纯java方法而不是反射进行属性赋值,做到了编译时类型安全,相当于编译时的代码生成器。

  • 性能更高:使用简单的Java方法调用代替反射,无需手动 set/get 或 implements Serializable 以达到深拷贝
  • 编译时类型安全:只能映射相同名称或带映射标记的属性,编译时如果映射不完整(存在未被映射的目标属性)或映射不正确(找不到合适的映射方法或类型转换)则会在编译时抛出异常

使用技巧

  • 技巧一:定义一个公共的转换器接口,使用泛型定义好常用的方法,如果字段完全一样公共接口就满足要求了
  • 技巧二:同类型不同名称的转换直接使用Mapping在转换方法上指定
  • 技巧三:不同类型同名称的,可以使用Mapping也可以使用default方法的方式
  • 技巧四:不同类型不同名称,可以使用Mapping+default方式或自定义转换类方式

运用这些技巧你还可以实现多个bean之间映射,复杂数据结构之间映射等,充分满足多种业务场景下使用。

PS:文中源码是示例地址:https://gitee.com/hzqiuxm/middleware-projects.git [java-base模块]-[mapstruct包]

未经允许不得转载:菡萏如佳人 » 工程师工具箱系列(1)

欢迎加入极客江湖

进入江湖关于作者