Chlx's Live

Back

MapStruct 就是用来做对象转换的。

Entity → DTO / VO
DTO → Entity
text

手写的话基本都是:

vo.setId(entity.getId());
vo.setName(entity.getName());
vo.setAvatar(entity.getAvatar());
java

字段多了以后,这些代码没什么价值。

MapStruct 在编译时把这些代码生成出来。


基本用法#

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

    UserVO toVO(User user);
}
java

字段名、类型都对应时,直接映射。

例如:

class User {
    private Long id;
    private String name;
}
java
class UserVO {
    private Long id;
    private String name;
}
java

编译后大致就是:

生成代码可以在:

target/generated-sources/annotations/
text

找到。

所以 MapStruct 不是运行时反射。

它做的是:

编译期生成代码

运行时执行普通 Java 代码
text

字段名不一样#

@Mapping

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

    @Mapping(source = "phone", target = "phoneNumber")
    UserVO toVO(User user);
}
java
source:源对象
target:目标对象
text

集合#

定义好单对象转换后,集合直接写:

UserVO toVO(User user);

List<UserVO> toVOList(List<User> users);
java

MapStruct 会生成遍历代码。


更新对象#

void update(
    UserUpdateDTO dto,
    @MappingTarget User user
);
java

直接修改传进来的 user,不会重新创建。


什么时候适合用#

MapStruct 适合做比较纯粹的对象转换:

Entity → VO
Entity → DTO
DTO → Entity

字段改名
简单类型转换
集合转换
嵌套对象转换
text

例如:

@Mapping(source = "phone", target = "phoneNumber")
UserVO toVO(User user);
java

这种事情交给 MapStruct 就行。


什么时候别用#

这里比记 API 更重要。

要查数据库#

例如:

User

根据 departmentId 查 Department

组装 UserVO
text

这不是 Mapper 的职责。

放 Service。

Service

查询数据

组装

Mapper
text

有业务计算#

例如:

VIP 八折
普通用户原价
活动期间再减免
text

这是业务逻辑。

别写成一堆:

expression = "java(...)"
java

塞进 Mapper。

放 Service / Domain。


要调用外部服务#

例如:

订单

用户服务

物流服务

优惠券服务

OrderVO
text

这也不是 Mapper 应该管的。


Mapper 已经开始很复杂#

如果一个 Mapper 里面出现:

大量 expression
大量条件判断
查询数据库
调用其他服务
复杂业务方法
text

基本就该拆。

一个简单的判断标准:

只是把 A 的数据搬到 B,用 MapStruct。

需要决定业务怎么做,不要用 MapStruct。


Entity 和 VO 为什么分开#

Entity 和 VO 是两种不同的模型。

Entity 面向数据库:

class User {
    private Long id;
    private String password;
    private Integer isDeleted;
    private String avatar;
}
java

VO 面向接口:

class UserVO {
    private Long id;
    private String avatar;
}
java

这样 Entity 增加字段时,不会自动把字段暴露给前端。

另外,VO 可以有接口层自己的序列化规则。

例如数据库里存:

avatar/abc.jpg
text

VO:

@JsonSerialize(using = CdnUrlSerializer.class)
private String avatar;
java

流程就是:

Entity

MapStruct

VO

Jackson

CdnUrlSerializer

JSON
text

MapStruct 只负责把 avatar 搬过去。

CDN URL 怎么生成,是序列化器的事情。


性能#

MapStruct 官方没有给一个固定的“快多少倍”的数字。

官方能确定的是:

  • 编译期生成实现类

  • 生成代码使用普通方法调用

  • 不依赖运行时反射

  • 编译期做映射检查

因此它的执行路径基本就是手写 Mapper 那套 getter / setter。

第三方 JMH 测试也能看到类似结果。例如一组公开基准中:

实现单次映射
手写12.94 ns
MapStruct13.50 ns
Orika174.78 ns
ModelMapper4074.94 ns

MapStruct 和手写代码非常接近。

不过这只是特定测试条件下的结果,不能理解成 MapStruct 在所有场景下都固定快这么多。

实际项目里也没必要为了这点转换性能做过度优化。数据库、网络、Redis、序列化这些通常更值得关注。


AI 时代怎么用#

现在写:

@Mapping(source = "phone", target = "phoneNumber")
java

这种代码,AI 基本可以直接生成。

所以没必要把 MapStruct 的所有 API 背下来。

记住几个常用的:

@Mapper
@Mapping
@Named
@MappingTarget
@InheritInverseConfiguration
java

不会的直接查。

真正需要自己判断的是:

为什么要做这个转换?
哪些字段应该暴露?
这个逻辑应该放 Mapper 还是 Service?
有没有漏字段?
AI 有没有塞进去一堆 expression?
text

代码可以让 AI 写。

分层和职责还是自己定。


项目里的常用写法#

@Mapper(
    componentModel = "spring",
    unmappedTargetPolicy = ReportingPolicy.ERROR
)
public interface UserMapper {

    @Mapping(source = "phone", target = "phoneNumber")
    UserVO toVO(User user);

    List<UserVO> toVOList(List<User> users);
}
java

unmappedTargetPolicy = ReportingPolicy.ERROR 比较实用。

VO 新增字段但 Mapper 没处理时,直接编译报错。


总结#

MapStruct 没什么好复杂的。

MapStruct

对象转换

Service / Domain

业务逻辑

Jackson

JSON
text

记住:

能不能用 MapStruct,不看代码能不能写,而看这件事是不是“对象映射”。

只是:

A → B
text

用 MapStruct。

如果已经变成:

查数据
算业务
调服务
做流程
text

就别往 Mapper 里塞。

AI 负责生成具体代码。

程序员需要判断的是对象边界和职责。

AI时代下MapStruct简单指南:优雅解决Java Bean映射难题
https://lixuan.live/blog/ai-shi-dai-xia-mapstruct-jian-dan-zhi-nan-you-ya-jie-jue-java-bean-ying-she-nan-ti
Author Chlx
Published at 2026年4月18日
Comment seems to stuck. Try to refresh?✨