前言

之前对于 MapStruct 没什么印象,MapStruct 一般会跟着 Mybatis 一起引入到项目中,猜测是一个跟 Mybatis 搭配使用的组件。

这次遇到比较一个棘手的情况,领导要把几家 GPS 供应商的 API 统一到一个项目中进行管理,包括车辆、设备(GPS)以及设备产生的历史轨迹和报警信息(设置电子围栏后会产生报警信息)。后面有可能的话,还要把设备本身集成到项目中统一管理。

这有点类似于之前的共享充电宝项目,接了 4、5 家厂商的设备,然后自己搭建的物联网系统(Netty),把厂商一个一个的接入到系统中,只不过目前只有包装后 API,而非直接的设备通讯协议。之前 PHP 项目(业务端)没有任何问题,因为每一个设备上报的报文消息都经过了物联网系统,之后给出的是统一的 API 接口。物联网系统接入设备因为各厂商字段不一样,所以除了上发报文和下发指令表,主要的设备、电池表因为字段个有不同,都是设计成不同的表进行存储的。

不同的厂商数据存储到不同的表中没有问题,但如果要统一到一个表,就需要将字段统一。就是将作用相同但名称不同的字段赋值到统一的表字段中,相同的字段名可以用 BeanUtils.copyProperties();,但这种不同名称的字段如何映射赋值还不太清楚。但维护的项目中有看到 MapStruct 的使用方式,大为震惊:

@Mapper
public interface DataConvert {

    DataConvert INSTANCE = Mappers.getMapper(DataConvert.class);

    @Mapping(source = "xx", target = "yy")
    A bToA(B b);
...

}
/** 实际使用 */
A a = DataConvert.INSTANCE.BToA(b);

没有任何的实现,就定义了一个接口,就能实现简单、乃至不同类型的复杂数据类型转化。

之后熟悉了 MapStruct 才明白,MapStruct 与 Lombok 有相似的用法,MapStruct 在编译运行时会生成接口对应的实现类,而 Lombok 也是在编译运行时才会给标注了 @Data 的 Bean 生成需要的 Getter 、Setter 和 toSting 等方法。这样一方面代码整洁度提高了,修改起来也方便。

MapStruct 1.5.5.Final 引入

MapStruct 1.5.5.Final 参考指南 - 官方文档

...
<properties>
    <org.mapstruct.version>1.5.5.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>
                <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>
...

如果系统中还集成了 Lombok ,并且引入 MapStruct 之后如果 @Data 注解的 Bean 报各种错误的话,可以通过将 Lombok 配置也添加到 build 插件配置中来解决:


...

           <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                    <encoding>${project.build.sourceEncoding}</encoding>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                            <version>${lombok.version}</version>
                            <scope>provided</scope>
                        </path>
                        <path>
                            <groupId>org.mapstruct</groupId>
                            <artifactId>mapstruct-processor</artifactId>
                            <version>${mapstruct.version}</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>

...

看 xml 元素名称是配置注解处理器的路径

我使用了若依框架作为初始开发框架,前后端分离版,所以不仅在 ruoyi 总的项目 pom.xml 中要加,还要将这些依赖和配置加到 admin 使用模块中。

MapStruct 学习“三板斧”

前言中的例子就是 MapStruct 最简单的使用方法,因为有一个 数据类型转换 规则。MapStruct 会自动将 @Mapping 定义的 source 源和 target目标结合起来,只要相同或者相近类型,不需要额外处理,就可以直接转化。

其他的复杂情况就需要增加一些操作。

List 列表(不同类型存储数据)转化

定义两个方法,一个 List 层级的,一个存储数据类型 Bean 的:

List<A> bToAList(List<B> bList);

@Mapping(source = "xx", target = "yy")
A bToA(B b);

不同字段类型添加映射处理

使用表达式

表达式 - 高级映射选项

@Mapper
public interface SourceTargetMapper {

    SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );

    @Mapping(target = "timeAndFormat",
         expression = "java( new org.sample.TimeAndFormat( s.getTime(), s.getFormat() ) )")
    Target sourceToTarget(Source s);
}

这里 java() 相当于 eval() 直接执行 java 表达式,不会检查使用的类是否引入,是否存在,存在异常报错或者未知风险。

default 自定义方法

向映射器添加自定义方法 - 定义映射器

比如需要将 Boolen 类型转化为 Integer(true or false => 1,0):

 default Integer boolToInt(Boolen boo) {
    return boo ? 1 : 0;
}

但是也会有一个问题,就是它会把所有输入输出类型一致的对应字段都是用这个方法进行转化,可能会导致数据转化不正确。

根据限定符选择映射方法(也是我主要使用的方法)

根据限定符选择映射方法 - 数据类型转化

可以重新定义个单独的类,里面每个方法专门负责单个不同类型的数据转化。相同数据类型(输入输出),可以定义不同限定符名称,来可选地,完全自定义处理。

@Named("TitleTranslator")
public class Titles {

    @Named("EnglishToGerman")
    public String translateTitleEG(String title) {
        // some mapping logic
    }

    @Named("GermanToEnglish")
    public String translateTitleGE(String title) {
        // some mapping logic
    }
}

@Mapper( uses = Titles.class )
public interface MovieMapper {

     @Mapping( target = "title", qualifiedByName = { "TitleTranslator", "EnglishToGerman" } )
     GermanRelease toGerman( OriginalRelease movies );

}

限定符说明 qualifiedByName 属性需要添加自定义类和方法上的两个限定符名称。

多震源参数映射方法

多震源参数映射方法 - 定义映射器

就是可以输入多个参数,实现输出一个参数。

@Mapper
public interface AddressMapper {

    @Mapping(target = "description", source = "person.description")
    @Mapping(target = "houseNumber", source = "address.houseNo")
    DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Address address);
}

多个输入参数的时候,source 中使用 Bean 中的字段需要标注来源哪个 Bean 的,就是 bean.xx,这样可以把额外的数据带入到映射当中。

定义默认值可以使用 @Mapping 中的 defaultValue 属性。

暂时完结

以上很多的概念和名称来源于谷歌翻译,不保证确定精准。

费了老大的劲,从文档里找到自己需要的东西,后面如遇到解决不了的问题,再去深挖。