SpringBoot 集成 MapStruct

目录

    • [一、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/

  • 官网文档: 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 文件中:

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 自定义字段的转换规则

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

  1. 注入 Spring 的 Bean 对象来调用转换方法(参考3.1用法);
  2. 使用 @MappingqualifiedByName 属性,配合 @Name 注解来实现;
  3. 使用 @Mappingexpression 属性,通过执行 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 的浅克隆问题:

  1. 将子对象声明为两个单独的类,缺点是如果增加字段的话两个类都需要维护。
  2. 可以在 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() ) 可以看出,不管已经存在的目标类的实例中是否有值都会发生强制覆盖。如果需要保留原有字段值,可以使用 @Mappingexpression 属性自行单独定义处理逻辑。

整理完毕,完结撒花~ 🌻

参考地址:

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

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

相关推荐
oak隔壁找我8 小时前
MyBatis的MapperFactoryBean详解
后端
王道长AWS_服务器8 小时前
AWS Elastic Load Balancing(ELB)—— 多站点负载均衡的正确打开方式
后端·程序员·aws
咖啡Beans9 小时前
SpringBoot2.7集成Swagger3.0
java·swagger
oak隔壁找我9 小时前
Spring BeanFactory 和 FactoryBean 详解
后端
用户4099322502129 小时前
只给表子集建索引?用函数结果建索引?PostgreSQL这俩操作凭啥能省空间又加速?
后端·ai编程·trae
oak隔壁找我9 小时前
SpringMVC 教程
后端
用户34325962788169 小时前
Spring AI Alibaba中使用Redis Vector报错修改过程
后端
oak隔壁找我9 小时前
MyBatis和SpringBoot集成的原理详解
后端
聪明的笨猪猪9 小时前
Java JVM “垃圾回收(GC)”面试清单(含超通俗生活案例与深度理解)
java·经验分享·笔记·面试
oak隔壁找我9 小时前
SpringBoot @Import 注解详解
后端