浅显理解 Bean 映射的原理与选择
为什么需要bean映射?
在分层架构中,我们通常会看到DTO、VO、PO、DO等多种对象,他们目的只有一个:通过分层和对象隔离,实现系统解耦,让不同层的变化不会相互影响。不同层使用不同的对象:
DTO(Data Transfer Object):用于前端与后端之间的数据传输
VO(View Object):用于后端返回给前端的展示数据
DO(Domain Object):用于承载业务逻辑
PO(Persistence Object):用于与数据库表结构映射
如下图所示

解耦之后,某一层的变化不会直接影响其他层,前端字段变化 → 只影响 DTO / VO,DO和PO不一定需要加字段,由于不同层对象结构不完全一致,就需要进行转换
如何做bean映射?
Bean映射的本质就是对不同层的对象进行数据转换,我们假设这里有两个对象:
// 1. 前端传来的请求 —— DTO
public class UserCreateDTO {
private String username;
private String password;
private String email;
private String phone;
}
// 2. Service 层用的 —— DO
public class UserDO {
private Long userId;
private String username;
private String encryptedPassword;
private String email;
private String phone;
private Integer status; // DTO 里没有,需要默认值
private String createTime; // DTO 里没有,需要当前时间
}
第一种方式是手写转换器实现bean之间的转换:
// 手写 DTO-->DO
public static UserDO toDO(UserCreateDTO dto) {
if (dto == null) {
return null;
}
UserDO userDO = new UserDO();
userDO.setUsername(dto.getUsername());
// 密码加密,业务逻辑
userDO.setEncryptedPassword(md5Encrypt(dto.getPassword()));
userDO.setEmail(dto.getEmail());
userDO.setPhone(dto.getPhone());
// DTO 没有的字段,业务层自己填
userDO.setStatus(1);
userDO.setCreateTime(LocalDateTime.now().toString());
return userDO;
}
第二种方式是Spring Beanutils,如果字段不一致,会直接跳过的,需要手动补充。
public UserDO toDO(UserCreateDTO dto) {
if (dto == null) return null;
UserDO userDO = new UserDO();
BeanUtils.copyProperties(dto, userDO); // 同名字段自动拷过去
userDO.setEncryptedPassword(md5(dto.getPassword()));
userDO.setStatus(1);
userDO.setCreateTime(LocalDateTime.now().toString());
return userDO;
}
第三种,也是最推荐的一种方式是MapStruct:
@Mapper
public interface UserConverter {
@Mapping(source = "password", target = "encryptedPassword")
@Mapping(target = "userId", ignore = true)
@Mapping(target = "status", constant = "1")
@Mapping(target = "createTime", expression = "java(java.time.LocalDateTime.now().toString())")
UserDO toDO(UserCreateDTO dto);
}
// 编译后会自动生成一个实现类
public class UserConverterImpl implements UserConverter {
@Override
public UserDO toDO(UserCreateDTO dto) {
if (dto == null) return null;
UserDO userDO = new UserDO();
// 同名字段,自动拷
userDO.setUsername(dto.getUsername());
userDO.setEmail(dto.getEmail());
userDO.setPhone(dto.getPhone());
// 字段名不一样 —— @Mapping 指定的
userDO.setEncryptedPassword(dto.getPassword());
// 源没有的字段
userDO.setStatus(1);
userDO.setCreateTime(LocalDateTime.now().toString());
return userDO;
}
}
原理和区别
这里只探讨BeanUtils和MapStruct。BeanUtils 提供开箱即用的对象拷贝能力,通过运行时反射实现字段匹配,使用简单但映射是隐式的。MapStruct 则要求显式定义映射接口,通过编译期生成代码实现转换逻辑,使映射关系变得可见、可控。
还记得mapstruct我们怎么使用的吗,只需要编写接口规范,然后编译器就会自动生成实现类,如何做到的呢?
首先MapStruct的本质就是一个注解处理器,我们平时javac编译的时候,javac也是一个程序,他会调用一系列注解处理器,javac程序有这一步的。我们导入MapStrcut之后,通过SPI机制,javac会发现这个注解处理器,然后调用他的逻辑,就会去生成对应的实现类。所以MapStruct是在编译期生成实现类的。
大概流程如下:
写 @Mapper 接口
↓
javac 编译时触发注解处理器
↓
MappingProcessor 分析接口定义
↓
FreeMarker 模板渲染生成 Impl.java
↓
生成的代码就是 target.setXxx(source.getXxx())
↓
和手写一样,没有用到反射,没有额外性能消耗
那么Spring BeanUtils呢,他是通过反射技术来实现bean的映射的,大概流程如下:
调用 BeanUtils.copyProperties(source, target)
↓
获取 source 的所有 getter 方法
↓
遍历,找到 target 中同名同类型的 setter
↓
反射调用:target.setXxx(source.getXxx())
用到反射,虽然很灵活,但是反射带来的性能损失也是不能忽略的,并且spring beanutils只能映射同名的属性。
这里再提一点容易踩的坑,springutils和mapstcut默认都是是浅拷贝,但是MapStruct可以通过@Mapping指定拷贝方式: @Mapping(target = "address", source = "address", qualifiedByName = "deepCopy")

浙公网安备 33010602011771号