官网地址:MapStruct

官方示例:mapstruct-examples

关联:

1. 概述

是什么?

一个 Java 注释处理器来生成类型安全的 Bean 映射类。

为什么要用?

Mapstruct的性能远远高于BeanUtils。且BeanUtils.copyProperties()只能转换类中字段名字一样且类型一样的字段

2. 功能说明

2.1 默认映射规则

  1. 同类型,同名的属性会自动映射

  2. 自动类型转换

    1. 基本类型和包装类型会自动转换,

    2. BigDecimal(等)、8种基本类型(及包装类型)和String

    3. 日期类型(java.time.LocalDateTime、java.util.Date等)和String

    4. enum和String

    5. java.sql.Timestamp和java.util.Date

2.2 指定某个属性的映射规则

  • source和target多余的属性对方没有,不会报错

  • source映射源属性,target目标映射属性,defaultValue默认值,若源字段为空,则目标字段就为此默认值。

  • numberFormat = "#.00" 数字格式化,保留两位小数

  • dateFormat = "yyyy-MM-dd HH:mm:ss" 日期格式化

  • ignore=true 不映射该属性

  • constant 标识字段值为常量

  • expression 为 target 属性赋予表达式的运算值,可以是默认值、一段代码,也可以是函数方法,还可以调用用@Mapper#imports 导入的类中的方法,当然也包括导入类的构造方法。

  • 嵌套结构,子对象的属性完全一致时,target 可以用.指代

  • defaultExpression 默认表达式,仅source值为null时生效

@Mapper(imports = {StringBOUtils.class})
public interface EntityMapper {

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

    // 定义映射方法,指定映射规则
    @Mappings({
            // 基本类型 --> String
            @Mapping(source = "intValue", target = "intValueStr"),
            // 基本类型包装类 --> 基本类型
            @Mapping(source = "integerValue", target = "intValue"),
            // LocalDateTime --> String
            @Mapping(source = "localDateTimeValue", target = "localDateTimeValueStr"),
            // BigDecimal --> String  保留两位小数
            @Mapping(source = "bigDecimalValue", target = "formattedBigDecimal", numberFormat = "#.00"),
            // Date --> String  指定时间格式 yyyy-MM-dd HH:mm:ss
            @Mapping(source = "dateValue", target = "formattedDate", dateFormat = "yyyy-MM-dd HH:mm:ss"),
            // 枚举类 --> String
            @Mapping(source = "enumValue", target = "enumValueAsString"),
            // Timestamp --> Date
            @Mapping(source = "timestampValue", target = "timestampAsDate"),
            // defaultValue 设置默认值,来源为null时,目标为默认值。
            @Mapping(source = "stringValue", target = "stringValue",defaultValue = "Hello"),
            // 目标值指定为常量
            @Mapping(target = "constantStr",constant = "我是常量"),
            // ignore = true 忽略属性,不拷贝
            @Mapping(target = "ignoreStr",ignore = true),
            // expression 表达式:常量
            @Mapping(target = "expression_str",expression = "java(\"Jaws\")"),
            // expression 表达式:一段代码
            @Mapping(target = "expression_date",expression = "java(\"表达式 --> 时间:\" + new java.util.Date())"),
            // expression 表达式:方法调用-当前类
            @Mapping(target = "expression_method",expression = "java(EntityMapper.getLastRunTime(sourceEntity))"),
            // expression 表达式:方法调用-外部工具类
            @Mapping(target = "expression_util", expression = "java(StringBOUtils.toBOString(sourceEntity.getIgnore()))"),
            // 嵌套-子对象属性完全一致,可以用.
            @Mapping(target = ".", source = "baby"),
            // 默认表达式,仅source值为null时生效
            @Mapping(target="id", source="sourceId", defaultExpression = "java(\"UUID\"  )")
    })
    TargetEntity mapToTarget(SourceEntity sourceEntity);

    static String getLastRunTime(SourceEntity sourceEntity){
        return "lastRunTime --> " + sourceEntity.getDateValue();
    }
}

2.3 @MappingTarget、@BeforeMapping、@AfterMapping、@Context

  • @MappingTarget 注解的参数获得的是目标实例

  • @MappingTarget 表示传来的carVO对象是已经赋值过的

  • @BeforeMapping 前置处理方法

  • @AfterMapping 后置处理方法

 @AfterMapping 
 public void dat2voAfter(CarDTO carDTO,@MappingTarget CarVO carVO){
      List<PartDTO> partDTOS = carDTO.getPartDTOS();
      //判断集合是否为空,给carVo设置值
      boolean hasPart = partDTOS != null && !partDTOS.isEmpty();
      carVO.setHasPart(hasPart);
 }

2.4 相似结构,且同属性名转换

示例详见 【3.6 mapstruct-field-mapping

List<CarVO> dtos2vos(List<CarDTO> carDTOS);

2.5 @BeanMappring 指定映射属性

配置忽略mapstruct的默认映射行为,只映射了Mapping的属性

	@BeanMapping(ignoreByDefault = true)
    @Mapping(source = "id",target = "id")
    @Mapping(source = "brand",target = "brandName")
    VehicleVO carDTO2vehicleVO(CarDTO carDTO);

2.6 @InheritConfiguration 配置复用

	@BeanMapping(ignoreByDefault = true)
    @Mapping(source = "id",target = "id")
    @Mapping(source = "brand",target = "brandName")
    VehicleVO carDTO2vehicleVO(CarDTO carDTO);


    @InheritConfiguration
    /*@BeanMapping(ignoreByDefault = true)
      @Mapping(source = "id",target = "id")
      @Mapping(source = "brand",target = "brandName")*/
    @Mapping(target = "id",ignore = true)
    VehicleVO updateVehicleVO(CarDTO carDTO,@MappingTarget VehicleVO vehicleVO);

2.7 @InheritInverseConfiguration 反向映射

  • 反向继承 name指定用哪个方法

  • 只继承@Mapping注解,不继承@BeanMapring注解

	@BeanMapping(ignoreByDefault = true)
    @Mapping(source = "id",target = "id")
    @Mapping(source = "brand",target = "brandName")
    //VehicleVO(id=330, price=null, brandName=品牌)
    public abstract VehicleVO carDTO2vehicleVO(CarDTO carDTO);
    

    @InheritInverseConfiguration(name = "carDTO2vehicleVO")
    @BeanMapping(ignoreByDefault = true)
    public abstract CarDTO vehicleVO2carDTO(VehicleVO vehicleVO);

2.8 与Spring整合

转换生成的对象会被Spring容器所管理

import org.mapstruct.Mapper;

@Mapper(componentModel = "spring")
public abstract class CarConvert {
	//获取对象INSTANCE并使用
    public static CarConvert INSTANCE = Mappers.getMapper(CarConvert.class);
    //转换方法
     public abstract CarVO dto2vo(CarDTO carDTO);
 }

3. 示例实践

此处,摘选自官方示例项目 mapstruct-examples 的README文档

个人实践示例代码详见:SpringBoot-Labs-Junw 中的 jLab-2-MapStruct-jdk17 jLab-2-MapStruct-jdk8

3.1 兼容性异常

3.1.1 mapstruct-lombok

展示如何将 MapStruct 与 Lombok 一起使用

注:高版本的Lombok 和 MapStruct 存在兼容性问题,解决方案为添加插件 lombok-mapstruct-binding

<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>${maven-compiler-plugin.version}</version>
				<configuration>
					<source>${java.version}</source>
					<target>${java.version}</target>
					<release>>${java.version}</release>
					<annotationProcessorPaths>
						<path>
							<groupId>org.mapstruct</groupId>
							<artifactId>mapstruct-processor</artifactId>
							<version>${mapstruct.version}</version>
						</path>
						<path>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
							<version>${lombok.version}</version>
						</path>
						<path>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok-mapstruct-binding</artifactId>
							<version>${lombok-mapstruct-binding.version}</version>
						</path>
					</annotationProcessorPaths>
				</configuration>
			</plugin>
		</plugins>
	</build>

3.1.2 mapStruct-swagger2

与 springfox-swagger2 冲突,该框架里也包含 mapstruct

<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>${swagger2.version}</version>
    <scope>compile</scope>
    <exclusions>
        <exclusion>
			<groupId>org.mapstruct</groupId>
			<artifactId>mapstruct</artifactId>
        </exclusion>
    </exclusions>
</dependency>

3.2 mapstruct-iterable-non-iterable

显示如何通过映射器将 util 类从可迭代元素转换为不可迭代元素

示例:

  • List<Integer> List<String>

  • 目标 Integer(集合中的第一个元素) String(集合中的最后一个元素)

    @Test
    public void testToTarget() {

        Source s = new Source();
        s.setMyIntegers( Arrays.asList( 5, 3, 7 ) );
        s.setMyStrings( Arrays.asList( "five", "three", "seven" ) );

        Target t = SourceTargetMapper.MAPPER.toTarget( s );
		// 取出集合中的第一个元素,为目标属性赋值
        assertEquals( new Integer( 5 ), t.getMyInteger() );
		// 集合中的最后一个元素,为目标属性赋值
        assertEquals( "seven", t.getMyString() );
    }

3.3 mapstruct-mapping-from-map

展示如何通过映射器 util 类和限定符在 Maps 上提取值。

示例1:

  • 源:Map<String, Object>

  • 目标:Bean (值来源于Map,属性名与Map中的key值相同)

    @Test
    public void testMapperOnExistingIpAndServer() {

        Map<String, Object> map = new HashMap<>();
        map.put("ip", "127.0.0.1");
        map.put("server", "168.192.1.1");

        Source s = new Source(map);
        Target t = SourceTargetMapper.MAPPER.toTarget( s );

        assertEquals(t.getIp(), "127.0.0.1");
        assertEquals(t.getServer(), "168.192.1.1");

    }

示例2:

  • 源:Map<String, Object>

  • 目标:Bean (值来源于Map,属性名与Map中的key值相同)

    • Bean中存在子bean(值来源于Map,属性名与Map中的key值不同

    @Test
    public void shouldMapMapToBean() {
        Map<String, String> map = new HashMap<>();
        map.put("id", "1234");
        map.put("name", "Tester");
        map.put("did", "4321"); //Department Id
        map.put("dname", "Test");// Depart name

        Employee employee = MapToBeanMapper.INSTANCE.fromMap(map);

        assertNotNull(employee);
        assertEquals(employee.getId(), "1234");
        assertEquals(employee.getName(), "Tester");

        Department department = employee.getDepartment();
        assertNotNull(department);
        assertEquals(department.getId(), "4321");
        assertEquals(department.getName(), "Test");
    }

3.4 mapstruct-rounding

显示如何通过映射器 util 类和限定符对 Numbers 进行四舍五入。

示例:

  • 源:BigDecimal

  • 目标:BigDecimal

  • Util 限定符:in == null ? null : in.setScale( 2, RoundingMode.DOWN );

3.5 mapstruct-updatemethods

显示如何更新现有目标对象。

注:目标有值,但是入参为null时,不会更新

示例:

  • 源1:Bean

  • 源2:值

  • 目标:Bean

测试用例-说明:

  • 源1:属性有值、源2:有值 ; 目标:new

    • 源有值 = 目标属性有值,其他为null

  • 源1:null、源2:有值 ; 目标:new

    • 源有值 = 目标属性有值,其他为null

  • 源1:new、源2:有值 ; 目标:new

    • 源有值 = 目标属性有值,其他为null

  • 源1:属性有值、源2:null ; 目标:new

    • 源有值 = 目标属性有值,其他为null

  • 源1:null、源2:null ; 目标:有值

    • 目标属性保持原值

3.6 mapstruct-field-mapping

显示如何将 Map结构 与具有公共字段的类似结构的对象一起使用。

示例:

  • 源:Long 、String 、Collection<OrderItem>

  • 目标:Long 、String 、List<OrderItemDto>

  • OrderItem 与 OrderItemDto 属性名称一致

测试用例-说明:

  • 源 --> 目标

  • 目标--> 源

3.7 mapstruct-nested-bean-mappings

展示如何通过主根方法映射对象图

示例:

  • 源:存在子Bean

  • 目标:存在子Bean

  • 多种映射方式


    @Mapping(target = "fish.kind", source = "fish.type")
    @Mapping(target = "fish.name", expression = "java(\"Jaws\")")
    @Mapping(target = "plant", ignore = true )
    @Mapping(target = "ornament", ignore = true )
    @Mapping(target = "material", ignore = true)
    @Mapping(target = "quality.document", source = "quality.report")
    @Mapping(target = "quality.document.organisation.name", constant = "NoIdeaInc" )
    FishTankWithNestedDocumentDto map(FishTank source);

3.8 mapstruct-mapping-with-cycles

展示如何映射可以包含循环的对象图

示例:

  • 源:Bean 包含 List<Bean>

  • 目标:Bean 包含 List<Bean>

  • 源Bean 与 目标Bean 属性名不完全一致

3.9 mapstruct-spi-accessor-naming

有关如何使用服务提供程序接口 (SPI) 进行自定义访问器命名策略的示例。

示例:

  • SPI 定义Bean新的Getter、Setter方法规则

  • 源、目标 引用了SPI

    // get、set被定义为with
	@Test
    public void test() {
        GolfPlayer golfPlayer1 = new GolfPlayer();
        golfPlayer1.withName( "Tiger Woods" ).withHandicap( 12 );

        GolfPlayerDto dto = GolfPlayerMapper.INSTANCE.toDto( golfPlayer1 );

        GolfPlayer golfPlayer2 = GolfPlayerMapper.INSTANCE.toPlayer( dto );

        Assert.assertEquals( "Tiger Woods", golfPlayer2.name() );
        Assert.assertEquals( 12L, golfPlayer2.handicap(), 0 );
    }

3.10 mapstruct-jpa-child-parent

如何在 JPA 中使用与父/子关系相关的@Context示例。

示例:

  • 源:String 、List<ChildDto>

    • ChildDto: String

  • 目标:String 、List<ChildEntity>

    • ChildEntity : String、目标父级ParentEntity

  • 源 与 目标 属性名称一致

3.11 mapstruct-suppress-unmapped

显示在混合方案中,如何在默认情况下忽略映射到目标属性而不发出警告。但是,仍将应用具有相同名称的 Bean 属性映射。

3.12 mapstruct-lookup-entity-with-id

显示如何在映射方法中从数据库中读取具有复合键的对象。

示例:

  • 源1:String1 String2

  • 源2:String1 String2

  • 目标:源1.String2 源2.String2

    • 子Bean:源1.String1 源2.String1

  • @ObjectFactory 自定义初始化工厂

3.13 mapstruct-clone

显示如何通过定义所有映射方法深度克隆对象。

3.14 mapstruct-metadata-annotations

演示如何读取注解并将其用作映射指令。

3.15 mapstruct-mapper-repo

演示如何通过代码生成来构建映射器的存储库。

3.99 暂未实践的示例

  • [mapstruct-protobuf3](mapstruct-protobuf3): protobuf3 和 MapStruct 的结合示例
  • [mapstruct-on-ant](mapstruct-on-ant):展示如何在基于 Ant 的项目中使用 MapStruct;要构建此示例,请在命令行上运行“ant build”
  • [mapstruct-on-gradle](mapstruct-on-gradle):展示如何在基于 Gradle 的项目中使用 MapStruct;若要生成示例项目,请在命令行上运行“./gradlew clean build”
  • [mapstruct-on-bazel](mapstruct-on-bazel):展示如何在基于 Bazel 的项目中使用 MapStruct;若要生成示例项目,请在命令行上运行“Bazel Build //...”,若要测试项目,请运行“Bazel Test //...”。
  • [mapstruct-kotlin](mapstruct-kotlin):使用 KAPT(Kotlin 注释处理工具)将 MapStruct 与 Kotlin 配合使用的示例
  • [mapstruct-kotlin-gradle](mapstruct-kotlin-gradle):使用 KAPT 将 MapStruct 与 Kotlin 和 Gradle Kotlin DSL 配合使用的示例