目录
-
- [一、MapStruct 简介](#一、MapStruct 简介)
-
- [1.1 概述](#1.1 概述)
- [1.2 为什么使用 MapStruct?](#1.2 为什么使用 MapStruct?)
- [1.3 Maven 依赖](#1.3 Maven 依赖)
- 二、SpringBoot实战
-
- [2.1 定义POJO](#2.1 定义POJO)
- [2.2 Mapper接口定义](#2.2 Mapper接口定义)
- [2.3 自动生成 Mapper 代码](#2.3 自动生成 Mapper 代码)
- [2.4 测试用例](#2.4 测试用例)
- 三、其他用法
-
- [3.1 注入 Spring 组件到 Mapper 中](#3.1 注入 Spring 组件到 Mapper 中)
- [3.2 使用 qualifiedName、expression 自定义字段的转换规则](#3.2 使用 qualifiedName、expression 自定义字段的转换规则)
- [3.3 不同字段名映射](#3.3 不同字段名映射)
- [3.4 子对象映射](#3.4 子对象映射)
- [3.5 浅克隆问题解决](#3.5 浅克隆问题解决)
- [3.6 数据类型映射](#3.6 数据类型映射)
- [3.7 使用抽象类自定义映射器](#3.7 使用抽象类自定义映射器)
- [3.8 @BeforeMapping 和 @AfterMapping 注解](#3.8 @BeforeMapping 和 @AfterMapping 注解)
- [3.9 defaultExpression支持](#3.9 defaultExpression支持)
- [3.10 Map 转 Bean](#3.10 Map 转 Bean)
- [3.11 Condition Mapping,有条件的转换](#3.11 Condition Mapping,有条件的转换)
- [3.12 @MappingTarget 直接使用已有Bean](#3.12 @MappingTarget 直接使用已有Bean)
-
官网地址: https://mapstruct.org/
一、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 文件中:
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
是要映射的目标类,他们具有相同的字段。
java
import lombok.Data;
/**
* 源类示例
*/
@Data
public class DemoSource {
private String name;
private String description;
}
java
import lombok.Data;
/**
* 目标类示例
*/
@Data
public class DemoDestination {
private String name;
private String description;
}
2.2 Mapper接口定义
在 MapStruct 中,我们将类型转换器称为 Mapper
,请不要和 Mybatis 中的 Mapper 混淆。下面,我们定义 Mapper 接口。使用 @Mapper
注解,告诉 MapStruct 这是我们的映射器。
java
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 为我们自动生成的:
java
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 {
@Override
public DemoDestination sourceToDestination(DemoSource source) {
if ( source == null ) {
return null;
}
DemoDestination demoDestination = new DemoDestination();
demoDestination.setName( source.getName() );
demoDestination.setDescription( source.getDescription() );
return demoDestination;
}
@Override
public 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,完成配置和代码生成后,我们可以进行转换测试:
java
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 {
@Autowired
private DemoSourceDestinationAssembler demoSourceDestinationAssembler;
/**
* 测试源对象到目标对象的映射功能是否正确
*/
@Test
public 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 容器中的组件该如何实现?这种情况下,我们需要改用抽象类而非接口类了。如下所示:
java
@Mapper(componentModel = "spring")
public abstract class DemoSourceDestinationAssembler {
@Autowired
protected DemoService demoService;
@Mapping(target = "name", expression = "java(demoService.enrichName(source.getName()))")
public abstract DemoDestination sourceToDestination(DemoSource source);
}
3.2 使用 qualifiedName、expression 自定义字段的转换规则
如果我们想自定义字段的转换规则,有三种方式:
- 注入 Spring 的 Bean 对象来调用转换方法(参考3.1用法);
- 使用
@Mapping
的qualifiedByName
属性,配合@Name
注解来实现; - 使用
@Mapping
的expression
属性,通过执行 java 代码调用自定义规则方法。(支持多个字段的合并)
第2、3种方法的代码实现如下:
java
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;
}
}
测试代码如下:
java
/**
* 测试源对象到目标对象的映射功能是否正确
*/
@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
实体类定义:
java
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class Employee {
private Integer id;
private String name;
}
DTO类:
java
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class EmployeeDTO {
private Integer employeeId;
private String employeeName;
}
2)Mapper 接口配置
映射字段不同时,我们需要在 @Mapping
注解中指定目标字段名。
在 MapStruct 中,我们还可以使用表达式来定义 Bean 的成员。
java
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)测试用例
下面进行测试:
java
/**
* 测试当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。
实体类定义:
java
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
public class Division {
private int id;
private String name;
}
java
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class Employee {
private Integer id;
private String name;
private Division division;
}
DTO类:
java
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
public class DivisionDTO {
private int id;
private String name;
}
java
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 检测到需要转换对象类型,并且要转换的方法存在于同一个类中,它将自动使用它。
java
/**
* 将Division实体转换为DivisionDTO数据传输对象
* @param entity 需要转换的Division实体对象
* @return 转换后的DivisionDTO对象
*/
DivisionDTO divisionToDivisionDTO(Division entity);
/**
* 将DivisionDTO对象转换为Division对象
* @param dto 需要转换的DivisionDTO数据传输对象
* @return 转换后的Division实体对象
*/
Division divisionDTOtoDivision(DivisionDTO dto);
3)测试用例
对上面的测试代码稍作修改:
java
/**
* 测试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 的浅克隆问题:
- 将子对象声明为两个单独的类,缺点是如果增加字段的话两个类都需要维护。
- 可以在 Mapper 类中增加相同类型的转换来解决这个问题,代码如下:
java
/**
* 将Division数据传输对象转换为Division实体对象
* @param dto 需要转换的Division数据传输对象
* @return 转换后的Division实体对象
*/
Division divisiontoDivision(Division dto);
我们调整一下测试代码的编译错误:
java
/**
* 测试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 字段:
java
import java.util.Date;
@Getter
@Setter
public class Employee {
private Integer id;
private String name;
private Division division;
private Date date;
}
java
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 指定日期转换格式。代码如下所示:
java
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)测试用例
对前面的测试代码稍作修改:
java
/**
* 测试将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 定义
实体类定义:
java
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类:
java
import lombok.Data;
@Data
public class TransactionDTO {
private String uuid;
private Long totalInCents;
}
注意!这里棘手的地方在于将 BigDecimal
类型的 total 美元金额,转换为 Long totalInCents
(以美分表示的总金额),这部分我们将使用自定义方法实现。
2)Mapper 定义
java
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 自动生成了剩余的部分:
java
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 {
@Override
public 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)模型定义
java
import lombok.Data;
@Data
public class Car {
private Integer id;
private String name;
}
Car 具有燃油车和电动车两个子类:
java
public class BioDieselCar extends Car {
}
java
public class ElectricCar extends Car {
}
CarDTO 中的 FuelType 为枚举类型,表示汽车能源类型:
java
import lombok.Data;
@Data
public class CarDTO {
private Integer id;
private String name;
private FuelType fuelType;
}
java
/**
* 燃料类型
*/
public enum FuelType {
ELECTRIC, BIO_DIESEL
}
2)Mapper 定义
编写 Car 到 CarDTO 的 Mapper 映射。
java
import org.mapstruct.*;
@Mapper
public abstract class CarAssembler {
@BeforeMapping
protected void enrichDTOWithFuelType(Car car, @MappingTarget CarDTO carDTO) {
if (car instanceof ElectricCar) {
carDTO.setFuelType(FuelType.ELECTRIC);
}
if (car instanceof BioDieselCar) {
carDTO.setFuelType(FuelType.BIO_DIESEL);
}
}
@AfterMapping
protected void convertNameToUpperCase(@MappingTarget CarDTO carDTO) {
carDTO.setName(carDTO.getName().toUpperCase());
}
public abstract CarDTO toCarDto(Car car);
}
@MappingTarget
是一个参数注释,在 @BeforeMapping
的情况下,在执行映射逻辑之前填充目标映射 DTO,在 @AfterMapping
注解方法的情况下在执行之后填充。
3)生成代码结果
生成后的代码如下:
java
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 {
@Override
public 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 定义
实体类定义:
java
import lombok.Data;
@Data
public class Person {
private String id;
private String name;
}
DTO类:
java
import lombok.Data;
@Data
public class PersonDTO {
private String id;
private String name;
}
2)Mapper 定义
如果源实体的 id
字段为 null,我们希望生成一个随机的 uuid
并将其分配给 id,代码实现如下:
java
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)测试代码
测试代码如下:
java
@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:
java
public class Customer {
private Long id;
private String name;
//getters and setter omitted for brevity
}
我们可以通过以下 Mapper 方法将 Map 转换为 Bean:
java
@Mapper
public interface CustomerMapper {
@Mapping(target = "name", source = "customerName")
Customer toCustomer(Map<String, String> map);
}
最终生成的转换代码如下所示:
java
// GENERATED CODE
public class CustomerMapperImpl implements CustomerMapper {
@Override
public 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
注解来实现条件转换。例如我们有如下转换代码:
java
@Mapper
public interface CarMapper {
CarDto carToCarDto(Car car);
@Condition
default boolean isNotEmpty(String value) {
return value != null && !value.isEmpty();
}
}
则 Map Struct 1.5.0 Final生成的代码是:
java
// GENERATED CODE
public class CarMapperImpl implements CarMapper {
@Override
public CarDto carToCarDto(Car car) {
if ( car == null ) {
return null;
}
CarDto carDto = new CarDto();
if ( isNotEmpty( car.getOwner() ) ) {
carDto.setOwner( car.getOwner() );
}
// Mapping of other properties
return carDto;
}
}
org.mapstruct.Condition
除了作用到整个bean
外还可以修饰具体的属性值,实现bean
属性维度的条件转换。
3.12 @MappingTarget 直接使用已有Bean
如果我们已经初始化了目标 Bean 中非源 Bean 中的一些字段,能不能直接使用呢?答案是可以的。
1)定义 POJO
源实体类定义:
java
import lombok.Data;
@Data
public class DemoSource {
private String name;
private String description;
}
目标实体类定义:
java
import lombok.Data;
@Data
public class DemoDestination {
private String name;
private Integer age;
private String description;
}
2)定义 Mapper
java
/**
* 使用已有实例进行转换
*/
void destinationToSource(@MappingTarget DemoDestination destination, DemoSource source);
不同于以往的示例,这个接口是没有返回值的,通过 @Mapping
注解标识来直接使用目标类的实例。
3)测试代码
测试代码如下:
java
/**
* 测试源对象到目标对象的映射功能是否正确
*/
@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
:丢失了原本已经赋值的内容,这是因为该字段在源类中也存在,发生了覆盖,我们看下生成的代码就明白了:
java
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 {
@Override
public void destinationToSource(DemoDestination destination, DemoSource source) {
if ( source == null ) {
return;
}
destination.setName( source.getName() );
destination.setDescription( source.getDescription() );
}
}
从 destination.setDescription( source.getDescription() )
可以看出,不管已经存在的目标类的实例中是否有值都会发生强制覆盖。如果需要保留原有字段值,可以使用 @Mapping
的 expression
属性自行单独定义处理逻辑。
整理完毕,完结撒花~ 🌻
参考地址:
1.MapStruct 快速指南,https://www.baeldung-cn.com/mapstruct
2.MapStruct 1.5 发布,终于支持Map转为Bean,https://blog.csdn.net/jj1245_/article/details/149250479