性能高功能齐全的java bean映射工具mapstruct

0 前言

业务变的越来越庞大复杂后,整个业务也被划分为很多层级功能,各层级功能各司其职,共同实现业务目标。代表各层级的数据对象如PO、DAO、DTO、VO、BO、QO等在这些层级间传递、转换、提取、组合和维护管理。所以这些数据对象之间的映射在业务主干数据流程中出现的非常频繁,也出现了非常多的对象映射组件,如apache BeanUtils、spring BeanUtils、CGLIB BeanCopier、Dozer、Orika、ModelMapper、JMapper、Selma等。

mapstruct由于基于注解和注解处理器在编译期自动生成java代码文件,编译后直接调用java bean的getter()setter()方法,实现bean的字段提取和赋值操作。跟其他bean mapping组件相比,mapstruct无需进行反射、运行时生成字节码等操作,效果上相当于手工编写代码,只是mapstruct把代码编码过程自动化了。所以mapstruct在性能上也基本相当于人工编写代码(只细微低于),但是节省了工程师大量的时间,也避免了低级错误发生的概率。由于mapstruct在编译期生成代码文件,我们甚至可以对生成的文件进一步进行手工修改,以追求极致预期,但是我没看到需要这样做的场景。同时mapstruct考虑各种应用场景,提供了非常丰富的操作。

1 基本功能介绍

1.1 package依赖

要使用mapstruct,需要引入它的package依赖:

    <!-- https://mvnrepository.com/artifact/org.mapstruct/mapstruct -->
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>1.4.2.Final</version>
    </dependency>

同时,由于mapstruct是基于注解处理器在编译期自动生成代码的,除了引入mapstruct的基本功能包依赖,还需要配置相应的compiler注解处理器。

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                            <version>1.18.22</version>
                        </path>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok-mapstruct-binding</artifactId>
                            <version>0.2.0</version>
                        </path>
                        <path>
                            <groupId>org.mapstruct</groupId>
                            <artifactId>mapstruct-processor</artifactId>
                            <version>1.4.2.Final</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
        </plugins>
    </build>

由于项目中使用lombok,lombok也是基于注解处理器在编译期自动生成class文件字节码的,所以同时配置了lombok和lombok-mapstruct-binding。

PS:

  1. 据个人一点经验,如果修改了java bean或mapstruct的mapper文件内容,再次编译前一定要先执行mvn clean将之前的package内容清除掉,否则会编译报错或生成的java文件功能不符合预期。
  2. java注解处理器的使用有点麻烦,注解处理器功能也是我们编写的代码,需要先编译成字节码再处理注解,但是编译时处理注解又要能找到注解处理器的字节码,是一个鸡生蛋、蛋生鸡的问题。如果项目中有多个注解处理器,它们之间可能会相互影响,比如项目中同时有lombok和mapstruct,一定要配置一个lombok-mapstruct-binding。如果项目中还有其他的使用了注解处理器的组件,需要特别留意这类问题。
  3. 看到网上有人介绍踩的坑,说lombok的注解处理器配置需要写在mapstruct之前,虽然看不出其中的逻辑,大家如果遇到一些奇怪的问题,也可以参考一下

1.2 一个简单的例子

先定义一个SrcObj和DstObj类:

package com.javatest.mapstruct;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class SrcObj {

    private String name;
    private int age;
    private double x;
    private String y;

}
package com.javatest.mapstruct;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class DstObj {
    private String name;
    private Integer age;
    private String x;
    private Double y;

}

然后定义一个mapstruct映射类

package com.javatest.mapstruct;

import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

@Mapper
public interface BaseMapper {
    BaseMapper MAPPER = Mappers.getMapper(BaseMapper.class);

    DstObj toDst(SrcObj src);

}

@Mapper注解是mapstruct定义的一个注解,mapstruct在编译期扫描所有的添加了这个注解的接口(也可以是抽象类),会自动生成一个这个接口的实现类。BaseMapper MAPPER = Mappers.getMapper(BaseMapper.class)则是获取这个接口实现类实例的静态接口,它可以放在任何地方,只是为了代码管理方便,一般放在mapstruct映射接口里。

可以看到,实现从SrcObj映射到DstObj的功能,我们不需要写任何代码,只要定义一个接口就可以了(跟spring data repository类似)。下面我们看下测试代码和结果:

package com.javatest.mapstruct;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@Slf4j
@SpringBootApplication
public class MapstructApplication {

    public static void main(String[] args) {
        SpringApplication.run(MapstructApplication.class, args);
    }

    @Bean
    @SuppressWarnings("unchecked")
    public CommandLineRunner runner() {
        return (args) -> {
            SrcObj src = new SrcObj("john", 30, 80.6D, "3.5");
            DstObj dst = BaseMapper.MAPPER.toDst(src);

            log.info("src: {}", new ObjectMapper().writeValueAsString(src));
            log.info("dst: {}", new ObjectMapper().writeValueAsString(dst));
        };

    }
}

编译执行上面代码后,打印结果如下:

2021-11-10 16:38:51.224  INFO 99570 --- [           main] c.j.mapstruct.MapstructApplication       : src: {"name":"john","age":30,"x":80.6,"y":"3.5"}
2021-11-10 16:38:51.225  INFO 99570 --- [           main] c.j.mapstruct.MapstructApplication       : dst: {"name":"john","age":30,"x":"80.6","y":3.5}

同时我们可以在项目的编译打包结果目录target/generated-sources/annotations/com/javatest/mapstruct下看到mapstruct自动生成的接口实现文件BaseMapperImpl.java,文件内容为:

package com.javatest.mapstruct;

import com.javatest.mapstruct.DstObj.DstObjBuilder;
import javax.annotation.Generated;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2021-11-10T16:38:48+0800",
    comments = "version: 1.4.2.Final, compiler: javac, environment: Java 1.8.0_292 (Azul Systems, Inc.)"
)
public class BaseMapperImpl implements BaseMapper {

    @Override
    public DstObj toDst(SrcObj src) {
        if ( src == null ) {
            return null;
        }

        DstObjBuilder dstObj = DstObj.builder();

        dstObj.name( src.getName() );
        dstObj.age( src.getAge() );
        dstObj.x( String.valueOf( src.getX() ) );
        if ( src.getY() != null ) {
            dstObj.y( Double.parseDouble( src.getY() ) );
        }

        return dstObj.build();
    }
}

1.3 基础功能

从 1.2 节的例子我可以看到,mapstruct不仅自动实现了SrcObj和DstObj两个bean之间相同类型相同名字的字段间的拷贝,同时还自动支持基本数据类型(int)与它对应的装箱(boxing)类型以及基本数据类型与String类型间的自动类型转换。当然,非常不建议实际项目在java bean中定义int、double等基本数据类型的字段,这里只是做个功能演示。

事实上,mapstruct支持以下情况的自动类型转换

1、基本数据类型与它对应的装箱类型之间相互转换。
2、不同基本数据类型(及装箱类型)之间相互转换,如int与long,int与Long。
2、String类型与基本数据类型(及装箱类型)之间相互转换
3、Enum类型与String类型的Enum名字之间相互转换

1.4 字段名字不同

映射方法上添加@Mapping注解,指定源和目的字段名字即可,如下所示:

@Mapper
public interface BaseMapper {

    @Mapping(source = "aliasName", target = "name")
    DstObj toDst(SrcObj src);

}

1.5 ignore指定字段不映射

@Mapper
public interface BaseMapper {

    @Mapping(source = "aliasName", target = "name")
    @Mapping(source = "x", target = "x", ignore = true)
    DstObj toDst(SrcObj src);

}

1.6 多个源对象映射到一个对象

mapstruct支持从多个源对象映射到一个目的对象(组合多个源对象的信息),只需要针对多个源对象中字段名字相同的字段明确指定使用哪个源对象的值即可,其他不冲突的字段跟一对一映射时处理一样。

@Mapper
public interface BaseMapper {
    
    // 将src1对象中的name字段映射到DstObj,忽略src2中name字段的值
    @Mapping(source = "src1.name", target = "name")
    DstObj toDst(SrcObj src1, Src2Obj src2);
}

1.7 源对象成员字段的字段直接映射到目的字段中

@Mapper
public interface BaseMapper {

    // subObj是SrcObj对象中的一个字段,它有两个字段名字分别为x和y,分别映射到DstObj的字段x和y
    @Mapping(source = "subObj.x", target = "x")
    @Mapping(source = "subObj.y", target = "y")
    DstObj toDst(SrcObj src);
}

1.8 更新现有目标对象的值,而不是创建一个新的实例

@Mapper
public interface BaseMapper {
    
    // 使用 @MappingTarget 注解即可
    void toDst(SrcObj src, @MappingTarget DstObj dst);
}

1.9 集合间的映射

直接支持,什么都不用做

@Mapper
public interface BaseMapper {

    List<DstObj> toDstList(List<SrcObj> src);
    Set<DstObj> toDstSet(Set<SrcObj> src);
    Set<DstObj> toDstSet(List<SrcObj> src);
    Map<String, DstObj> toDstMap(Map<String, SrcObj> src);
}

2 类型转换

2.1 不同Enum类型之间的转换

不同的Enum类型之间转换时,相同的Enum名字值会直接映射,不同名字的值使用@ValueMapping指定映射关系即可

public enum SrcEnum {

    AA,
    B,
    C,
    D

}
public class SrcObj {

    private SrcEnum type;

}
public enum DstEnum {
    A,
    B,
    C
}
public class DstObj {
    private DstEnum type;
}
@Mapper
public interface BaseMapper {

    @ValueMapping(source = "AA", target = "A")
    // 找不到映射关系的,全部映射到C
    @ValueMapping(source = MappingConstants.ANY_UNMAPPED, target = "C")
    DstObj toDst(SrcObj src);

}

2.2 mapstruct mapper实现字段类型转换

如下所示,SrcObj有一个成员字段SubSrcObj sub,DstObj有一个成员字段SubDstObj sub,需要把SubSrcObj sub内容映射到SubDstObj sub

public class SubSrcObj {

    private String name;
}

public class SrcObj {

    private SubSrcObj sub;

}

public class SubDstObj {
    private String name;
}

public class DstObj {
    private SubDstObj sub;
}

这种情况可以通过一下两种方式之一解决:
1)在同一个mapper接口定义字段类型转换方法

@Mapper
public interface BaseMapper {

    SubDstObj toSubDst(SubSrcObj sub);
    DstObj toDst(SrcObj src);
}

2)在不同mapper接口定义字段转换方法,然后通过uses映入依赖mapper,如下所示

@Mapper
public interface SubMapper {

    SubDstObj toSubDst(SubSrcObj sub);
}

@Mapper(uses = {SubMapper.class})
public interface BaseMapper {

    DstObj toDst(SrcObj src);
}

2.3 expression表达式转换

使用expression表达式可以调用任意java方法来进行映射。例如,实现上面一样的功能,自定义一个映射方法

package com.javatest.mapstruct;

public class SubSrcToDstConverter {

    public static SubDstObj toDst(SubSrcObj src) {
        return new SubDstObj(src.getName());
    }

}

然后通过expression指定映射方法:

@Mapper
public interface BaseMapper {

    @Mapping(target = "sub", expression = "java(com.javatest.mapstruct.SubSrcToDstConverter.toDst(src.getSub()))")
    DstObj toDst(SrcObj src);
}

3 其他特性

3.1 为目标对象赋值常量

@Mapper
public interface BaseMapper {
    // 不管src对象中是否有name字段,以及值是什么,目的对象name字段值始终为harry
    @Mapping(target = "name", constant = "harry")
    DstObj toDst(SrcObj src);
}

3.2 默认值

@Mapper
public interface BaseMapper {
    // 如果src对象的name字段值为null,则为目的对象name字段值赋值harry
    @Mapping(source = "name", target = "name", defaultValue = "harry")
    DstObj toDst(SrcObj src);
}

3.3 mapstruct mapper为抽象类

前面介绍功能时,举的例子@Mapper注解都是注解在接口上,其实@Mapper也可以注解抽象类,这样抽象类可以引入其他对象或实现其他功能,例如实现@BeforeMapping@AfterMapping,让mapper在执行映射前后分别执行特定的功能。例子代码如下:

package com.javatest.mapstruct;

import lombok.extern.slf4j.Slf4j;
import org.mapstruct.AfterMapping;
import org.mapstruct.BeforeMapping;
import org.mapstruct.Mapper;

@Slf4j
@Mapper
public abstract class AbstractMapper {

    @BeforeMapping
    public void doBefore() {
        log.info("before mapping");
    }

    @AfterMapping
    public void doAfter() {
        log.info("after mapping");
    }

    abstract DstObj toDst(SrcObj src);

}

3.4 spring环境下为mapper实例注册spring bean实例

@Mapper注解中直接提供componentModel名字,即spring bean名字即可

@Mapper(componentModel = "baseMapper")
public interface BaseMapper {

    DstObj toDst(SrcObj src);
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容