当前位置: 首页 > news >正文

SpringBoot 集成 MapStruct

目录

    • 一、MapStruct 简介
      • 1.1 概述
      • 1.2 为什么使用 MapStruct?
      • 1.3 Maven 依赖
    • 二、SpringBoot实战
      • 2.1 定义POJO
      • 2.2 Mapper接口定义
      • 2.3 自动生成 Mapper 代码
      • 2.4 测试用例
    • 三、其他用法
      • 3.1 注入 Spring 组件到 Mapper 中
      • 3.2 使用 qualifiedName、expression 自定义字段的转换规则
      • 3.3 不同字段名映射
      • 3.4 子对象映射
      • 3.5 浅克隆问题解决
      • 3.6 数据类型映射
      • 3.7 使用抽象类自定义映射器
      • 3.8 @BeforeMapping 和 @AfterMapping 注解
      • 3.9 defaultExpression支持
      • 3.10 Map 转 Bean
      • 3.11 Condition Mapping,有条件的转换
      • 3.12 @MappingTarget 直接使用已有Bean

  • 官网地址: https://mapstruct.org/
  • 官网文档: https://mapstruct.org/documentation/installation/
  • GitHub: https://github.com/mapstruct/mapstruct

一、MapStruct 简介

1.1 概述

MapStruct 是一个 Java Bean mapper,用于 JavaBean 之间的转换。MapStruct 基于约定优于配置的设计思想, 相较于常用的 BeanUtils.copyProperties() 它更高效、优雅。

1.2 为什么使用 MapStruct?

在软件开发中,我们通常使用多层架构设计模式,不同层次的对象模型需要相互转换,例如持久层中的 Entity 与 DTO 之间的转换。

编写此类映射代码是一项繁琐且容易出错的任务。MapStruct 旨在通过尽可能自动化来简化这项工作。

与其他框架相比,MapStruct 在编译时生成 bean 映射代码,从而确保高性能,允许开发人员快速发现并及时解决问题。

1.3 Maven 依赖

将下面依赖添加到 Maven 的 pom.xml 文件中:

<dependency><groupId>org.mapstruct</groupId><artifactId>mapstruct</artifactId><version>1.6.3</version>
</dependency>
<dependency><groupId>org.mapstruct</groupId><artifactId>mapstruct-processor</artifactId><version>1.6.3</version>
</dependency>
  • mapstruct依赖:核心库,提供 MapStruct 的核心注解(如 @Mapper@Mapping)和运行时支持的类。
  • mapstruct-processor依赖:注解处理器,在编译阶段处理 @Mapper 等注解,自动生成映射接口的实现类。

二、SpringBoot实战

本节我们通过示例,快速演示 MapStruct 的用法。

2.1 定义POJO

首先,我们定义两个 Java 类,DemoSource 是源类,DemoDestination 是要映射的目标类,他们具有相同的字段。

import lombok.Data;/*** 源类示例*/
@Data
public class DemoSource {private String name;private String description;
}
import lombok.Data;/*** 目标类示例*/
@Data
public class DemoDestination {private String name;private String description;
}

2.2 Mapper接口定义

在 MapStruct 中,我们将类型转换器称为 Mapper,请不要和 Mybatis 中的 Mapper 混淆。下面,我们定义 Mapper 接口。使用 @Mapper 注解,告诉 MapStruct 这是我们的映射器。

import org.mapstruct.Mapper;@Mapper(componentModel = "spring")
public interface DemoSourceDestinationAssembler {/*** 将DemoSource对象转换为DemoDestination对象* @param source 需要转换的源对象* @return 转换后生成的目标对象*/DemoDestination sourceToDestination(DemoSource source);/*** 将DemoDestination对象转换为DemoSource对象* @param destination 需要转换的DemoDestination对象* @return 转换后生成的DemoSource对象*/DemoSource destinationToSource(DemoDestination destination);
}

注意: 我们只需定义接口即可,MapStruct 会自动创建对应的实现类。

2.3 自动生成 Mapper 代码

执行 mvn clean install 触发 MapStruct 插件自动生成代码,生成后的实现类在 /target/generated-sources/annotations 目录下。

下面就是 MapStruct 为我们自动生成的:

import javax.annotation.Generated;
import org.springframework.stereotype.Component;@Generated(value = "org.mapstruct.ap.MappingProcessor",date = "2025-07-22T09:27:07+0800",comments = "version: 1.6.3, compiler: javac, environment: Java 1.8.0_451 (Oracle Corporation)"
)
@Component
public class DemoSourceDestinationAssemblerImpl implements DemoSourceDestinationAssembler {@Overridepublic DemoDestination sourceToDestination(DemoSource source) {if ( source == null ) {return null;}DemoDestination demoDestination = new DemoDestination();demoDestination.setName( source.getName() );demoDestination.setDescription( source.getDescription() );return demoDestination;}@Overridepublic DemoSource destinationToSource(DemoDestination destination) {if ( destination == null ) {return null;}DemoSource dmeoSource = new DemoSource();demoSource.setName( destination.getName() );demoSource.setDescription( destination.getDescription() );return demoSource;}
}

2.4 测试用例

OK,完成配置和代码生成后,我们可以进行转换测试:

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;@SpringBootTest
public class DemoSourceDestinationAssemblerIntegrationTest {@Autowiredprivate DemoSourceDestinationAssembler demoSourceDestinationAssembler;/*** 测试源对象到目标对象的映射功能是否正确*/@Testpublic void givenSourceToDestination() {// 创建源对象并赋值DemoSource demoSource = new DemoSource();demoSource.setName("SourceName");demoSource.setDescription("SourceDescription");// 拷贝DemoDestination destination = demoSourceDestinationAssembler.sourceToDestination(demoSource);// 比较值是否一样Assertions.assertEquals(demoSource.getName(), destination.getName());Assertions.assertEquals(demoSource.getDescription(), destination.getDescription());}}

三、其他用法

3.1 注入 Spring 组件到 Mapper 中

对于 Spring 应用,如果我们需要在 Mapper 中引用 Spring 容器中的组件该如何实现?这种情况下,我们需要改用抽象类而非接口类了。如下所示:

@Mapper(componentModel = "spring")
public abstract class DemoSourceDestinationAssembler {@Autowiredprotected DemoService demoService;@Mapping(target = "name", expression = "java(demoService.enrichName(source.getName()))")public abstract DemoDestination sourceToDestination(DemoSource source);
}

3.2 使用 qualifiedName、expression 自定义字段的转换规则

如果我们想自定义字段的转换规则,有三种方式:

  1. 注入 Spring 的 Bean 对象来调用转换方法(参考3.1用法);
  2. 使用 @MappingqualifiedByName 属性,配合 @Name 注解来实现;
  3. 使用 @Mappingexpression 属性,通过执行 java 代码调用自定义规则方法。(支持多个字段的合并)

第2、3种方法的代码实现如下:

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;@Mapper(componentModel = "spring")
public interface DemoSourceDestinationAssembler {/*** 将源对象转换为目标对象并丰富目标对象属性* @param source 源对象,包含需要转换的基础数据* @return 转换后的目标对象,包含经过处理的属性值*/@Mappings({@Mapping(target = "name", source = "name", qualifiedByName = "enrichName"),@Mapping(target = "description", expression = "java(concatenate(source.getName(), source.getDescription()))")})DemoDestination sourceToDestinationEnrich(DemoSource source);@Named("enrichName")default String enrichName(String name) {return name + " enriched";}default String concatenate(String name, String description) {return name + " 的描述是:" + description;}
}

测试代码如下:

/*** 测试源对象到目标对象的映射功能是否正确*/
@Test
public void givenSourceToDestinationEnrich() {// 创建源对象并赋值DemoSource demoSource = new DemoSource();demoSource.setName("SourceName");demoSource.setDescription("SourceDescription");// 拷贝DemoDestination destination = demoSourceDestinationAssembler.sourceToDestinationEnrich(demoSource);// 比较值是否一样System.out.println(demoSource);System.out.println(destination);
}

执行结果:

在这里插入图片描述

可以看到,name 属性按照我们自定义的规则修改了。


3.3 不同字段名映射

在之前的例子中,我们定义的两个 Bean 具有相同的字段名称,MapStruct 能够自动完成映射。如果字段类型不一样呢?

下面我们创建两个新的员工Bean:Employee 和 EmployeeDTO。

1)新 POJOs

实体类定义:

import lombok.Getter;
import lombok.Setter;@Getter
@Setter
public class Employee {private Integer id;private String name;
}

DTO类:

import lombok.Getter;
import lombok.Setter;@Getter
@Setter
public class EmployeeDTO {private Integer employeeId;private String employeeName;
}

2)Mapper 接口配置

映射字段不同时,我们需要在 @Mapping 注解中指定目标字段名。

在 MapStruct 中,我们还可以使用表达式来定义 Bean 的成员。

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;@Mapper(componentModel = "spring")
public interface EmployeeAssembler {/*** 将Employee实体对象转换为EmployeeDTO数据传输对象* @param entity 要转换的Employee实体对象,包含员工原始数据* @return 转换后的EmployeeDTO对象,包含映射后的员工数据*/@Mapping(target = "employeeId", source = "entity.id")@Mapping(target = "employeeName", source = "entity.name")EmployeeDTO employeeToEmployeeDTO(Employee entity);/*** 将EmployeeDTO对象转换为Employee对象* @param dto 包含员工信息的传输对象,其中employeeId字段将映射为目标对象的id字段* @return 转换后的Employee实体对象,其id字段来源于dto.employeeId*/@Mapping(target = "id", source = "dto.employeeId")@Mapping(target = "name", source = "dto.employeeName")Employee employeeDTOToEmployee(EmployeeDTO dto);}

3)测试用例

下面进行测试:

/*** 测试当EmployeeDTO对象与Employee对象的名称不同时,employeeDTOToEmployee方法的转换功能*/
@Test
public void givenEmployeeDTOWithDiffNameToEmployee() {EmployeeDTO dto = new EmployeeDTO();dto.setEmployeeId(1);dto.setEmployeeName("John");Employee entity = employeeAssembler.employeeDTOToEmployee(dto);Assertions.assertEquals(dto.getEmployeeId(), entity.getId());Assertions.assertEquals(dto.getEmployeeName(), entity.getName());
}

执行结果:

在这里插入图片描述


3.4 子对象映射

POJO 通常中不会只包含基本数据类型,其中往往会包含其它类。下面演示如何映射具有子对象引用的 Bean。

1)修改 POJO

向 Employee 和 EmployeeDTO 中分别新增一个对象引用 Division、DivisionDTO。

实体类定义:

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;@Getter
@Setter
@AllArgsConstructor
public class Division {private int id;private String name;
}
import lombok.Getter;
import lombok.Setter;@Getter
@Setter
public class Employee {private Integer id;private String name;private Division division;
}

DTO类:

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;@Getter
@Setter
@AllArgsConstructor
public class DivisionDTO {private int id;private String name;
}
import lombok.Getter;
import lombok.Setter;@Getter
@Setter
public class EmployeeDTO {private Integer employeeId;private String employeeName;private Division division;
}

2)修改 Mapper

这里我们需要为 Division 与 DivisionDTO 之间的转换添加方法。如果 MapStruct 检测到需要转换对象类型,并且要转换的方法存在于同一个类中,它将自动使用它。

/*** 将Division实体转换为DivisionDTO数据传输对象* @param entity 需要转换的Division实体对象* @return 转换后的DivisionDTO对象*/
DivisionDTO divisionToDivisionDTO(Division entity);/*** 将DivisionDTO对象转换为Division对象* @param dto 需要转换的DivisionDTO数据传输对象* @return 转换后的Division实体对象*/
Division divisionDTOtoDivision(DivisionDTO dto);

3)测试用例

对上面的测试代码稍作修改:

/*** 测试EmployeeDTO到Employee对象的嵌套映射转换是否正确*/
@Test
public void givenEmpDTONestedMappingToEmp() {EmployeeDTO dto = new EmployeeDTO();dto.setDivision(new DivisionDTO(1, "Division1"));Employee entity = employeeAssembler.employeeDTOToEmployee(dto);System.out.println(Objects.equals(dto.getDivision(), entity.getDivision()));System.out.println("dto.getDivision(): " + dto.getDivision());System.out.println("entity.getDivision(): " + entity.getDivision());Assertions.assertEquals(dto.getDivision().getId(), entity.getDivision().getId());Assertions.assertEquals(dto.getDivision().getName(), entity.getDivision().getName());
}

执行结果:

在这里插入图片描述

注意: 这一步看似没有问题,但是大家思考一下,如果 Employee 和 EmployeeDTO 中使用的都是 Division 类型,会不会存在浅克隆问题呢?答案是会的!


3.5 浅克隆问题解决

我们先来复现一下浅克隆的问题:

把 EmployeeDTO 中的子对象也改为 Division 类型,然后重新跑一次 3.4 中的测试代码,结果如下:

在这里插入图片描述

可以看到 Employee 和 EmployeeDTO 这两个对象中的子对象 Division 的句柄是完全一样的,说明是同一个对象,也就证明存在浅克隆问题。

有两种方法可以解决 MapStruct 的浅克隆问题:

  1. 将子对象声明为两个单独的类,缺点是如果增加字段的话两个类都需要维护。
  2. 可以在 Mapper 类中增加相同类型的转换来解决这个问题,代码如下:
/*** 将Division数据传输对象转换为Division实体对象* @param dto 需要转换的Division数据传输对象* @return 转换后的Division实体对象*/
Division divisiontoDivision(Division dto);

我们调整一下测试代码的编译错误:

/*** 测试EmployeeDTO到Employee对象的嵌套映射转换是否正确*/
@Test
public void givenEmpDTONestedMappingToEmp() {EmployeeDTO dto = new EmployeeDTO();dto.setDivision(new Division(1, "Division1"));Employee entity = employeeAssembler.employeeDTOToEmployee(dto);System.out.println(Objects.equals(dto.getDivision(), entity.getDivision()));System.out.println("dto.getDivision(): " + dto.getDivision());System.out.println("entity.getDivision(): " + entity.getDivision());Assertions.assertEquals(dto.getDivision().getId(), entity.getDivision().getId());Assertions.assertEquals(dto.getDivision().getName(), entity.getDivision().getName());
}

执行结果:

在这里插入图片描述


3.6 数据类型映射

MapStruct 还提供了一些开箱即用的隐式类型转换。本例中,我们希望将 String 类型的日期转换为 Date 对象。

1)修改 POJO

向 Employee 中新增一个 date 字段:

import java.util.Date;@Getter
@Setter
public class Employee {private Integer id;private String name;private Division division;private Date date;
}
import lombok.Getter;
import lombok.Setter;@Getter
@Setter
public class EmployeeDTO {private Integer employeeId;private String employeeName;private DivisionDTO division;private String dateString;
}

2)修改 Mapper

修改 Mapper,设置 @Mapping 注解的 dateFormat 参数,为 date 指定日期转换格式。代码如下所示:

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;@Mapper(componentModel = "spring")
public interface EmployeeAssembler {/*** 将Employee实体对象转换为EmployeeDTO数据传输对象* @param entity 要转换的Employee实体对象,包含员工原始数据* @return 转换后的EmployeeDTO对象,包含映射后的员工数据*/@Mapping(target = "employeeId", source = "entity.id")@Mapping(target = "employeeName", source = "entity.name")@Mapping(target = "dateString", source = "entity.date", dateFormat = "yyyy-MM-dd HH:mm:ss")EmployeeDTO employeeToEmployeeDTO(Employee entity);/*** 将EmployeeDTO对象转换为Employee对象* @param dto 包含员工信息的传输对象,其中employeeId字段将映射为目标对象的id字段* @return 转换后的Employee实体对象,其id字段来源于dto.employeeId*/@Mapping(target = "id", source = "dto.employeeId")@Mapping(target = "name", source = "dto.employeeName")@Mapping(target = "date", source = "dto.dateString", dateFormat = "yyyy-MM-dd HH:mm:ss")Employee employeeDTOToEmployee(EmployeeDTO dto);
}

3)测试用例

对前面的测试代码稍作修改:

/*** 测试将Employee实体映射为EmployeeDTO时日期字段的转换是否正确*/
@Test
public void givenEmpDateMappingToEmpDTO() throws ParseException {Employee entity = new Employee();entity.setDate(new Date());EmployeeDTO dto = employeeAssembler.employeeToEmployeeDTO(entity);SimpleDateFormat format = new SimpleDateFormat(DATE_FORMAT);System.out.println("dto.getDateString(): " + dto.getDateString());System.out.println("format.parse(dto.getDateString()).toString(): " + format.parse(dto.getDateString()).toString());Assertions.assertEquals(format.parse(dto.getDateString()).toString(), entity.getDate().toString());
}/*** 测试EmployeeDTO到Employee对象的日期字段映射是否正确*/
@Test
public void givenTempDTODteMappingToEmp() throws ParseException {EmployeeDTO dto = new EmployeeDTO();dto.setDateString("01-04-2016 01:00:00");Employee entity = employeeAssembler.employeeDTOToEmployee(dto);SimpleDateFormat format = new SimpleDateFormat(DATE_FORMAT);Assertions.assertEquals(format.parse(dto.getDateString()).toString(), entity.getDate().toString());
}

执行结果:

在这里插入图片描述

在这里插入图片描述


3.7 使用抽象类自定义映射器

一些特殊场景 @Mapping 无法满足时,我们需要定制化开发,同时希望保留 MapStruct 自动生成代码的能力。下面我们演示如何通过创建抽象类实现。

1)Bean 定义

实体类定义:

import lombok.Data;import java.math.BigDecimal;
import java.util.UUID;@Data
public class Transaction {private Long id;private String uuid = UUID.randomUUID().toString();private BigDecimal total;}

DTO类:

import lombok.Data;@Data
public class TransactionDTO {private String uuid;private Long totalInCents;}

注意!这里棘手的地方在于将 BigDecimal 类型的 total 美元金额,转换为 Long totalInCents (以美分表示的总金额),这部分我们将使用自定义方法实现。

2)Mapper 定义

import org.mapstruct.Mapper;import java.util.Collection;
import java.util.List;@Mapper
public abstract class TransactionAssembler {/*** 将Transaction实体转换为TransactionDTO数据传输对象* @param entity 要转换的Transaction实体对象* @return 转换后生成的TransactionDTO对象*/public TransactionDTO toTransactionDTO(Transaction entity) {TransactionDTO dto = new TransactionDTO();dto.setUuid(entity.getUuid());dto.setTotalInCents(entity.getTotal().movePointRight(2).longValue());return dto;}/*** 将交易DTO集合转换为交易实体对象集合* @param dto 交易DTO集合,包含待转换的交易数据传输对象* @return 转换后的交易实体对象集合*/public abstract List<TransactionDTO> toTransactionDTO(Collection<Transaction> dto);}

在这里,单个对象之间的转换完全使用我们自定义的映射方法。

而 Collection 与 List 类型之间的转换,仍然交给 MapStruct 完成,我们只需定义接口。

3)生成代码结果

生成代码如下,和我们预想的一样,MapStruct 自动生成了剩余的部分:

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import javax.annotation.Generated;
import org.springframework.stereotype.Component;@Generated(value = "org.mapstruct.ap.MappingProcessor",date = "2025-07-22T15:54:59+0800",comments = "version: 1.6.3, compiler: javac, environment: Java 1.8.0_451 (Oracle Corporation)"
)
@Component
public class TransactionAssemblerImpl extends TransactionAssembler {@Overridepublic List<TransactionDTO> toTransactionDTO(Collection<Transaction> dto) {if ( dto == null ) {return null;}List<TransactionDTO> list = new ArrayList<TransactionDTO>( dto.size() );for ( Transaction transaction : dto ) {list.add( toTransactionDTO( transaction ) );}return list;}
}

可以看到在 list.add( toTransactionDTO( transaction ) ) 处,MapStruct 调用了我们自己实现的方法。


3.8 @BeforeMapping 和 @AfterMapping 注解

我们可以通过 @BeforeMapping@AfterMapping 注解定制化需求。显然,这两个方法是在每次映射之前和之后执行的。也就是说,在最终的实现代码中,会在两个对象真正映射之前和之后添加并执行这两个方法。

举个例子,如何将 Car 的子类 ElectricCar 和 BioDieselCar 映射到 CarDTO。

在映射时,我们希望将汽车的类型映射到 DTO 中的 FuelType 枚举字段。然后在映射完成后,我们想 DTO 的 name 字段转为全大写字母。

1)模型定义

import lombok.Data;@Data
public class Car {private Integer id;private String name;
}

Car 具有燃油车和电动车两个子类:

public class BioDieselCar extends Car {
}
public class ElectricCar extends Car {
}

CarDTO 中的 FuelType 为枚举类型,表示汽车能源类型:

import lombok.Data;@Data
public class CarDTO {private Integer id;private String name;private FuelType fuelType;
}
/*** 燃料类型*/
public enum FuelType {ELECTRIC, BIO_DIESEL
}

2)Mapper 定义

编写 Car 到 CarDTO 的 Mapper 映射。

import org.mapstruct.*;@Mapper
public abstract class CarAssembler {@BeforeMappingprotected void enrichDTOWithFuelType(Car car, @MappingTarget CarDTO carDTO) {if (car instanceof ElectricCar) {carDTO.setFuelType(FuelType.ELECTRIC);}if (car instanceof BioDieselCar) {carDTO.setFuelType(FuelType.BIO_DIESEL);}}@AfterMappingprotected void convertNameToUpperCase(@MappingTarget CarDTO carDTO) {carDTO.setName(carDTO.getName().toUpperCase());}public abstract CarDTO toCarDto(Car car);}

@MappingTarget 是一个参数注释,在 @BeforeMapping 的情况下,在执行映射逻辑之前填充目标映射 DTO,在 @AfterMapping 注解方法的情况下在执行之后填充。

3)生成代码结果

生成后的代码如下:

import javax.annotation.Generated;@Generated(value = "org.mapstruct.ap.MappingProcessor",date = "2025-07-22T16:30:53+0800",comments = "version: 1.6.3, compiler: javac, environment: Java 1.8.0_451 (Oracle Corporation)"
)
public class CarAssemblerImpl extends CarAssembler {@Overridepublic CarDTO toCarDto(Car car) {if ( car == null ) {return null;}CarDTO carDTO = new CarDTO();enrichDTOWithFuelType( car, carDTO ); // @BeforeMapping修饰的方法carDTO.setId( car.getId() );carDTO.setName( car.getName() );convertNameToUpperCase( carDTO ); // @AfterMapping修饰的方法return carDTO;}
}

结果与我们定义的行为一致。


3.9 defaultExpression支持

1.3.0 版本开始,我们可以使用 @Mapping 注解的 defaultExpression 参数来指定一个表达式:如果源字段为 null,该表达式将赋予一个默认值。

1)POJO 定义

实体类定义:

import lombok.Data;@Data
public class Person {private String id;private String name;
}

DTO类:

import lombok.Data;@Data
public class PersonDTO {private String id;private String name;
}

2)Mapper 定义

如果源实体的 id 字段为 null,我们希望生成一个随机的 uuid 并将其分配给 id,代码实现如下:

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;@Mapper(componentModel = "spring")
public interface PersonAssembler {/*** 将Person对象转换为PersonDTO对象* @param person 要转换的Person实体对象* @return 转换后的PersonDTO对象,其中id字段若为空则自动生成UUID值*/@Mapping(target = "id", source = "person.id",defaultExpression = "java(java.util.UUID.randomUUID().toString())")PersonDTO personToPersonDTO(Person person);
}

3)测试代码

测试代码如下:

@Autowired
private PersonAssembler personAssembler;@Test
public void givenPersonEntityToPersonWithExpression() {Person person = new Person();person.setName("Micheal");PersonDTO personDTO = personAssembler.personToPersonDTO(person);System.out.println(person);System.out.println(personDTO);Assertions.assertNull(person.getId());Assertions.assertNotNull(personDTO.getId());Assertions.assertEquals(person.getName(), personDTO.getName());
}

执行结果:

在这里插入图片描述

可以看到转换后的 PersonDTO 中,已经生成了 UUID 格式的 id 字段。


3.10 Map 转 Bean

如果我们有以下 Bean:

public class Customer {private Long id;private String name;//getters and setter omitted for brevity
}

我们可以通过以下 Mapper 方法将 Map 转换为 Bean:

@Mapper
public interface CustomerMapper {@Mapping(target = "name", source = "customerName")Customer toCustomer(Map<String, String> map);
}

最终生成的转换代码如下所示:

// GENERATED CODE
public class CustomerMapperImpl implements CustomerMapper {@Overridepublic Customer toCustomer(Map<String, String> map) {// ...if ( map.containsKey( "id" ) ) {customer.setId( Integer.parseInt( map.get( "id" ) ) );}if ( map.containsKey( "customerName" ) ) {customer.setName( source.get( "customerName" ) );}// ...}
}

不过需要注意,转换的 Map 的 key 必须是 String 类型的,否则代码会跳过这个 key。


3.11 Condition Mapping,有条件的转换

从1.5.0 Final 版本之前,如果 Java bean 中含有 hasXXX 或者 isXXX 的这类方法(XXX 是 bean 中的属性名),则MapStruct生成的代码中则会调用这类方法来判断是否在转换后的bean中是否包含原来的属性,但是遗憾的是,大多数情况下,我们并不能直接修改原bean的代码。基于此,1.5.0 Final 版本引入了 org.mapstruct.Condition 注解来实现条件转换。例如我们有如下转换代码:

@Mapper
public interface CarMapper {CarDto carToCarDto(Car car);@Conditiondefault boolean isNotEmpty(String value) {return value != null && !value.isEmpty();}
}

则 Map Struct 1.5.0 Final生成的代码是:

// GENERATED CODE
public class CarMapperImpl implements CarMapper {@Overridepublic CarDto carToCarDto(Car car) {if ( car == null ) {return null;}CarDto carDto = new CarDto();if ( isNotEmpty( car.getOwner() ) ) {carDto.setOwner( car.getOwner() );}// Mapping of other propertiesreturn carDto;}
}

org.mapstruct.Condition除了作用到整个bean外还可以修饰具体的属性值,实现bean属性维度的条件转换。


3.12 @MappingTarget 直接使用已有Bean

如果我们已经初始化了目标 Bean 中非源 Bean 中的一些字段,能不能直接使用呢?答案是可以的。

1)定义 POJO

源实体类定义:

import lombok.Data;@Data
public class DemoSource {private String name;private String description;
}

目标实体类定义:

import lombok.Data;@Data
public class DemoDestination {private String name;private Integer age;private String description;
}

2)定义 Mapper

/*** 使用已有实例进行转换*/
void destinationToSource(@MappingTarget DemoDestination destination, DemoSource source);

不同于以往的示例,这个接口是没有返回值的,通过 @Mapping 注解标识来直接使用目标类的实例。

3)测试代码

测试代码如下:

/*** 测试源对象到目标对象的映射功能是否正确*/
@Test
public void givenSourceToDestinationWithExistingInstance() {// 创建源对象并赋值DemoSource demoSource = new DemoSource();demoSource.setName("SourceName");// 拷贝DemoDestination destination = new DemoDestination();destination.setAge(18);destination.setDescription("DestinationDescription");demoSourceDestinationAssembler.destinationToSource(destination, demoSource);// 比较值是否一样System.out.println(demoSource);System.out.println(destination);
}

执行结果:

在这里插入图片描述

目标类的实例中,3个属性分别对应了3种情况:

  • name=SourceName:说明成功从源类的实例中获取到了属性值;
  • age=18:说明保留了原本已经赋值的、源类中不存在的属性的值;
  • description=null:丢失了原本已经赋值的内容,这是因为该字段在源类中也存在,发生了覆盖,我们看下生成的代码就明白了:
import javax.annotation.Generated;
import org.springframework.stereotype.Component;@Generated(value = "org.mapstruct.ap.MappingProcessor",date = "2025-07-22T18:32:44+0800",comments = "version: 1.6.3, compiler: javac, environment: Java 1.8.0_451 (Oracle Corporation)"
)
@Component
public class DemoSourceDestinationAssemblerImpl implements DemoSourceDestinationAssembler {@Overridepublic void destinationToSource(DemoDestination destination, DemoSource source) {if ( source == null ) {return;}destination.setName( source.getName() );destination.setDescription( source.getDescription() );}
}

destination.setDescription( source.getDescription() ) 可以看出,不管已经存在的目标类的实例中是否有值都会发生强制覆盖。如果需要保留原有字段值,可以使用 @Mappingexpression 属性自行单独定义处理逻辑。

整理完毕,完结撒花~ 🌻





参考地址:

1.MapStruct 快速指南,https://www.baeldung-cn.com/mapstruct

2.MapStruct 1.5 发布,终于支持Map转为Bean,https://blog.csdn.net/jj1245_/article/details/149250479

http://www.dtcms.com/a/322671.html

相关文章:

  • 10. 怎么实现深拷贝?
  • 大模型SSE流式输出技术
  • C++ 类模板
  • 使用langchain框架开发一个能调用工具的聊天助手Demo
  • MCU-基于TC397的启动流程
  • SQL基本
  • “自动报社保 + 查询导出 ” 的完整架构图和 Playwright C# 项目初始化模板
  • 2022 RoboCom 世界机器人开发者大赛-本科组(国赛)
  • 【阿里巴巴大数据实践之路学习记录】第十章-维度设计
  • 算法_python_学习记录_02
  • Docker大全
  • 通过 Docker 运行 Prometheus 入门
  • 开源智能手机安全相机推荐:Snap Safe
  • 数据结构(9)——排序
  • 【C++上岸】C++常见面试题目--数据结构篇(第十五期)
  • 搜索算法经典案例
  • SpringSecurity过滤器链全解析
  • navicat 连接docker容器里面mysql服务失败解决
  • 传输线的瞬时阻抗
  • UE破碎Chaos分配模型内部面材质
  • Jmeter性能测试之安装及启动Jmeter
  • Nginx 安全加固与服务监控体系
  • 如何无损压缩图片至原大小的10%?
  • ComfyUI——舒服地让大模型为我所用
  • 常用性能测试工具及使用方法介绍
  • 【内核配置】CONFIG_DEBUG_USER 配置项原理分析
  • 线程安全的单例模式,STL和智能指针
  • golang的二维数组
  • 直接插入排序算法:可视化讲解与C语言实现
  • 【R语言】单细胞数据整合质量评估(3)