这个过程在软件开发中无处不在,尤其是在处理数据传输对象、API 响应、数据库实体等场景下。

下面我将从核心概念、多种实现方式、最佳实践三个方面,详细为你解释这个问题。
核心概念:为什么需要转换?
我们需要在两种或多种不同结构的对象之间转换数据,主要原因有:
-
分层架构:在典型的三层架构中,各层的数据模型是不同的。
- 数据库层 (Entity/DO):
User.java(与数据库表结构一一对应) - 业务逻辑层 (Domain/BO):
UserProfile.java(包含业务逻辑所需的所有信息) - 表现层/数据传输层 (DTO):
UserDTO.java(用于 API 请求和响应,只包含必要的信息) - 当数据从数据库流向 API 时,需要将
User转换成UserDTO。
- 数据库层 (Entity/DO):
-
数据隐藏与安全:不希望将数据库实体(可能包含敏感信息如密码哈希、内部ID等)直接暴露给前端,DTO 可以只包含用户名、邮箱等安全信息。
(图片来源网络,侵删) -
API 契约:外部 API 的数据结构与你内部的 Java 对象结构不匹配,需要进行转换。
多种实现方式
从最基础到最现代,主要有以下几种转换方式。
手动转换(最基础、最直接)
这是最原始的方式,通过 getter 和 setter 逐个属性进行赋值。
场景:两个类结构简单且属性不多时。
示例:
// 源对象 - 数据库实体
class UserEntity {
private Long id;
private String username;
private String passwordHash;
private String email;
// Constructor, Getters, Setters...
public UserEntity(Long id, String username, String passwordHash, String email) {
this.id = id;
this.username = username;
this.passwordHash = passwordHash;
this.email = email;
}
public Long getId() { return id; }
public String getUsername() { return username; }
public String getPasswordHash() { return passwordHash; }
public String getEmail() { return email; }
}
// 目标对象 - 数据传输对象
class UserDTO {
private String username;
private String email;
// Constructor, Getters, Setters...
public UserDTO(String username, String email) {
this.username = username;
this.email = email;
}
public String getUsername() { return username; }
public String getEmail() { return email; }
public void setUsername(String username) { this.username = username; }
public void setEmail(String email) { this.email = email; }
}
// 转换代码
public class ManualConversion {
public UserDTO convertToDto(UserEntity entity) {
UserDTO dto = new UserDTO();
dto.setUsername(entity.getUsername());
dto.setEmail(entity.getEmail());
// 忽略 entity 中的 id 和 passwordHash
return dto;
}
}
- 优点:
- 简单直观,不依赖任何第三方库。
- 性能最高,没有反射等额外开销。
- 缺点:
- 代码冗余:每个转换方法都要写一遍,非常枯燥。
- 维护困难:
UserEntity新增了age属性,所有相关的转换方法都需要手动修改,容易遗漏。 - 容易出错:手动拼写属性名时容易出错。
使用第三方工具库(主流方式)
当项目变大,对象关系变复杂时,手动转换变得不可维护,这时,成熟的第三方工具库就派上用场了,它们通过反射或字节码生成技术,实现自动化转换。
MapStruct (强烈推荐)
MapStruct 是一个代码生成器,它在编译期生成转换代码,这意味着它没有运行时性能开销,就像你亲手写的一样,同时又能享受自动化的便利。
步骤:
-
添加依赖 (Maven):
<dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>1.5.5.Final</version> <!-- 使用最新版本 --> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.26</version> <scope>provided</scope> </dependency> -
定义 Mapper 接口:
import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.factory.Mappers; // @Mapper 注解告诉 MapStruct 这是一个映射接口 @Mapper public interface UserMapper { // 获取此 Mapper 的实例 UserMapper INSTANCE = Mappers.getMapper(UserMapper.class); // 定义映射规则 // source 是源对象的属性,target 是目标对象的属性 // 可以忽略不需要映射的属性,如 id 和 passwordHash @Mapping(target = "username", source = "username") @Mapping(target = "email", source = "email") UserDTO toUserDTO(UserEntity userEntity); } -
使用:
public class Main { public static void main(String[] args) { UserEntity entity = new UserEntity(1L, "john_doe", "hashed_password", "john@example.com"); // 调用接口方法,MapStruct 会自动生成实现 UserDTO dto = UserMapper.INSTANCE.toUserDTO(entity); System.out.println(dto.getUsername()); // 输出: john_doe System.out.println(dto.getEmail()); // 输出: john@example.com } }
- 优点:
- 高性能:编译期生成代码,运行时性能与手写代码无异。
- 类型安全:在编译阶段就能检查出映射错误。
- 可读性强:代码简洁,意图明确。
- 缺点:
需要额外引入依赖和配置。
ModelMapper
ModelMapper 是一个运行时反射库,使用起来非常简单,但性能不如 MapStruct。
步骤:
-
添加依赖 (Maven):
<dependency> <groupId>org.modelmapper</groupId> <artifactId>modelmapper</artifactId> <version>3.1.1</version> </dependency> -
使用:
import org.modelmapper.ModelMapper; public class Main { public static void main(String[] args) { ModelMapper modelMapper = new ModelMapper(); UserEntity entity = new UserEntity(1L, "john_doe", "hashed_password", "john@example.com"); // 直接进行映射 UserDTO dto = modelMapper.map(entity, UserDTO.class); System.out.println(dto.getUsername()); // 输出: john_doe System.out.println(dto.getEmail()); // 输出: john@example.com } }
- 优点:
API 极其简洁,一行代码搞定。
- 缺点:
- 性能较低:基于反射,且在运行时进行类型检查和转换,性能开销较大。
- 调试困难:错误在运行时才暴露,且难以追踪内部转换逻辑。
Spring BeanUtils (仅适用于属性名完全相同的情况)
如果你的源对象和目标对象属性名和类型都完全一样,可以使用 Spring 框架自带的工具。
import org.springframework.beans.BeanUtils;
public class Main {
public static void main(String[] args) {
UserEntity entity = new UserEntity(1L, "john_doe", "hashed_password", "john@example.com");
UserDTO dto = new UserDTO();
// 将 entity 的属性复制到 dto
// 注意:这要求两个类必须有完全相同的属性名和类型
// 我们的例子中,UserEntity 多了 id 和 passwordHash,所以不能直接复制
// 但如果只是复制部分同名属性,可以这样:
BeanUtils.copyProperties(entity, dto, "id", "passwordHash");
System.out.println(dto.getUsername()); // 输出: john_doe
System.out.println(dto.getEmail()); // 输出: john@example.com
}
}
- 优点:
- 无需额外依赖(如果项目已用 Spring)。
- 简单。
- 缺点:
- 功能有限:只支持属性名完全相同的映射,灵活性差。
- 性能一般:底层也是反射。
最佳实践与总结
| 特性 | 手动转换 | MapStruct (推荐) | ModelMapper | Spring BeanUtils |
|---|---|---|---|---|
| 性能 | 极高 | 极高 (编译期生成) | 较低 (运行时反射) | 一般 (反射) |
| 易用性 | 差 (代码冗余) | 好 (接口定义) | 极好 (一行代码) | 好 (简单场景) |
| 可维护性 | 差 | 好 (类型安全,编译期检查) | 差 (运行时错误) | 一般 (功能受限) |
| 灵活性 | 高 (可写任意逻辑) | 高 (支持复杂映射) | 中等 | 低 |
| 依赖 | 无 | 需 MapStruct 依赖 | 需 ModelMapper 依赖 | 需 Spring 框架 |
如何选择?
-
新项目,追求高性能和可维护性:首选 MapStruct,这是目前业界公认的最佳实践,尤其在 Spring Boot 项目中广泛使用,它完美平衡了性能、功能和开发体验。
-
快速原型开发或小型项目:如果对象结构非常简单,转换逻辑极少,可以考虑 Spring BeanUtils 或手动转换。
-
追求极致的代码简洁性,且不介意性能损失:可以考虑 ModelMapper,但在生产环境中,特别是对性能有要求的场景,应谨慎使用。
-
避免:在大型、复杂的项目中,大量使用手动转换,这会导致代码泥潭,难以维护。
“Java object 转 对象” 的核心是数据模型转换,从手动转换的“笨办法”,到 MapStruct 的“工业级标准解决方案”,这个演进过程体现了对工程效率和代码质量的不断追求。
对于绝大多数现代 Java 应用程序,MapStruct 是你的不二之选,它让你从繁琐的 getter/setter 中解放出来,专注于业务逻辑,同时保证了代码的高性能和高可维护性。
