Mapstruct详细使用说明

2年前 (2022) 程序员胖胖胖虎阿
209 0 0

前言

在使用分层或者分模块化的项目中,我们可能定义各种各样的O,例如:DO,VO,DTO等等。我们在进行这些对象之间的拷贝时,通过手动写get/set方法进行属性之间的赋值。因为他们之间的属性大部分都是相同的,不仅浪费时间,并且还有大量重复代码。所以,各种框架都添加的对象之间的拷贝的工具类。例如:

Spring自带了BeanUtils

Apatch自带的BeanUtils

Apatch自带的PropertyUtils

mapstruct提供Mappers

    本文主要介绍MapStruct的基础知识、Mapstruct的优缺点、Mapctruct的拷贝示例、以及四种方法时间对比。

Mapstuct介绍

1.mapstruct的官网地址:
https://mapstruct.org

2.mapstruct的文档地址:
https://mapstruct.org/documentation/stable/reference/html/

3.mapstrcut的示例github地址
https://github.com/mapstruct/mapstruct-examples

4.mapstruct的介绍
mapstruct的作用:就像mapstruct官网所说的:mapsrtuct是一个用于简化在JavaBean之间映射的代码生成器工具,并且是基于约定高于配置的方式。

    因为生成的代码使用简单的get/set方法,所以mapstruct可以更快执行、类型之间复制更安全、且容易理解。

mapstruct的优缺点

下面看看mapstruct相比其他映射工具有什么优点

1.mapstruct的优点:
mapstruct是在代码编译的时候,生成其映射规则的代码,所以会在编译时暴露映射错误的代码,让错误提前暴露。

因为使用的是一些简单方法:set/get或者Fluent方式,而非反射方式,所以可以更快的执行完成。可以通过04章节查看各个方法效率上对比

可以实现深拷贝,上面4种方法中只有mapstruct可以实现深拷贝。但是需要进行配置(使用@mapper(mappingControl=DeepClone.class)进行设置)。其他的都是浅拷贝。从而mapstruct实现修改新对象不会对老对象产生影响。

类型更加安全

可以进行自定义的映射。可以自定义各种方法,完成属性映射。例如:将两个属性数据合并成一个设置到目标属性上

2.mapstruct的缺点
必须添加一个接口或者抽象类,才能实现映射。其他的三种方法都不用写额外接口或者类。

Mapstruct的简单示例使用

上面主要是介绍一下Mapstruct的基础知识,下面看一个mapstruct的简单示例,完成属性之间的映射。

    在示例中,我们使用mapstruct的1.4.2.Final版本,进行试验的

    1.第一个原属性数据
//第一个拷贝对象
public class CompareA {
 
    private String name;
 
    private Integer age;
 
    private LocalDateTime createTime;
 
    private double score;
 
    private Double totalScore;
 
    private Date updateTIme;
 
    private ChildCompare childCompare;
 
    private Long days;
    
    //省略其get/set方法,也可使用Lombok,以后讲解lombok与mapstruct结合使用
 }
  1. 第二个目标属性数据
public class CompareB {
 
    private String name;
 
    private Integer age;
 
    private String createTime;
 
    private double score;
 
    private Double totalScore;
 
    private Date updateTIme;
 
    private ChildCompare childCompare;
 
    private Long day;
    
    //省略其get/set方法,
}

3.子类

//这是CompareA和CompareB共有的对象
public class ChildCompare {
 
    private String childName;
 
    private long childAge;
    
    //省略其get/set方法,
}

上面是两个实体对象,观察这两个实体可以看到有两点不同:

CompareA中有一个createTime属性,数据类型为LocalDateTime,CompareB也有一个createTime,但是属性类型为String
CompareA有一个days属性,而CompareB有一个day属性

4.mapstruct接口(这是需要我们手动写的)


 

@Mapper
public interface CompareMapper {
 
    CompareMapper INSTANCE = Mappers.getMapper(CompareMapper.class);
 
    @Mapping(target = "day", source = "days")
    @Mapping(target = "createTime", source = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
    @Mapping(target = "childCompare", source = "childCompare", mappingControl = DeepClone.class)
    CompareB mapper(CompareA compareA);
}

从上面的实现中,我们加入了三个注解:

第一个注解:完成属性名不匹配的问题,将compareA中的days(source)赋值到compareB的day(target)
第二个注解:完成属性类型不匹配的问题,将compareA中的createTime,赋值到compareB的createTime,这里进行日期的格式转化,使用dateFormat设置格式化类型
第三个注解:完成深度拷贝,将compareA中的childCompare属性进行深度拷贝,而不是指针复制,最重要的是最后一个参数:mappingControl = DeepClone.class

5.最后就会自动生成一个实现类:


 

@Generated(
    value = "org.mapstruct.ap.MappingProcessor"
)
public class CompareMapperImpl implements CompareMapper {
 
    @Override
    public CompareB mapper(CompareA compareA) {
        if ( compareA == null ) {
            return null;
        }
 
        CompareB compareB = new CompareB();
 
        compareB.setDay( compareA.getDays() );
        if ( compareA.getCreateTime() != null ) {
            compareB.setCreateTime( DateTimeFormatter.ofPattern( "yyyy-MM-dd HH:mm:ss" ).format( compareA.getCreateTime() ) );
        }
        compareB.setChildCompare( childCompareToChildCompare( compareA.getChildCompare() ) );
        compareB.setName( compareA.getName() );
        compareB.setAge( compareA.getAge() );
        compareB.setScore( compareA.getScore() );
        compareB.setTotalScore( compareA.getTotalScore() );
        compareB.setUpdateTIme( compareA.getUpdateTIme() );
 
        return compareB;
    }
 
    protected ChildCompare childCompareToChildCompare(ChildCompare childCompare) {
        if ( childCompare == null ) {
            return null;
        }
 
        ChildCompare childCompare1 = new ChildCompare();
 
        childCompare1.setChildName( childCompare.getChildName() );
        childCompare1.setChildAge( childCompare.getChildAge() );
 
        return childCompare1;
    }
}

从上面的实现,我们可以看出,添加一个childCompareToChildCompare方法,来完成对象之间的深度拷贝。同样的将days设置到了day属性,等等。

      6.最后,我们调用这个mapper方法,就可以完成对象之间的拷贝:如下:

CompareMapper.INSTANCE.mapper(new CompareA());

对象映射之间的比较



public class CompareTest {
 
    int count = 1000000;
 
    /**
     * Spring中的BeanUtils的使用,最后使用了6700ms
     */
    @Test
    public void springBeanUtils() {
        CompareA populate = populate();
        StopWatch stopWatch = new StopWatch();
        stopWatch.start("开始拷贝");
        for (int i = 0; i < count; i++) {
            CompareB compareB = new CompareB();
            BeanUtils.copyProperties(populate, compareB);
        }
        stopWatch.stop();
        System.out.println(stopWatch.getTotalTimeMillis());
    }
 
    /**
     * 这是使用mapstruct进行对象拷贝,使用时间54ms
     */
    @Test
    public void mapstructBeanUtils() {
        CompareA populate = populate();
        StopWatch stopWatch = new StopWatch();
        stopWatch.start("开始拷贝");
        for (int i = 0; i < count; i++) {
            CompareB mapper = CompareMapper.INSTANCE.mapper(populate);
        }
        stopWatch.stop();
        System.out.println(stopWatch.getTotalTimeMillis());
    }
 
    /**
     * 这是Apache的BeanUtils工具,使用7086ms
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     */
    @Test
    public void commonBeanUtils() throws InvocationTargetException, IllegalAccessException {
        CompareA populate = populate();
        StopWatch stopWatch = new StopWatch();
        stopWatch.start("开始拷贝");
        for (int i = 0; i < count; i++) {
            CompareB compareB = new CompareB();
            org.apache.commons.beanutils.BeanUtils.copyProperties(compareB, populate);
        }
        stopWatch.stop();
        System.out.println(stopWatch.getTotalTimeMillis());
    }
 
    /**
     * 使用Apache中的propertyUtils静态方法,使用3756ms
     * @throws IllegalAccessException
     * @throws NoSuchMethodException
     * @throws InvocationTargetException
     */
    @Test
    public void commonPropertiesUtils() throws IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        CompareA populate = populate();
        StopWatch stopWatch = new StopWatch();
        stopWatch.start("开始拷贝");
        for (int i = 0; i < count; i++) {
            CompareB compareB = new CompareB();
            PropertyUtils.copyProperties(compareB, populate);
        }
        stopWatch.stop();
        System.out.println(stopWatch.getTotalTimeMillis());
    }
 
    public CompareA populate() {
        CompareA compareA = new CompareA();
        compareA.setAge(10);
        compareA.setName("moxiao");
        compareA.setCreateTime(LocalDateTime.now());
        compareA.setDays(100L);
        compareA.setTotalScore(100.0);
        compareA.setScore(12.3);
        compareA.setUpdateTIme(new Date());
        ChildCompare childCompare = new ChildCompare();
        childCompare.setChildAge(200);
        childCompare.setChildName("moxiaoChild");
        compareA.setChildCompare(childCompare);
        return compareA;
    }
 
}

综上所述,1百万数据,mapstruct可以在50ms左右完成。

Mapstruct的属性之间的映射规则

在属性映射方面,mapstruct相比于其他映射工具有:

  • 可以进行不同属性名之间的映射,使用@Mapping注解的source和target
  • 可以使用表达式进行设置,使用@Mapping注解中的expression
  • 可以设置默认值,使用@Mapping注解中的constant,
  • 可以进行不同类型之间的映射,并且更加安全
  • 可以进行自定义映射方法

mapstruct的映射控制说明

2.1 映射控制的作用:

    在源对象和目标对象之间进行映射时,哪种映射方法将被考虑。即:只有在有这个控制条件下,才会执行对应的映射方法。

    所有属性之间的复制或者拷贝都将是依据以下映射控制来完成的。只有更好地理解这四种映射控制,才能按照我们的想法进行对象之间的拷贝。

2.2 映射控制的种类:

mapstruct一共有四种映射控制:

MappingControl.Use.DIRECT
直接映射:直接将源属性赋值到目标属性上,如果是对象的话,就是指针复制,也就是浅拷贝。这种方法有一个前提:目标属性是源属性的父类或者相同类型。

MappingControl.Use.BUILT_IN_CONVERSION
内置映射:将使用mapstruct内置的映射方法。有哪些内置映射方法,例如:日期和字符串、原生类型和对应的包装对象、字符串和枚举类型等等。

MappingControl.Use.MAPPING_METHOD
映射方法:包含两类:可以是自定义的映射方法,也可以是系统自动生成的映射方法。注意:自定义映射方法比系统自动生成的映射方法的优先级更高。例如:深度拷贝就是使用这个映射控制。系统自动生成的映射方法:一般都是对象之间的映射。

MappingControl.Use.COMPLEX_MAPPING
复合映射:是一种结合BUILT_IN_CONVERSION和MAPPING_METHOD的映射控制,来完成对象属性之间的拷贝。当进行COMPLEX_MAPPING时:一共有三种形态:

            a) target = method1( method2( source ) )

            b) target = method( conversion( source ) )

            c) target = conversion( method( source ) )

    其中method开头表示:自定义映射方法,conversion开头的表示:内置映射方法。

    注意:复合映射中只能进行两次转化,不能多于两次,也不能少于两次。这三种形态不含两个内置映射方法来完成属性之间的拷贝。即:至少有一个自定义的映射方法的存在。

    注意:在没有某一中映射控制时,就不会采用这种方式完成属性映射,例如:没有MAPPING_METHOD就会不采用自定义的映射方法完成属性映射。

2.3 映射控组组合

    四种映射控制可以组合使用,即同时使用两个及以上的映射控制,从而在这些映射控制中选择一种方式使用。那这就涉及到映射控制之间优先级。

2.4 mapstruct内置的映射控制组合

    在mapstruct中一共有三种内置的映射控制组合,以及自定义的映射组合。

@MappingControl注解类

    该注解使用了全部的映射控制:DIRECT、MAPPING_METHOD、 BUILT_IN_CONVERSION、COMPLEX_MAPPING。这也这是:默认使用映射控制。

@DeepClone注解类

   该注解只使用了:MAPPING_METHOD映射,当我们使用这个注解时,将不会采用其他的三种方式来完成属性之间的拷贝。

@NoComplexMapping注解类

   改注解使用了三种:DIRECT、MAPPING_METHOD、 BUILT_IN_CONVERSION,即将不会使用复合映射方法完成属性之间的拷贝

自定义的映射组合
我们可以声明一个注解类,自定义只是用哪些映射控制,例如:只使用内置的映射方法

@Retention(value = RetentionPolicy.CLASS)
@MappingControl( MappingControl.Use.BUILT_IN_CONVERSION )
public @interface BuildInConversionControl {
}

2.5 映射控制的使用

    mapstruct默认使用的是:@MappingControl注解类,即四种映射控制

    我们可以在@Mapper注解中的mappingControl属性中设置,他的属性值是一个注解类。在@Mapper注解中设置这个属性:将会应用到这个类中的所有方法的属性之间的拷贝

    我们也可以在@Mapping注解中的mappingControl属性中设置。在@Mapping注解中设置这个属性:将只会应用到指定属性上。

    @Mapping的优先级比@Mapper的高,即优先采用@Mapping,如果没有才会使用@Mapper进行属性之间的映射。

mapstruct的映射控制优先级

在有多个映射控制使用时,例如在使用:@MappingControl注解,mapstruct将会如何进行属性之间的映射。这里就涉及到四种映射控制之间的优先级。

3.1 四种映射控制的优先级,从高到低

在有MAPPING_METHOD控制下,并且包含有一个自定义的映射方法。这个自定义方法必须满足:输入参数为源属性父级或者相同类型,这个映射方法的返回值为目标属性的子类或相同类型。必须都满足这两个条件:这样在进行属性拷贝时,才会使用自定义的映射方法。不然则跳过。
在有DIRECT控制下,并且目标属性必须是源属性的父类或者相同类型。功能:直接将源属性的指针拷贝到目标属性下。必须满足这两个条件,才会使用指针拷贝,不然则跳过。
如果有BUILT_IN_CONVERSION的控制下,在源属性和目标属性之间拷贝时,mapstruct就会根据源属性类型和目标属性类型找到对应的内置映射方法使用,没有找到,就跳过。
如果有COMPLEX_MAPPING控制下,它又划分成三种类型:
如果存在两个自定义的方法,可以将源属性转化为目标属性,即: target = method1( method2( source ) ),优先使用。

            如果有一个自定义方法,一个内置映射方法,可以将源属性转化为目标属性,就会使用其中一个target = method( conversion( source ) )或者target = conversion( method( source ) )。

 5. 如果有MAPPING_METHOD控制下,将自动生成一个映射方法,完成属性之间的映射。必须对象,属性不能是:8中基本类型和对应的包装类以及String

3.2 注意事项:

    如果源属性是8种基本类型或者包装体,而目标属性是对应的类型(不能是不对应的),则会直接拷贝。就算有就不会走上面的优先级。例如:源属性为long,目标属性为Long,不管配置的什么,都会直接赋值。

mapstruct的映射控制的示例

public class MappingOrder {
 
    private String name;
 
    private Integer age;
 
    private LocalDateTime birthTime;
 
    private MapperOrderChild mapperOrderChild;
 
    private MappingOrderChildChild mapperOrderChild2;
    //省略其set/get方法
 }

目标对象

public class MappingOrderVO {
 
    private String name;
 
    private Number age;
 
    private String birthString;
 
    private MapperOrderChild mapperOrderChild;
 
    private MapperOrderChild2 mapperOrderChild2;
 }

映射类(必须自己单独定义一个,mapstruct必须提供一个mapper接口或者抽象类)


@Mapper
public interface MapperOrderMapper {
 
    @Mapping(source = "birthTime", target = "birthString", dateFormat = "yyyy-MM-dd HH:mm:ss")
    //这一行进行深度拷贝,注意最后一个参数很重要:mappingControl = DeepClone.class
    @Mapping(source = "mapperOrderChild2", target = "mapperOrderChild2", mappingControl = DeepClone.class)
    MappingOrderVO entityToVo(MappingOrder mappingOrder);
    
}

mapstruct自动生成的文件

public class MapperOrderMapperImpl implements MapperOrderMapper {
 
    @Override
    public MappingOrderVO entityToVo(MappingOrder mappingOrder) {
        if ( mappingOrder == null ) {
            return null;
        }
 
        MappingOrderVO mappingOrderVO = new MappingOrderVO();
        
        //第一个属性,因为没有自定义方法从LocalDateTime转化为String,将跳过,
        //不能直接映射,优先级第二条
        //mapstruct有一个从LocalDateTime转化为String的内置方法,从而满足第三条映射
        if ( mappingOrder.getBirthTime() != null ) {
            mappingOrderVO.setBirthString( DateTimeFormatter.ofPattern( "yyyy-MM-dd HH:mm:ss" ).format( mappingOrder.getBirthTime() ) );
        }
        
        //我们在方法中使用DEEPCLONE,即MAPPING_METHOD控制
        //所以只有第一条优先级和第五条优先级,满足。因为没有自定义映射方法,从而跳过
        //所以Mapstruct使用第五条,自动生成一个映射方法
        mappingOrderVO.setMapperOrderChild2( mappingOrderChildChildToMapperOrderChild2( mappingOrder.getMapperOrderChild2() ) );
        //DIRECT优先级获胜
        mappingOrderVO.setName( mappingOrder.getName() );
        //DIRECT优先级获胜
        mappingOrderVO.setAge( mappingOrder.getAge() );
        //DIRECT优先级获胜
        mappingOrderVO.setMapperOrderChild( mappingOrder.getMapperOrderChild() );
        return mappingOrderVO;
    }
 
    protected MapperOrderChild2 mappingOrderChildChildToMapperOrderChild2(MappingOrderChildChild mappingOrderChildChild) {
        if ( mappingOrderChildChild == null ) {
            return null;
        }
        MapperOrderChild2 mapperOrderChild2 = new MapperOrderChild2();
 
        mapperOrderChild2.setFirstName( mappingOrderChildChild.getFirstName() );
        mapperOrderChild2.setSecondName( mappingOrderChildChild.getSecondName() );
 
        return mapperOrderChild2;
    }
}

版权声明:程序员胖胖胖虎阿 发表于 2022年11月3日 下午10:48。
转载请注明:Mapstruct详细使用说明 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...