目录

一、为什么你需要 MapStruct

在真实项目里,我们经常要写各种“对象转换”代码:

  • 数据库实体 UserEntity 转接口输出 UserVO
  • 接口入参 CreateOrderRequest 转领域命令 CreateOrderCommand
  • 多个来源对象合并成一个展示对象 OrderDetailVO

如果不用框架,通常会出现三种问题:

  1. 手写 setter 太多:重复、啰嗦、容易漏字段。
  2. BeanUtils 反射映射慢且不安全:运行时才发现问题,重构易出事故。
  3. 团队风格不统一:每个人写法不同,难维护、难 Review。

MapStruct 的核心价值是:

  • 编译期生成映射代码
  • 类型安全(字段不匹配会在编译阶段暴露)
  • 性能接近手写代码
  • 可维护性高(映射规则集中、可审计)

一句话总结:用编译期代码生成,解决运行期反射映射的性能与可靠性问题。


二、MapStruct 本质原理

MapStruct 是一个 Annotation Processor(注解处理器)。它在 javac 编译阶段扫描你的 @Mapper 接口,生成对应实现类(通常是 xxxImpl)。

2.1 运行流程

  1. 你写一个 @Mapper 接口
  2. 编译时注解处理器解析方法签名与注解规则
  3. 生成具体转换实现类(纯 Java 代码)
  4. 运行时直接调用生成类方法(无反射)

2.2 为什么快

因为生成的是类似下面这种代码:

target.setName(source.getName());
target.setAge(source.getAge());

本质就是你手写 setter,JIT 很容易优化。


三、快速上手:5 分钟可运行 Demo

3.1 Maven 依赖

<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>1.5.5.Final</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.11.0</version>
            <configuration>
                <source>17</source>
                <target>17</target>
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>1.5.5.Final</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

如果你用了 Lombok,后面第十章会给出兼容方案。

3.2 定义对象

public class UserEntity {
    private Long id;
    private String nickname;
    private Integer age;
    // getter/setter
}

public class UserVO {
    private Long id;
    private String nickname;
    private Integer age;
    // getter/setter
}

3.3 定义 Mapper

import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

@Mapper
public interface UserMapper {
    UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
    UserVO toVO(UserEntity entity);
}

3.4 调用

UserEntity entity = new UserEntity();
entity.setId(1L);
entity.setNickname("pjy");
entity.setAge(28);

UserVO vo = UserMapper.INSTANCE.toVO(entity);

编译后会生成 UserMapperImpl,你能直接看到具体映射代码。


四、最常用注解详解

4.1 @Mapper

用于标注映射接口。常见参数:

  • componentModel = "spring":交给 Spring 管理
  • uses = {...}:复用其他 mapper
  • unmappedTargetPolicy:未映射字段策略
@Mapper(componentModel = "spring")
public interface UserMapper {
    UserVO toVO(UserEntity entity);
}

4.2 @Mapping

用于字段级映射。

@Mapping(target = "userName", source = "nickname")
UserVO toVO(UserEntity entity);

4.3 @Mappings

旧写法,@Mapping 可重复后一般不用 @Mappings

4.4 @BeanMapping

用于方法级规则,比如忽略 null、指定构建策略。

4.5 @InheritConfiguration / @InheritInverseConfiguration

用于复用已有映射规则,避免重复配置。

4.6 @Named + qualifiedByName

用于指定某个字段映射使用哪一个自定义方法。


五、核心能力详解

5.1 字段改名映射

@Mapper(componentModel = "spring")
public interface UserMapper {
    @Mapping(target = "userName", source = "nickname")
    UserDTO toDTO(UserEntity entity);
}

5.2 多字段映射与常量/default

@Mapping(target = "status", constant = "ACTIVE")
@Mapping(target = "level", defaultValue = "0")
UserDTO toDTO(UserEntity entity);

5.3 表达式映射

@Mapping(target = "fullName", expression = "java(entity.getFirstName() + \" \" + entity.getLastName())")
UserDTO toDTO(UserEntity entity);

expression 灵活但可维护性差,建议复杂逻辑放到具名方法。

5.4 集合映射

List<UserDTO> toDTOList(List<UserEntity> list);
Set<UserDTO> toDTOSet(Set<UserEntity> set);

MapStruct 会自动循环调用单个元素映射方法。

5.5 嵌套对象映射

@Mapping(target = "addressCity", source = "address.city")
UserDTO toDTO(UserEntity entity);

5.6 多源对象合并

@Mapping(target = "orderNo", source = "order.orderNo")
@Mapping(target = "userName", source = "user.nickname")
OrderDetailVO toVO(OrderEntity order, UserEntity user);

5.7 枚举映射

public enum Gender { MALE, FEMALE }
public enum GenderDTO { M, F }

@ValueMappings({
    @ValueMapping(source = "MALE", target = "M"),
    @ValueMapping(source = "FEMALE", target = "F")
})
GenderDTO map(Gender gender);

5.8 日期与数字格式

@Mapping(target = "birthday", dateFormat = "yyyy-MM-dd")
UserDTO toDTO(UserEntity entity);

六、更新场景

新增通常是 toDTO(source),但更新常见需求是:

  • “把 request 更新到已有 entity”
  • “null 不覆盖老值”

MapStruct 正确姿势:

@Mapper(componentModel = "spring")
public interface UserMapper {

    @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
    void update(@MappingTarget UserEntity entity, UpdateUserRequest request);
}

这里 @MappingTarget 是关键,表示更新目标对象而不是创建新对象。


七、空值策略

MapStruct 有多个空值控制点:

  • NullValueCheckStrategy
  • NullValuePropertyMappingStrategy
  • NullValueMappingStrategy

常见推荐:

  • 更新场景:NullValuePropertyMappingStrategy.IGNORE
  • 集合场景:明确空集合与 null 的语义

团队统一配置示例(后面第九章给出 @MapperConfig):

@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)

八、自定义转换方法

8.1 用默认方法

@Mapper(componentModel = "spring")
public interface PriceMapper {

    @Mapping(target = "amountYuan", source = "amountFen", qualifiedByName = "fenToYuan")
    PriceVO toVO(PriceEntity entity);

    @Named("fenToYuan")
    default BigDecimal fenToYuan(Long fen) {
        if (fen == null) return BigDecimal.ZERO;
        return BigDecimal.valueOf(fen).divide(BigDecimal.valueOf(100));
    }
}

8.2 抽成独立 Mapper 复用

@Mapper(componentModel = "spring")
public interface CommonConvertMapper {
    @Named("maskPhone")
    default String maskPhone(String phone) { ... }
}

@Mapper(componentModel = "spring", uses = CommonConvertMapper.class)
public interface UserMapper {
    @Mapping(target = "mobile", source = "mobile", qualifiedByName = "maskPhone")
    UserVO toVO(UserEntity entity);
}

九、企业级统一配置:@MapperConfig

大项目不要每个 mapper 重复写配置,建议集中:

@MapperConfig(
    componentModel = "spring",
    unmappedTargetPolicy = ReportingPolicy.ERROR,
    injectionStrategy = InjectionStrategy.CONSTRUCTOR,
    nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE
)
public interface CentralMapperConfig {
}

然后在 mapper 中使用:

@Mapper(config = CentralMapperConfig.class, uses = {CommonConvertMapper.class})
public interface UserMapper {
    UserVO toVO(UserEntity entity);
}

推荐策略说明

  • ReportingPolicy.ERROR:未映射字段直接编译报错(强约束)
  • CONSTRUCTOR:便于测试与依赖注入一致性
  • 统一 null 策略,避免“有的覆盖、有的不覆盖”

十、MapStruct + Lombok 正确姿势

很多项目会遇到“编译能过但生成异常/字段拿不到”的问题,根因常是注解处理器顺序与兼容包。

10.1 典型依赖(Maven)

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.30</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.5.5.Final</version>
</dependency>

注解处理器:

<annotationProcessorPaths>
    <path>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.30</version>
    </path>
    <path>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok-mapstruct-binding</artifactId>
        <version>0.2.0</version>
    </path>
    <path>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct-processor</artifactId>
        <version>1.5.5.Final</version>
    </path>
</annotationProcessorPaths>

lombok-mapstruct-binding 在很多版本组合下是关键。


十一、MapStruct + Spring Boot 实战结构

推荐分层:

  • controller:只处理协议对象(request/response)
  • service:用 mapper 在 DTO/command/entity 之间转换
  • repository:返回 entity

示例:

@Service
public class UserService {
    private final UserRepository userRepository;
    private final UserMapper userMapper;

    public UserService(UserRepository userRepository, UserMapper userMapper) {
        this.userRepository = userRepository;
        this.userMapper = userMapper;
    }

    public UserVO get(Long id) {
        UserEntity entity = userRepository.findById(id);
        return userMapper.toVO(entity);
    }
}

十二、性能对比:MapStruct vs BeanUtils vs 手写

结论先行:绝大多数场景下,MapStruct 性能接近手写,显著优于反射型 BeanUtils。

12.1 原因

  • MapStruct:编译期生成 setter 代码
  • BeanUtils:运行期反射 + 类型检查 + 额外开销
  • 手写:最快但开发维护成本高

12.2 实战建议

  • 核心链路(批量导出、列表大分页)优先 MapStruct
  • 非关键链路小项目可灵活选型
  • 任何性能争论都应基于 JMH/压测数据

十三、最常见 12 个坑

  1. 字段漏映射却无告警
    解决:unmappedTargetPolicy = ERROR

  2. 更新场景把 null 覆盖掉老值
    解决:@MappingTarget + IGNORE

  3. Lombok 兼容异常
    解决:加 lombok-mapstruct-binding

  4. 表达式过多导致可读性差
    解决:提取具名方法

  5. 枚举新增值没处理
    解决:@ValueMapping + 编译期检查

  6. 同名不同义字段被误映射
    解决:显式 @Mapping

  7. 跨模块 mapper 扫描不到
    解决:componentModel = spring + 包扫描

  8. 集合 null 语义不一致
    解决:统一 null 策略

  9. 多个 mapper 规则不统一
    解决:@MapperConfig

  10. IDEA 看不到生成类误以为失败
    解决:确认 annotation processing 开启

  11. 自定义方法歧义(多个同签名)
    解决:@Named + qualifiedByName

  12. 单测不覆盖映射路径
    解决:为关键 mapper 建映射测试


十四、测试策略:映射层也要写测试

建议至少覆盖:

  • 正常映射
  • null 输入
  • 枚举映射
  • 更新忽略 null
  • 自定义转换正确性

示例(JUnit):

@SpringBootTest
class UserMapperTest {
    @Autowired
    private UserMapper userMapper;

    @Test
    void shouldMap() {
        UserEntity entity = new UserEntity();
        entity.setNickname("Tom");
        UserVO vo = userMapper.toVO(entity);
        assertEquals("Tom", vo.getNickname());
    }
}

十五、工程规范模板

15.1 命名约定

  • XxxMapper:映射接口
  • toVO / toDTO / toEntity:单对象转换
  • toVOList:集合转换
  • update@MappingTarget 更新

15.2 包结构建议

com.xxx.project
  ├─ mapper
  │   ├─ config
  │   │   └─ CentralMapperConfig.java
  │   ├─ common
  │   │   └─ CommonConvertMapper.java
  │   └─ user
  │       └─ UserMapper.java

15.3 强约束规则

  • 必须使用统一 @MapperConfig
  • 未映射字段编译报错
  • 禁止在 mapper 里写复杂业务逻辑
  • mapper 层必须有单测

十六、进阶:对象构建器、不可变对象、Record

MapStruct 支持 Builder 模式对象映射(尤其配合 Lombok @Builder)。

对不可变对象(只读字段)也可以通过构造器映射实现,但要注意:

  • 构造器参数名/顺序
  • Builder 规范一致

如果你使用 Java Record,也可以映射到 Record(本质是调用 canonical constructor)。


十七、与其他方案如何选

17.1 MapStruct vs BeanUtils

  • 性能:MapStruct 更优
  • 安全:MapStruct 编译期暴露问题
  • 维护:MapStruct 规则集中清晰

17.2 MapStruct vs 手写

  • 手写在极端性能场景可能略优
  • MapStruct 在大规模业务中“速度 + 可维护性”综合更好

17.3 MapStruct vs ModelMapper/Dozer

  • 后者多为运行期反射/动态映射,灵活但成本较高
  • MapStruct 偏工程稳态、偏静态类型安全

十八、迁移路线:老项目如何平滑接入

推荐分三步:

  1. 新增模块先用 MapStruct(不动旧代码)
  2. 热点模块替换手写映射(先测后替)
  3. 统一配置与规范@MapperConfig + CI 规则)

迁移原则:

  • 先外围后核心
  • 先简单对象后复杂对象
  • 每次迁移都保留回归测试

十九、FAQ

Q1:MapStruct 能做深拷贝吗?

默认是按字段赋值,不等同于“完整语义深拷贝”。复杂嵌套对象需明确定义映射策略。

Q2:字段太多不想一个个写 @Mapping

同名字段会自动映射;仅对异名字段显式配置。

Q3:能映射分页对象吗?

可以,通常映射 records 列表与分页元数据。

Q4:报 “No implementation was created” 怎么办?

先检查:

  • 注解处理器是否启用
  • mapstruct-processor 是否在 annotation processor path
  • Lombok 兼容包是否缺失

Q5:能在 Mapper 里注入 service 吗?

不建议。Mapper 负责纯映射;业务逻辑放 service 层。


二十、可复制的完整示例

@Mapper(config = CentralMapperConfig.class, uses = {CommonConvertMapper.class})
public interface OrderMapper {

    @Mapping(target = "orderNo", source = "entity.orderNo")
    @Mapping(target = "userName", source = "user.nickname")
    OrderVO toVO(OrderEntity entity, UserEntity user);

    @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
    void update(@MappingTarget OrderEntity entity, UpdateOrderRequest req);
}

这套模式适合绝大多数企业后端项目。


二十一、结语:把“对象映射”变成工程能力

很多团队把对象转换当成“脏活累活”,结果是代码里布满零散 setter,隐患很高。
MapStruct 的真正价值不只是减少代码,而是让映射规则 可审计、可测试、可演进

如果你正在做中大型 Java 项目,建议直接落地三件事:

  1. @MapperConfig 统一规范
  2. 对关键链路 mapper 补单测
  3. 开启未映射字段编译报错

做到这三点,你的转换层质量会明显上一个台阶。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐