1. 概述

本文将深入讲解如何使用 MapStruct 实现对象集合的映射。

前提是你已经对 MapStruct 有基本了解。如果还不熟悉,建议先阅读我们的 MapStruct 快速入门指南

本文重点聚焦于集合映射的常见场景和高级策略,适合有一定使用经验的开发者参考,避免踩坑。

2. 集合映射基础

MapStruct 对集合的映射机制与基本类型一致:✅ 你只需定义接口方法,剩下的交给 MapStruct 自动生成

其核心逻辑非常简单粗暴:

自动生成的代码会遍历源集合,逐个调用元素映射方法,最终组装成目标集合。

下面通过几个典型示例展开说明。

2.1 映射 List

先定义源对象 Employee

public class Employee {
    private String firstName;
    private String lastName;

    // 构造函数、getter 和 setter 省略
}

目标 DTO 类:

public class EmployeeDTO {
    private String firstName;
    private String lastName;

    // getter 和 setter
}

定义映射接口:

@Mapper
public interface EmployeeMapper {
    List<EmployeeDTO> map(List<Employee> employees);
}

MapStruct 会自动生成如下实现类:

public class EmployeeMapperImpl implements EmployeeMapper {

    @Override
    public List<EmployeeDTO> map(List<Employee> employees) {
        if (employees == null) {
            return null;
        }

        List<EmployeeDTO> list = new ArrayList<EmployeeDTO>(employees.size());
        for (Employee employee : employees) {
            list.add(employeeToEmployeeDTO(employee));
        }

        return list;
    }

    protected EmployeeDTO employeeToEmployeeDTO(Employee employee) {
        if (employee == null) {
            return null;
        }

        EmployeeDTO employeeDTO = new EmployeeDTO();
        employeeDTO.setFirstName(employee.getFirstName());
        employeeDTO.setLastName(employee.getLastName());
        return employeeDTO;
    }
}

关键点:

  • MapStruct 自动推导出了 Employee → EmployeeDTO 的单个对象映射逻辑 ✅
  • 无需手动定义单个对象映射方法,只要字段名一致即可

⚠️ 但当字段不匹配时,自动映射会失败。例如目标对象为:

public class EmployeeFullNameDTO {
    private String fullName;
    // getter 和 setter
}

此时若只声明:

@Mapper
public interface EmployeeFullNameMapper {
    List<EmployeeFullNameDTO> map(List<Employee> employees);
}

编译时会收到警告:

Warning:(11, 31) java: Unmapped target property: "fullName". 
  Mapping from Collection element "com.baeldung.mapstruct.mappingCollections.model.Employee employee" to 
  "com.baeldung.mapstruct.mappingCollections.dto.EmployeeFullNameDTO employeeFullNameDTO".

❌ 原因:MapStruct 无法自动拼接 firstName + lastName 生成 fullName

✅ 解决方案:手动提供单个对象映射方法:

@Mapper
public interface EmployeeFullNameMapper {
    List<EmployeeFullNameDTO> map(List<Employee> employees);

    default EmployeeFullNameDTO map(Employee employee) {
        EmployeeFullNameDTO dto = new EmployeeFullNameDTO();
        dto.setFullName(employee.getFirstName() + " " + employee.getLastName());
        return dto;
    }
}

此时 MapStruct 会自动调用这个 map(Employee) 方法来处理集合中的每个元素。

📌 总结:只要你在 Mapper 中提供了 S → T 的映射方法,MapStruct 就能自动用于 List<S> → List<T> 等集合映射。

2.2 映射 Set 和 Map

Set 的映射方式与 List 完全一致:

@Mapper
public interface EmployeeMapper {
    Set<EmployeeDTO> map(Set<Employee> employees);
}

生成代码会使用 HashSet 作为默认实现:

public class EmployeeMapperImpl implements EmployeeMapper {
    @Override
    public Set<EmployeeDTO> map(Set<Employee> employees) {
        if (employees == null) return null;

        Set<EmployeeDTO> set = new HashSet<>(Math.max((int)(employees.size() / .75f) + 1, 16));
        for (Employee employee : employees) {
            set.add(employeeToEmployeeDTO(employee));
        }
        return set;
    }
    // ...
}

Map 的映射也类似:

@Mapper
public interface EmployeeMapper {
    Map<String, EmployeeDTO> map(Map<String, Employee> idEmployeeMap);
}

生成代码会遍历 entry 并逐个转换 value:

public class EmployeeMapperImpl implements EmployeeMapper {
    @Override
    public Map<String, EmployeeDTO> map(Map<String, Employee> idEmployeeMap) {
        if (idEmployeeMap == null) return null;

        Map<String, EmployeeDTO> map = new HashMap<>(Math.max((int)(idEmployeeMap.size() / .75f) + 1, 16));
        for (java.util.Map.Entry<String, Employee> entry : idEmployeeMap.entrySet()) {
            String key = entry.getKey();
            EmployeeDTO value = employeeToEmployeeDTO(entry.getValue());
            map.put(key, value);
        }
        return map;
    }
    // ...
}

✅ 所有集合类型(List/Set/Map)的映射逻辑高度统一,掌握一种即可类推。

3. 集合映射策略(Collection Mapping Strategy)

当目标对象包含集合字段时,MapStruct 提供多种策略来决定如何“填充”该集合。

通过 @Mapper(collectionMappingStrategy = ...) 配置,可选值包括:

  • ACCESSOR_ONLY(默认):仅使用 setter
  • SETTER_PREFERRED:优先 setter,无则用 adder
  • ADDER_PREFERRED:优先 adder,无则用 setter ✅ 推荐用于构建者模式
  • TARGET_IMMUTABLE:目标不可变,需构造时初始化

3.1 ACCESSOR_ONLY 策略

这是默认策略,优先调用 setter 方法。

示例源对象:

public class Company {
    private List<Employee> employees;
    // getter 和 setter
}

目标 DTO(同时提供 setter 和 adder):

public class CompanyDTO {
    private List<EmployeeDTO> employees;

    public List<EmployeeDTO> getEmployees() { return employees; }
    public void setEmployees(List<EmployeeDTO> employees) { this.employees = employees; }

    public void addEmployee(EmployeeDTO employeeDTO) {
        if (employees == null) employees = new ArrayList<>();
        employees.add(employeeDTO);
    }
}

Mapper 定义:

@Mapper(uses = EmployeeMapper.class)
public interface CompanyMapper {
    CompanyDTO map(Company company);
}

生成代码:

public class CompanyMapperImpl implements CompanyMapper {
    private final EmployeeMapper employeeMapper = Mappers.getMapper(EmployeeMapper.class);

    @Override
    public CompanyDTO map(Company company) {
        if (company == null) return null;
        CompanyDTO dto = new CompanyDTO();
        dto.setEmployees(employeeMapper.map(company.getEmployees())); // 使用 setter
        return dto;
    }
}

✅ 特点:简洁高效,但要求集合字段可被直接替换。

3.2 ADDER_PREFERRED 策略

当你希望逐个添加元素时(如集合为 final 或使用构建者模式),应使用此策略。

配置方式:

@Mapper(
    collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
    uses = EmployeeMapper.class
)
public interface CompanyMapperAdderPreferred {
    CompanyDTO map(Company company);
}

⚠️ 注意:此时必须显式提供单个对象映射方法,否则无法编译:

@Mapper
public interface EmployeeMapper {
    EmployeeDTO map(Employee employee); // 必须存在!
    List<EmployeeDTO> map(List<Employee> employees);
    // 其他集合方法...
}

生成代码将使用 adder 逐个添加:

public class CompanyMapperAdderPreferredImpl implements CompanyMapperAdderPreferred {
    private final EmployeeMapper employeeMapper = Mappers.getMapper(EmployeeMapper.class);

    @Override
    public CompanyDTO map(Company company) {
        if (company == null) return null;
        CompanyDTO dto = new CompanyDTO();

        if (company.getEmployees() != null) {
            for (Employee employee : company.getEmployees()) {
                dto.addEmployee(employeeMapper.map(employee)); // 逐个 add
            }
        }
        return dto;
    }
}

📌 适用场景:

  • 目标对象集合字段为 final
  • 使用 Lombok @Singular 生成 adder
  • 需要懒加载或按条件添加元素

4. 目标集合的实现类型

MapStruct 在生成代码时会为集合接口选择默认实现类,例如:

接口类型 默认实现
List ArrayList
Set HashSet
Map HashMap

你可以在 官方文档 查看完整映射表。

⚠️ 注意:这些实现是写死在生成代码中的,无法通过配置更改。若需特定实现(如 LinkedHashSet),建议在目标对象中直接声明具体类型,或使用 @ObjectFactory 自定义。

5. 总结

本文系统梳理了 MapStruct 集合映射的核心机制与最佳实践:

  • ✅ 集合映射本质是“循环 + 单个对象映射”
  • ✅ 字段名一致时自动映射,否则需手动提供转换逻辑
  • ✅ List/Set/Map 映射方式统一,API 设计简洁
  • ADDER_PREFERRED 策略更适合复杂对象构建场景
  • ✅ 默认使用常见集合实现类(ArrayList/HashSet/HashMap)

掌握这些技巧后,你可以更高效地处理 DTO 转换,避免手写重复的 for 循环代码。

完整示例代码已上传至 GitHub:https://github.com/tech-tutorial/mapstruct-collections-demo


原始标题:Mapping Collections with MapStruct