关于 mapstruct
面向 Java 开发者的 MapStruct 映射参考技能,涵盖 DTO 与实体间映射、更新策略及常见问题。还提供 Spring、Lombok 集成和映射实践建议。
主要功能
- 01对象与集合映射
- 02字段及嵌套对象映射
- 03部分与完整更新
- 04枚举和条件映射
- 05映射前后自定义处理
- 06配置与故障排查
适用场景
- 01将实体转换为响应 DTO
- 02用 DTO 更新现有实体
- 03映射不同字段名和嵌套类型
MCP 相关
提及可通过 `mcp__documentation__fetch_docs` 获取 MapStruct 的深入文档。
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ claude-dev-suite/ claude-dev-suite --skill mapstruct安装后在 Claude Code 中输入/mapstruct确认已加载。
详细介绍
MapStruct 对象映射
深入了解:使用 technology 为
mapstruct的mcp__documentation__fetch_docs获取完整文档。
基本映射器
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface UserMapper {
UserResponse toResponse(User user);
List<UserResponse> toResponseList(List<User> users);
@Mapping(target = "id", ignore = true)
@Mapping(target = "createdAt", ignore = true)
@Mapping(target = "updatedAt", ignore = true)
@Mapping(target = "password", ignore = true)
User toEntity(CreateUserRequest dto);
}
更新映射
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface UserMapper {
// Partial update - ignores null values
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void updateEntity(UpdateUserRequest dto, @MappingTarget User user);
// Full update - sets all fields including nulls
void updateEntityFull(UpdateUserRequest dto, @MappingTarget User user);
}
字段映射
@Mapper(componentModel = "spring")
public interface OrderMapper {
// Different field names
@Mapping(source = "customer.name", target = "customerName")
@Mapping(source = "customer.email", target = "customerEmail")
@Mapping(source = "items", target = "orderItems")
OrderResponse toResponse(Order order);
// Constant values
@Mapping(target = "status", constant = "PENDING")
@Mapping(target = "createdAt", expression = "java(java.time.LocalDateTime.now())")
Order toEntity(CreateOrderRequest dto);
// Expression
@Mapping(target = "fullName", expression = "java(user.getFirstName() + \" \" + user.getLastName())")
UserResponse toResponse(User user);
// Date formatting
@Mapping(source = "createdAt", target = "createdDate", dateFormat = "yyyy-MM-dd")
OrderResponse toResponse(Order order);
}
嵌套对象
@Mapper(componentModel = "spring", uses = {AddressMapper.class, ItemMapper.class})
public interface OrderMapper {
// Uses AddressMapper for address field
// Uses ItemMapper for items collection
OrderResponse toResponse(Order order);
}
@Mapper(componentModel = "spring")
public interface AddressMapper {
AddressResponse toResponse(Address address);
}
@Mapper(componentModel = "spring")
public interface ItemMapper {
ItemResponse toResponse(Item item);
}
枚举映射
@Mapper(componentModel = "spring")
public interface StatusMapper {
@ValueMappings({
@ValueMapping(source = "ACTIVE", target = "ENABLED"),
@ValueMapping(source = "INACTIVE", target = "DISABLED"),
@ValueMapping(source = MappingConstants.ANY_REMAINING, target = "UNKNOWN")
})
ExternalStatus toExternalStatus(InternalStatus status);
}
自定义方法
@Mapper(componentModel = "spring")
public abstract class UserMapper {
@Autowired
protected RoleRepository roleRepository;
public abstract UserResponse toResponse(User user);
@Mapping(target = "roles", source = "roleIds")
public abstract User toEntity(CreateUserRequest dto);
// Custom mapping method
protected Set<Role> mapRoles(Set<Long> roleIds) {
if (roleIds == null) return new HashSet<>();
return roleIds.stream()
.map(id -> roleRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Role", "id", id)))
.collect(Collectors.toSet());
}
}
集合映射
@Mapper(componentModel = "spring")
public interface ProductMapper {
ProductResponse toResponse(Product product);
List<ProductResponse> toResponseList(List<Product> products);
Set<ProductResponse> toResponseSet(Set<Product> products);
// Page mapping
default Page<ProductResponse> toResponsePage(Page<Product> products) {
return products.map(this::toResponse);
}
}
映射前/后处理
@Mapper(componentModel = "spring")
public abstract class UserMapper {
@AfterMapping
protected void afterMapping(@MappingTarget UserResponse response, User user) {
response.setDisplayName(user.getFirstName() + " " + user.getLastName().charAt(0) + ".");
}
@BeforeMapping
protected void beforeMapping(CreateUserRequest dto) {
if (dto.getEmail() != null) {
dto.setEmail(dto.getEmail().toLowerCase().trim());
}
}
}
条件映射
@Mapper(componentModel = "spring")
public abstract class UserMapper {
@Condition
public boolean isNotEmpty(String value) {
return value != null && !value.trim().isEmpty();
}
// Only maps non-empty strings
UserResponse toResponse(User user);
}
Maven 配置
<properties>
<mapstruct.version>1.6.2</mapstruct.version>
<lombok.version>1.18.30</lombok.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>17</source>
<target>17</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>-Amapstruct.defaultComponentModel=spring</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
关键注解
| 注解 | 用途 |
|---|---|
@Mapper | 定义映射器接口/抽象类 |
@Mapping | 配置字段级映射 |
@MappingTarget | 更新现有对象 |
@BeanMapping | 配置 Bean 级映射 |
@AfterMapping | 后处理方法 |
@BeforeMapping | 预处理方法 |
@Condition | 条件映射 |
@ValueMapping | 枚举值映射 |
最佳实践
- 在 Spring 注入中使用
componentModel = "spring" - 对字段较少的 DTO 使用
unmappedTargetPolicy = ReportingPolicy.IGNORE - 从 DTO 映射时,始终排除
id、createdAt、updatedAt - 使用
@MappingTarget进行部分更新 - 按顺序配置注解处理器:Lombok → MapStruct → Binding
- 自定义逻辑使用抽象类而不是接口
- 使用
@Condition实现条件映射逻辑
不适用此技能的场景
| 场景 | 替代方案 |
|---|---|
| Java 语言特性 | java skill |
| Lombok 注解 | lombok skill |
| Spring 配置 | backend-spring-boot skill |
| JPA 实体操作 | JPA 专用技能 |
| 简单复制 | 手动映射或 BeanUtils |
反模式
| 反模式 | 缺点 | 正确做法 |
|---|---|---|
| 实体映射到实体 | 会破坏变更跟踪 | 将 DTO 映射到实体 |
| 不使用 @MappingTarget | 更新效率低 | 用于部分更新 |
| 在表达式中编写复杂逻辑 | 难以测试 | 使用自定义方法 |
| 忽略所有未映射项 | 会静默遗漏字段 | 使用 WARN 或 ERROR 策略 |
| 不排除审计字段 | 会覆盖元数据 | 排除 id 和时间戳 |
| 循环引用 | StackOverflowError | 使用自定义映射打破循环 |
| 使用接口实现自定义逻辑 | 无法注入依赖 | 使用抽象类 |
| 不测试映射器 | 运行时映射错误 | 编写映射器测试 |
快速故障排查
| 问题 | 原因 | 解决方案 |
|---|---|---|
| "Cannot find implementation" | 注解处理失败 | 检查处理器配置 |
| 找不到 Lombok 字段 | 处理器顺序错误 | 将 Lombok 放在 MapStruct 之前 |
| 映射器未自动装配 | componentModel 错误 | 使用 componentModel = "spring" |
| 循环依赖 | 映射器相互引用 | 使用 @Lazy 或重构 |
| UnmappedTargetProperty 警告 | 缺少映射 | 添加 @Mapping 或忽略策略 |
| 映射时出现 NullPointerException | 源对象为 null | 添加空值检查或使用 NullValuePropertyMappingStrategy |
| 未调用自定义方法 | 方法签名错误 | 确保方法参数完全匹配 |
| 生成的代码未更新 | IDE 缓存 | 清理并重新构建项目 |
参考文档
深入了解:使用 technology 为
mapstruct的mcp__documentation__fetch_docs获取完整文档。
评论
支持 Markdown