BeanUtils(对象复制工具)
# BeanUtils(对象复制工具)
前言
在 Java 应用开发,尤其是在分层的架构设计中(如常见的 MVC 模式),我们经常需要在不同的对象之间传递数据。例如,从数据库查询出的实体对象(Entity/POJO)需要转换为数据传输对象(DTO)以供业务逻辑层使用,或者将 DTO 转换为视图对象(VO)以适应前端展示的需求。手动编写大量的 getter/setter
调用来进行属性复制,不仅代码冗余、枯燥乏味,而且极易引入错误(如漏掉字段、类型不匹配等)。BeanUtils
应运而生,它提供了一种便捷、自动化的方式来完成对象间的属性复制,从而显著提升开发效率,减少样板代码,让开发者更专注于核心业务逻辑。
# 一、BeanUtils 简介与核心原理
# 1.1 什么是 BeanUtils?
BeanUtils
并非特指某一个类,而是一类用于操作 Java Bean(符合特定规范的 Java 对象,通常拥有私有属性和公共的 getter/setter
方法)的工具类的统称。它们的核心功能是在两个不同的 Java 对象实例之间,根据一定的规则(通常是属性名相同)自动复制属性值。
市面上存在多种流行的 BeanUtils
实现,主要包括:
- Apache Commons BeanUtils: 来自著名的 Apache Commons 项目,功能较为丰富,提供了属性复制、属性访问、类型转换等多种功能。但其早期版本因性能问题和复杂的类型转换逻辑而受到一些批评。
- Spring Framework BeanUtils: Spring 框架自带的工具类 (
org.springframework.beans.BeanUtils
),专注于属性复制,设计简洁,性能相较于 Apache Commons BeanUtils 有明显提升,是 Spring 生态中最常用的选择。 - CGLib BeanCopier: CGLib 库提供的工具。它通过在运行时动态生成字节码的方式来实现属性复制,性能非常高,接近手动
getter/setter
,但使用上相对复杂一些,且不提供自动类型转换。 - 其他库: 如 MapStruct、ModelMapper 等,它们提供了更高级的对象映射功能,通常基于注解处理器或运行时配置,性能和功能各有侧重(后文会简单介绍)。
本文将重点介绍和对比 Spring BeanUtils 和 Apache Commons BeanUtils 这两个最常用的实现。
# 1.2 BeanUtils 的工作原理:反射机制
BeanUtils
工具的核心是利用 Java 反射 (Reflection) 机制。其基本工作流程大致如下:
- 内省与属性发现 (Introspection & Discovery): 工具类通过 Java 反射 API(如
java.beans.Introspector
或直接访问Class
对象的方法)来分析源对象 (source
) 和目标对象 (target
) 的类结构,获取它们各自拥有的公开属性(通常是通过getter
和setter
方法识别)。 - 属性匹配 (Property Matching): 遍历源对象的可读属性(有
getter
方法)和目标对象的可写属性(有setter
方法),寻找名称相同的属性对。这是最基础的匹配规则。 - 类型兼容性检查与转换 (Type Compatibility & Conversion): 对于名称匹配的属性对,检查它们的类型是否兼容。
- 如果类型完全相同,可以直接赋值。
- 如果类型不同但兼容(如
int
和Integer
),大多数BeanUtils
实现能自动处理。 - 对于不兼容的类型(如
String
转Integer
),不同的实现处理方式不同:Spring BeanUtils 通常要求类型兼容,否则会忽略或报错;Apache Commons BeanUtils 则会尝试进行自动类型转换(使用ConvertUtils
),但这有时可能导致意外结果或性能下降。
- 值复制 (Value Copying): 如果属性匹配且类型兼容(或可转换),工具类通过反射调用源对象的
getter
方法获取属性值,然后通过反射调用目标对象的setter
方法将该值设置进去。
示意图:
/**
* BeanUtils 工作原理示意图 (基于反射)
*
* 源对象 (Source Object) <-- 反射 --> 目标对象 (Target Object)
* +---------------------+ +---------------------+
* | class User | | class UserDTO |
* |---------------------| |---------------------|
* | private Integer id; | --(getter: getId())--> 值(1) ----->| private Integer id; | (setter: setId(1))--
* | private String name;| --(getter: getName())--> 值("张三")-->| private String name;| (setter: setName("张三"))--
* | private Integer age;| --(getter: getAge())--> 值(30) ---->| private Integer age;| (setter: setAge(30))--
* +---------------------+ +---------------------+
* | ^
* | <---- 1. 获取属性描述符 (PropertyDescriptors) ---------- |
* | <---- 2. 匹配同名属性 (e.g., "id", "name", "age") ------ |
* | <---- 3. 检查类型兼容性 -------------------------------- |
* | <---- 4. 通过反射调用 getter/setter -------------------- |
*/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
理解反射原理有助于我们认识到 BeanUtils
的便利性是以一定的性能开销为代价的(反射调用通常比直接方法调用慢)。
# 二、Spring BeanUtils 详解
Spring Framework 提供的 BeanUtils
类 (org.springframework.beans.BeanUtils
) 是 Spring 应用中最常用的属性复制工具。它简洁、高效,且与 Spring 生态无缝集成。
依赖配置:
如果你的项目是标准的 Spring Boot 项目,那么 spring-boot-starter
中已经包含了 spring-beans
依赖,无需额外添加。
<!-- Spring Boot 项目通常已包含 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<!-- 使用你的 Spring Boot 版本 -->
</dependency>
2
3
4
5
6
如果是传统的 Spring Framework 项目,或者需要单独使用,请确保添加 spring-beans
依赖:
<!-- Maven 配置 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.3.23</version> <!-- 使用与你项目兼容的 Spring 版本 -->
</dependency>
2
3
4
5
6
// Gradle 配置
implementation 'org.springframework:spring-beans:5.3.23' // 使用兼容版本
2
# 2.1 基本用法:copyProperties
核心方法是 copyProperties(Object source, Object target)
,它将 source
对象中所有名称和类型兼容的可读属性值复制到 target
对象对应的可写属性中。
import org.springframework.beans.BeanUtils; // 引入 Spring 的 BeanUtils
import lombok.Data; // 使用 Lombok 简化代码,可选
import java.util.Date;
/**
* Spring BeanUtils 基本用法示例
*/
public class SpringBeanUtilsDemo {
public static void main(String[] args) {
// 1. 创建源对象 (通常是 Entity 或 POJO)
User sourceUser = new User();
sourceUser.setId(101);
sourceUser.setName("张三");
sourceUser.setAge(30);
sourceUser.setEmail("zhangsan@example.com");
sourceUser.setCreateTime(new Date());
sourceUser.setUpdateTime(new Date()); // User 有 updateTime 字段
// 2. 创建目标对象 (通常是 DTO 或 VO)
UserDTO targetDTO = new UserDTO();
// targetDTO 的初始状态:所有字段为 null 或默认值
// 3. 使用 Spring BeanUtils 的 copyProperties 方法进行属性复制
// 参数1: 源对象 (source)
// 参数2: 目标对象 (target)
// 逻辑:遍历 source 的可读属性,找到 target 中同名且类型兼容的可写属性,然后复制值。
BeanUtils.copyProperties(sourceUser, targetDTO);
// 4. 输出复制后的目标对象内容
System.out.println("复制后的 DTO 对象: " + targetDTO);
// 预期输出:UserDTO{id=101, name='张三', age=30, email='zhangsan@example.com', createTime=当前时间}
// 注意:sourceUser 的 updateTime 字段在 targetDTO 中不存在,因此会被自动忽略。
}
}
/**
* 用户实体类 (源对象)
* 假设这是从数据库或其他来源获取的数据模型
*/
@Data // Lombok 注解,自动生成 getter, setter, toString, equals, hashCode
class User {
private Integer id; // 用户唯一标识
private String name; // 用户名称
private Integer age; // 用户年龄
private String email; // 电子邮箱地址
private Date createTime; // 记录创建时间
private Date updateTime; // 记录最后更新时间
}
/**
* 用户数据传输对象 (目标对象)
* 用于在不同层之间传递数据,可能只包含部分字段
*/
@Data
class UserDTO {
private Integer id; // 用户 ID
private String name; // 用户名称
private Integer age; // 用户年龄
private String email; // 电子邮箱
private Date createTime; // 创建时间
// 注意:此 DTO 中没有 updateTime 字段
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
关键点:
- 只复制名称相同且类型兼容的属性。
- 如果源对象的属性值为
null
,null
值也会被复制到目标对象(除非目标属性是基本类型,此时可能抛异常或保持默认值,取决于具体情况和 Spring 版本)。 - 源对象或目标对象中独有的属性会被忽略。
- 性能相对较好,内部有对反射信息的缓存。
# 2.2 忽略指定属性
有时我们不希望复制某些敏感字段(如密码)或不需要的字段。可以使用 copyProperties
的重载方法,传入一个 String
数组来指定要忽略的属性名。
import org.springframework.beans.BeanUtils;
import lombok.Data;
import java.util.Date;
/**
* Spring BeanUtils 忽略指定属性示例
*/
public class SpringIgnorePropertiesDemo {
public static void main(String[] args) {
// 创建源对象
User sourceUser = new User();
sourceUser.setId(102);
sourceUser.setName("李四");
sourceUser.setAge(28);
sourceUser.setEmail("lisi@example.com");
sourceUser.setCreateTime(new Date());
// 假设有一个敏感字段
sourceUser.setPassword("secret123");
// 创建目标对象
UserDTO targetDTO = new UserDTO();
// 复制属性,但忽略 'id' 和 'password' 字段
// 参数3: 一个包含要忽略的属性名称的字符串数组
BeanUtils.copyProperties(sourceUser, targetDTO, "id", "password");
// 输出结果
System.out.println("忽略部分属性复制后的 DTO 对象: " + targetDTO);
// 预期输出:UserDTO{id=null, name='李四', age=28, email='lisi@example.com', createTime=当前时间}
// id 因为被忽略,所以是 null。password 在 UserDTO 中不存在,也会被忽略。
}
// 假设 User 类增加 password 字段
@Data
static class User {
private Integer id;
private String name;
private Integer age;
private String email;
private Date createTime;
private String password; // 新增敏感字段
private Date updateTime;
}
// UserDTO 保持不变
@Data
static class UserDTO {
private Integer id;
private String name;
private Integer age;
private String email;
private Date createTime;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# 2.3 处理名称不同的属性(Spring BeanUtils 的局限)
Spring BeanUtils 不直接支持名称不同的属性之间的映射。如果源和目标对象的属性名称不一致(例如 Java 驼峰 userId
vs 数据库下划线 user_id
),copyProperties
无法自动处理。这种情况下,你需要:
- 先使用
copyProperties
复制所有同名属性。 - 手动调用
getter
和setter
来处理名称不同的属性。
import org.springframework.beans.BeanUtils;
import lombok.Data;
/**
* Spring BeanUtils 处理不同名称属性的示例 (需要手动辅助)
*/
public class SpringDifferentNamesDemo {
public static void main(String[] args) {
// 源对象 (User 类定义见上文)
User sourceUser = new User();
sourceUser.setId(103);
sourceUser.setName("王五");
sourceUser.setAge(35); // User 有 age 字段
// 目标对象 (属性名不同)
UserViewModel targetViewModel = new UserViewModel();
// targetViewModel 有 userId, userName, 但没有 age
// 1. 使用 copyProperties 复制同名属性 (如果有的话)
// 在这个例子中,没有同名属性会被复制
BeanUtils.copyProperties(sourceUser, targetViewModel);
System.out.println("仅 copyProperties 后 (无同名属性): " + targetViewModel);
// 预期输出: UserViewModel{userId=null, userName=null}
// 2. 手动处理名称不同的属性映射
targetViewModel.setUserId(sourceUser.getId()); // 手动将 User.id 复制到 UserViewModel.userId
targetViewModel.setUserName(sourceUser.getName()); // 手动将 User.name 复制到 UserViewModel.userName
// sourceUser 的 age 字段在 targetViewModel 中没有对应,被忽略
System.out.println("手动处理不同名称属性后: " + targetViewModel);
// 预期输出: UserViewModel{userId=103, userName='王五'}
}
// 假设 User 类定义如前
@Data
static class User {
private Integer id;
private String name;
private Integer age;
// ... 其他字段
}
/**
* 用户视图模型 (属性名与 User 类不同)
* 用于前端展示的模型
*/
@Data
static class UserViewModel {
private Integer userId; // 对应 User.id
private String userName; // 对应 User.name
// 注意:这里没有 age 属性
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
对于需要大量处理不同名称属性映射的场景,Spring BeanUtils 可能不是最高效的选择,可以考虑使用 MapStruct 等更专业的映射工具(见后文)。
# 2.4 在业务层(Service Layer)中使用 BeanUtils
BeanUtils
在实际项目中常用于 Service 层,处理 Entity、DTO、VO 之间的转换。
// 假设环境:Spring Boot 项目
// 需要引入相关依赖: spring-boot-starter-data-jpa, lombok, javax.persistence-api 等
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; // 用于事务管理
import java.util.Date;
import java.util.List;
import java.util.Optional;
import javax.persistence.EntityNotFoundException; // 假设自定义了异常
import java.beans.PropertyDescriptor; // 用于 copyNonNullProperties
import java.lang.reflect.Method; // 用于 copyNonNullProperties
import java.util.HashSet; // 用于 copyNonNullProperties
import java.util.Set; // 用于 copyNonNullProperties
import org.springframework.beans.BeanWrapper; // 用于 copyNonNullProperties
import org.springframework.beans.BeanWrapperImpl; // 用于 copyNonNullProperties
import java.util.Collections; // 用于 copyList
import java.util.ArrayList; // 用于 copyList
import java.util.Collection; // 用于 copyPropertiesDeep
// 假设存在 UserRepository 接口 (extends JpaRepository<User, Integer>)
// @Repository
// interface UserRepository extends org.springframework.data.jpa.repository.JpaRepository<User, Integer> {}
// 假设 User, UserDTO 类定义如前
/**
* 用户服务类,演示在业务逻辑中使用 Spring BeanUtils
*/
@Service // 标记为 Spring Service 组件
public class UserService {
private final UserRepository userRepository; // 依赖注入 UserRepository
/**
* 通过构造器注入 UserRepository 依赖
* @param userRepository 数据访问仓库
*/
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
/**
* 根据用户 ID 获取用户详细信息 (DTO)
* @param userId 要查询的用户 ID
* @return 包含用户信息的 UserDTO
* @throws EntityNotFoundException 如果用户不存在
*/
@Transactional(readOnly = true) // 标记为只读事务,提高性能
public UserDTO getUserDetails(Integer userId) {
// 1. 从数据库或其他数据源获取用户实体 (Entity)
User userEntity = userRepository.findById(userId)
.orElseThrow(() -> new EntityNotFoundException("用户不存在,ID: " + userId));
// 2. 创建数据传输对象 (DTO)
UserDTO userDTO = new UserDTO();
// 3. 使用 BeanUtils 将实体属性复制到 DTO
BeanUtils.copyProperties(userEntity, userDTO);
// 4. (可选) 执行额外的业务逻辑或数据转换
// 例如,格式化日期、计算派生字段、屏蔽敏感信息等
enrichUserDTO(userDTO, userEntity);
// 5. 返回 DTO
return userDTO;
}
/**
* 创建新用户
* @param createUserRequestDTO 包含新用户信息的 DTO
* @return 创建成功后的用户 DTO (通常包含生成的 ID 和时间戳)
*/
@Transactional // 标记为读写事务
public UserDTO createUser(UserDTO createUserRequestDTO) {
// 1. 将传入的 DTO 转换为实体对象 (Entity)
User newUserEntity = new User();
BeanUtils.copyProperties(createUserRequestDTO, newUserEntity);
// 2. 设置实体的默认值或计算值 (如创建时间、初始状态等)
Date now = new Date();
newUserEntity.setCreateTime(now);
newUserEntity.setUpdateTime(now);
// newUserEntity.setStatus("ACTIVE"); // 假设有状态字段
// 3. 调用 Repository 将实体保存到数据库
User savedUserEntity = userRepository.save(newUserEntity);
// 4. 将保存后的实体 (可能包含数据库生成的 ID) 转换回 DTO 以便返回给调用者
UserDTO resultDTO = new UserDTO();
BeanUtils.copyProperties(savedUserEntity, resultDTO);
return resultDTO;
}
/**
* 更新现有用户信息 (部分更新)
* @param userId 要更新的用户 ID
* @param updateUserRequestDTO 包含要更新字段的 DTO (通常只包含需要修改的字段)
* @return 更新成功后的用户 DTO
* @throws EntityNotFoundException 如果用户不存在
*/
@Transactional
public UserDTO updateUser(Integer userId, UserDTO updateUserRequestDTO) {
// 1. 从数据库加载现有的用户实体
User existingUserEntity = userRepository.findById(userId)
.orElseThrow(() -> new EntityNotFoundException("用户不存在,ID: " + userId));
// 2. **选择性地**将 DTO 中的非 null 属性复制到实体中
// 注意:Spring BeanUtils 自身不直接支持 "仅复制非 null 属性"
// 这里我们调用一个自定义的辅助方法来实现此逻辑 (见下方)
copyNonNullProperties(updateUserRequestDTO, existingUserEntity);
// 3. 更新实体的 'updateTime' 字段
existingUserEntity.setUpdateTime(new Date());
// 4. 保存更新后的实体到数据库
User updatedUserEntity = userRepository.save(existingUserEntity);
// 5. 将更新后的实体转换回 DTO 返回
UserDTO resultDTO = new UserDTO();
BeanUtils.copyProperties(updatedUserEntity, resultDTO);
return resultDTO;
}
/**
* 辅助方法:仅复制源对象中非 null 的属性到目标对象
* 这是对 Spring BeanUtils 的一个常见扩展。
* @param source 源对象 (e.g., DTO)
* @param target 目标对象 (e.g., Entity)
*/
public static void copyNonNullProperties(Object source, Object target) {
// 获取源对象中所有值为 null 的属性名
String[] nullPropertyNames = getNullPropertyNames(source);
// 调用 Spring BeanUtils 的 copyProperties,并忽略这些 null 值的属性
BeanUtils.copyProperties(source, target, nullPropertyNames);
}
/**
* 辅助方法:获取一个对象中所有值为 null 的属性名数组。
* @param source 要检查的对象
* @return 包含所有 null 属性名称的字符串数组
*/
private static String[] getNullPropertyNames(Object source) {
final BeanWrapper src = new BeanWrapperImpl(source); // 使用 Spring 的 BeanWrapper 来访问属性
PropertyDescriptor[] pds = src.getPropertyDescriptors(); // 获取所有属性描述符
Set<String> emptyNames = new HashSet<>(); // 用于存储 null 属性的名称
for (PropertyDescriptor pd : pds) {
Object srcValue = src.getPropertyValue(pd.getName()); // 获取属性值
if (srcValue == null) {
emptyNames.add(pd.getName()); // 如果值为 null,则添加到集合中
}
}
return emptyNames.toArray(new String[0]); // 将集合转换为数组返回
}
/**
* (可选) 示例:对 DTO 进行额外处理
* @param userDTO 目标 DTO
* @param userEntity 源实体 (可能需要用于计算或关联查询)
*/
private void enrichUserDTO(UserDTO userDTO, User userEntity) {
// 比如:计算用户年龄(如果 DTO 没有 age 字段但需要展示)
// 比如:格式化日期显示
// 比如:根据关联 ID 查询并设置关联对象的名称等
// userDTO.setFormattedCreateTime(formatDate(userEntity.getCreateTime()));
}
// 模拟 UserRepository
interface UserRepository {
Optional<User> findById(Integer id);
User save(User user);
List<User> findAll();
}
}
// 假设 EntityNotFoundException 已定义
class EntityNotFoundException extends RuntimeException {
public EntityNotFoundException(String message) { super(message); }
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
这个例子展示了如何在 Service 层结合数据库操作使用 BeanUtils
进行对象转换,以及如何通过自定义方法 copyNonNullProperties
来实现常见的“部分更新”需求。
# 三、Apache Commons BeanUtils 详解
Apache Commons BeanUtils 是另一个广泛使用的 Bean 操作库,它提供了比 Spring BeanUtils 更丰富的功能,但也通常伴随着更复杂的行为和较低的性能。
依赖配置:
需要单独添加 commons-beanutils
依赖:
<!-- Maven 配置 -->
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.4</version> <!-- 建议使用官方最新稳定版 -->
</dependency>
<!-- Apache Commons BeanUtils 内部依赖 logging, 可能需要添加 -->
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
2
3
4
5
6
7
8
9
10
11
12
// Gradle 配置
implementation 'commons-beanutils:commons-beanutils:1.9.4'
implementation 'commons-logging:commons-logging:1.2' // 可能需要
2
3
# 3.1 基本用法:copyProperties
与 PropertyUtils
Apache Commons 提供了两个主要的属性复制类:
org.apache.commons.beanutils.BeanUtils
:- 核心方法
copyProperties(Object dest, Object orig)
。 - 会自动进行类型转换。例如,如果源属性是
String
"30",目标属性是Integer
,它会尝试将其转换为Integer
30。这是其与 Spring BeanUtils 的一个显著区别。这种自动转换有时很方便,但也可能隐藏类型不匹配的问题或导致意外行为。 - 性能相对较低,因为它涉及类型转换逻辑和更复杂的反射处理。
- 核心方法
org.apache.commons.beanutils.PropertyUtils
:- 核心方法
copyProperties(Object dest, Object orig)
。 - 不进行自动类型转换。它要求源和目标属性的类型严格兼容(或者可以通过 Java 的自动装箱/拆箱兼容)。如果类型不匹配,会抛出异常。
- 性能通常优于
BeanUtils
,因为它跳过了类型转换的开销。
- 核心方法
// 引入 Apache Commons BeanUtils 和 PropertyUtils
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.PropertyUtils;
import lombok.Data;
import java.util.Date;
import java.lang.reflect.InvocationTargetException; // 需要处理的异常
/**
* Apache Commons BeanUtils 和 PropertyUtils 基本用法示例
*/
public class ApacheBeanUtilsDemo {
public static void main(String[] args) {
// 1. 创建源对象
SourceBean source = new SourceBean();
source.setId(201);
source.setName("源对象");
source.setValue("123.45"); // String 类型的值
source.setActive(true);
source.setDate(new Date());
// 2. 创建目标对象
TargetBean target1 = new TargetBean();
TargetBean target2 = new TargetBean();
System.out.println("源对象: " + source);
System.out.println("复制前目标对象1: " + target1);
System.out.println("复制前目标对象2: " + target2);
try {
// 3. 使用 Apache BeanUtils.copyProperties 进行复制
// 注意参数顺序:dest 在前,orig 在后
// 它会尝试将 source.value (String) 转换为 target1.value (Double)
BeanUtils.copyProperties(target1, source);
System.out.println("\n使用 Apache BeanUtils 复制后: " + target1);
// 预期输出: TargetBean{id=201, name='源对象', value=123.45, active=true, date=当前时间}
} catch (IllegalAccessException | InvocationTargetException e) {
System.err.println("Apache BeanUtils 复制时出错: " + e.getMessage());
// 异常可能发生在类型转换失败时
}
try {
// 4. 使用 Apache PropertyUtils.copyProperties 进行复制
// 它要求类型兼容,source.value (String) 与 target2.value (Double) 不兼容
// 因此,value 属性不会被复制,其他兼容的属性会被复制
PropertyUtils.copyProperties(target2, source);
System.out.println("\n使用 Apache PropertyUtils 复制后: " + target2);
// 预期输出: TargetBean{id=201, name='源对象', value=null, active=true, date=当前时间}
// value 保持 null,因为类型不匹配且 PropertyUtils 不转换
// 如果尝试让 PropertyUtils 复制不兼容类型,通常会报错或被忽略
// 例如,如果 TargetBean 的 id 是 String 类型,复制会失败
} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
// PropertyUtils 可能抛出 NoSuchMethodException 如果 getter/setter 不存在
System.err.println("Apache PropertyUtils 复制时出错: " + e.getMessage());
}
}
@Data
static class SourceBean {
private Integer id;
private String name;
private String value; // 注意这里是 String 类型
private Boolean active;
private Date date;
}
@Data
static class TargetBean {
private Integer id;
private String name;
private Double value; // 注意这里是 Double 类型
private Boolean active;
private Date date;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
选择 BeanUtils
vs PropertyUtils
:
- 如果需要自动类型转换(并接受其潜在风险),使用
BeanUtils
。 - 如果希望类型严格匹配,或者更关心性能,使用
PropertyUtils
。
# 3.2 灵活的属性访问与设置
与 PropertyUtils
类似,BeanUtils
也提供了单独设置和获取属性值的方法,并支持类型转换。
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.PropertyUtils;
import lombok.Data;
/**
* Apache BeanUtils/PropertyUtils 的属性访问与设置示例
*/
public class ApachePropertyAccessDemo {
public static void main(String[] args) throws Exception { // 简化异常处理
User user = new User();
// --- 使用 BeanUtils (带类型转换) ---
System.out.println("--- 使用 BeanUtils ---");
// 设置属性值,BeanUtils 会尝试将 "张三" 和 "30" (String) 转换为目标类型
BeanUtils.setProperty(user, "name", "张三");
BeanUtils.setProperty(user, "age", "30"); // 传入字符串 "30"
BeanUtils.setProperty(user, "email", "zhangsan@apache.org");
// 获取属性值,BeanUtils.getProperty 返回 String 类型
String nameStr = BeanUtils.getProperty(user, "name");
String ageStr = BeanUtils.getProperty(user, "age");
String emailStr = BeanUtils.getProperty(user, "email");
System.out.println("姓名 (String): " + nameStr); // 输出:张三
System.out.println("年龄 (String): " + ageStr); // 输出:30
System.out.println("邮箱 (String): " + emailStr); // 输出:zhangsan@apache.org
System.out.println("User 对象状态: " + user); // age 字段已被成功设置为 Integer 30
// --- 使用 PropertyUtils (不带类型转换) ---
System.out.println("\n--- 使用 PropertyUtils ---");
User user2 = new User();
// 设置属性值,必须传入正确的类型
PropertyUtils.setProperty(user2, "name", "李四");
PropertyUtils.setProperty(user2, "age", 35); // 必须传入 Integer (或 int)
// PropertyUtils.setProperty(user2, "age", "35"); // 这行会抛出类型不匹配的异常
// 获取属性值,返回原始类型 (需要强制类型转换)
String nameOrig = (String) PropertyUtils.getProperty(user2, "name");
Integer ageOrig = (Integer) PropertyUtils.getProperty(user2, "age");
System.out.println("姓名 (原始类型 String): " + nameOrig); // 输出:李四
System.out.println("年龄 (原始类型 Integer): " + ageOrig); // 输出:35
}
// 假设 User 类定义如前
@Data
static class User {
private Integer id;
private String name;
private Integer age;
private String email;
private Date createTime;
private Date updateTime;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# 3.3 嵌套属性与映射属性访问
Apache Commons BeanUtils 的一个强大之处在于它支持使用点 (.
) 操作符访问嵌套对象的属性,以及使用括号 (()
) 操作符访问 Map
中的值。
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.PropertyUtils;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
/**
* Apache BeanUtils 访问嵌套属性和 Map 属性示例
*/
public class ApacheNestedPropertyDemo {
public static void main(String[] args) throws Exception { // 简化异常处理
// 1. 创建嵌套对象结构
Department dept = new Department();
dept.setName("研发中心");
// dept.location 初始为 null
Employee employee = new Employee();
employee.setEmployeeName("张工程师");
employee.setDepartment(dept); // 设置嵌套对象
Map<String, String> contactInfo = new HashMap<>();
contactInfo.put("email", "zhang.gcs@example.com");
contactInfo.put("mobile", "13912345678");
employee.setContactInfo(contactInfo); // 设置 Map 属性
// --- 访问和设置嵌套属性 ---
System.out.println("--- 嵌套属性操作 ---");
// 使用 BeanUtils 设置嵌套属性 department.location
// 会自动找到 employee.getDepartment().setLocation("北京总部A座")
BeanUtils.setProperty(employee, "department.location", "北京总部A座");
// 使用 BeanUtils 获取嵌套属性 department.name (返回 String)
String deptName = BeanUtils.getProperty(employee, "department.name");
String deptLocation = BeanUtils.getProperty(employee, "department.location");
System.out.println("部门名称: " + deptName); // 输出:研发中心
System.out.println("部门位置: " + deptLocation); // 输出:北京总部A座
// 使用 PropertyUtils 访问 (返回原始类型)
String deptNameOrig = (String) PropertyUtils.getProperty(employee, "department.name");
System.out.println("部门名称 (原始类型): " + deptNameOrig);
// --- 访问和设置 Map 属性 ---
System.out.println("\n--- Map 属性操作 ---");
// 使用 BeanUtils 访问 Map 中的值 contactInfo(key)
// 会调用 employee.getContactInfo().get("email")
String email = BeanUtils.getProperty(employee, "contactInfo(email)");
String mobile = BeanUtils.getProperty(employee, "contactInfo(mobile)");
System.out.println("电子邮箱: " + email); // 输出:zhang.gcs@example.com
System.out.println("手机号: " + mobile); // 输出:13912345678
// 使用 BeanUtils 设置 Map 中的值 contactInfo(key)
BeanUtils.setProperty(employee, "contactInfo(officePhone)", "010-88886666");
// 验证设置是否成功
String officePhone = BeanUtils.getProperty(employee, "contactInfo(officePhone)");
System.out.println("办公电话: " + officePhone); // 输出:010-88886666
System.out.println("更新后的联系方式 Map: " + employee.getContactInfo());
}
/**
* 部门类
*/
@Data
static class Department {
private String name; // 部门名称
private String location; // 部门位置
}
/**
* 员工类 (包含嵌套 Department 和 Map ContactInfo)
*/
@Data
static class Employee {
private String employeeName; // 员工姓名
private Department department; // 所属部门 (嵌套对象)
private Map<String, String> contactInfo; // 联系方式 (Map 属性)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
这种“属性表达式”的写法非常灵活,但也增加了代码理解的复杂度,且过度使用可能影响性能和类型安全。
# 四、性能考量与最佳实践
# 4.1 BeanUtils 性能对比分析
不同的 BeanUtils
实现在性能上存在显著差异,了解这些差异对于选择合适的工具至关重要。
import org.springframework.beans.BeanUtils; // Spring
import org.apache.commons.beanutils.BeanUtilsBean; // Apache (使用 BeanUtilsBean 实例通常更好)
import org.apache.commons.beanutils.PropertyUtilsBean; // Apache PropertyUtils
import lombok.Data;
import java.util.Date;
/**
* 不同 BeanUtils 实现的性能对比测试
*/
public class BeanUtilsPerformanceTest {
// 准备测试数据
private static final int ITERATIONS = 1_000_000; // 执行一百万次复制操作
private static User sourceUser = createTestSourceUser();
private static UserDTO targetUserDTO = new UserDTO();
public static void main(String[] args) throws Exception { // 简化异常处理
System.out.println("开始性能测试 (执行 " + ITERATIONS + " 次复制)...");
// --- 测试 Spring BeanUtils ---
long startSpring = System.nanoTime();
for (int i = 0; i < ITERATIONS; i++) {
BeanUtils.copyProperties(sourceUser, targetUserDTO);
}
long endSpring = System.nanoTime();
long springTimeMs = (endSpring - startSpring) / 1_000_000;
System.out.printf("Spring BeanUtils: %d ms%n", springTimeMs);
// --- 测试 Apache BeanUtils (使用 BeanUtilsBean 实例) ---
BeanUtilsBean apacheBeanUtils = BeanUtilsBean.getInstance();
// Apache BeanUtils 需要目标在前,源在后
long startApache = System.nanoTime();
for (int i = 0; i < ITERATIONS; i++) {
apacheBeanUtils.copyProperties(targetUserDTO, sourceUser);
}
long endApache = System.nanoTime();
long apacheTimeMs = (endApache - startApache) / 1_000_000;
System.out.printf("Apache BeanUtils: %d ms%n", apacheTimeMs);
// --- 测试 Apache PropertyUtils (使用 PropertyUtilsBean 实例) ---
PropertyUtilsBean apachePropertyUtils = PropertyUtilsBean.getInstance();
long startApacheProp = System.nanoTime();
for (int i = 0; i < ITERATIONS; i++) {
apachePropertyUtils.copyProperties(targetUserDTO, sourceUser);
}
long endApacheProp = System.nanoTime();
long apachePropTimeMs = (endApacheProp - startApacheProp) / 1_000_000;
System.out.printf("Apache PropertyUtils: %d ms%n", apachePropTimeMs);
// --- 测试手动 Getter/Setter 复制 ---
long startManual = System.nanoTime();
for (int i = 0; i < ITERATIONS; i++) {
manualCopy(sourceUser, targetUserDTO);
}
long endManual = System.nanoTime();
long manualTimeMs = (endManual - startManual) / 1_000_000;
System.out.printf("手动 Getter/Setter: %d ms%n", manualTimeMs);
/*
* 典型性能对比结果 (结果可能因机器和 JVM 版本而异):
* Spring BeanUtils: 相对较快 (几十到几百毫秒)
* Apache BeanUtils: 相对很慢 (几百到几千毫秒,因为涉及类型转换)
* Apache PropertyUtils: 比 Apache BeanUtils 快,接近 Spring BeanUtils (几十到几百毫秒)
* 手动 Getter/Setter: 最快 (几毫秒到几十毫秒)
*/
}
/**
* 手动复制方法,作为性能基准
*/
private static void manualCopy(User source, UserDTO target) {
target.setId(source.getId());
target.setName(source.getName());
target.setAge(source.getAge());
target.setEmail(source.getEmail());
target.setCreateTime(source.getCreateTime());
}
/**
* 创建用于测试的源 User 对象
*/
private static User createTestSourceUser() {
User user = new User();
user.setId(1);
user.setName("性能测试用户");
user.setAge(30);
user.setEmail("performance@example.com");
user.setCreateTime(new Date());
user.setUpdateTime(new Date());
return user;
}
// 假设 User 和 UserDTO 类定义如前
@Data static class User { private Integer id; private String name; private Integer age; private String email; private Date createTime; private Date updateTime; }
@Data static class UserDTO { private Integer id; private String name; private Integer age; private String email; private Date createTime; }
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
性能分析:
- 手动复制:最快,因为是直接的方法调用,没有反射开销。
- Spring BeanUtils:性能较好。它内部对类的反射信息(如属性描述符)进行了缓存,减少了重复反射的开销。
- Apache PropertyUtils:性能接近 Spring BeanUtils,因为它不做类型转换,反射逻辑相对简单。
- Apache BeanUtils:性能最差。主要是因为它复杂的类型转换逻辑(查找和执行
Converter
)以及相对较旧的反射处理方式带来了显著的额外开销。
# 4.2 性能优化与使用建议
基于性能和功能考量,给出以下建议:
- 首选 Spring BeanUtils: 在 Spring 环境中,优先使用
org.springframework.beans.BeanUtils
,它提供了良好的性能和简洁性。 - 谨慎使用 Apache Commons BeanUtils: 只有当你确实需要其强大的自动类型转换功能,并且能接受其性能开销时才考虑使用。了解其类型转换规则,避免意外行为。如果不需要类型转换,应使用
PropertyUtils
。 - 减少复制次数和范围:
- 避免在循环内部频繁创建
BeanUtils
实例(如果使用的是 Apache Commons 的非静态方法)。 - 如果只需要复制少量属性,考虑手动
setter
调用可能更高效。 - 使用
ignoreProperties
(Spring) 或仅复制所需属性(如果手动实现)来避免不必要的开销。
- 避免在循环内部频繁创建
- 性能敏感路径考虑替代方案: 对于应用程序的核心路径或需要处理大量对象复制的场景(如批处理),反射带来的开销可能无法接受。此时应考虑:
- 手动编写
getter/setter
代码。 - 使用编译时生成代码的映射库,如 MapStruct,它几乎能达到手动编码的性能。
- 手动编写
- 缓存反射信息 (高级): 如果你自己实现 BeanUtils 功能或扩展现有库,考虑缓存
PropertyDescriptor
等反射信息,避免重复查找。
# 4.3 封装自定义 BeanUtils 工具类
为了满足项目中特定的、重复的复制需求(如仅复制非 null 属性、列表复制、深度复制等),可以封装一个自定义的工具类。
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import java.beans.PropertyDescriptor;
import java.util.*; // 引入 Collections, List, ArrayList, Set, HashSet, Collection
import java.lang.reflect.Method; // 引入 Method
/**
* 自定义的 BeanUtils 工具类,扩展 Spring BeanUtils 功能
*/
public final class CustomBeanUtils {
// 私有构造函数,防止实例化
private CustomBeanUtils() {}
/**
* 复制源对象中非 null 的属性到目标对象。
* 对于目标对象中存在的同名属性,只有当源对象的对应属性值不为 null 时才进行复制。
*
* @param source 源对象
* @param target 目标对象
*/
public static void copyNonNullProperties(Object source, Object target) {
// 核心逻辑:调用 Spring BeanUtils 的 copyProperties,
// 并传入一个忽略属性列表,这个列表包含了源对象中所有值为 null 的属性名。
BeanUtils.copyProperties(source, target, getNullPropertyNames(source));
}
/**
* 获取一个对象中所有值为 null 的属性名数组。
* (代码同前文 Service 中的实现)
* @param source 要检查的对象
* @return 包含所有 null 属性名称的字符串数组
*/
private static String[] getNullPropertyNames(Object source) {
final BeanWrapper src = new BeanWrapperImpl(source);
PropertyDescriptor[] pds = src.getPropertyDescriptors();
Set<String> emptyNames = new HashSet<>();
for (PropertyDescriptor pd : pds) {
if (src.getPropertyValue(pd.getName()) == null) {
emptyNames.add(pd.getName());
}
}
return emptyNames.toArray(new String[0]);
}
/**
* 将一个列表中的所有对象复制到另一个指定类型的列表中。
* 对列表中的每个元素执行 BeanUtils.copyProperties。
*
* @param sourceList 源对象列表
* @param targetClass 目标列表中元素的 Class 对象
* @param <T> 目标元素的类型
* @param <S> 源元素的类型
* @return 一个新的包含复制后对象的列表;如果源列表为 null 或空,返回空列表。
*/
public static <T, S> List<T> copyList(List<S> sourceList, Class<T> targetClass) {
if (sourceList == null || sourceList.isEmpty()) {
return Collections.emptyList(); // 返回不可变的空列表更安全
}
List<T> targetList = new ArrayList<>(sourceList.size());
try {
for (S source : sourceList) {
T target = targetClass.getDeclaredConstructor().newInstance(); // 创建目标对象实例
BeanUtils.copyProperties(source, target); // 复制属性
targetList.add(target);
}
} catch (Exception e) {
// 实际项目中应使用更具体的异常处理和日志记录
throw new RuntimeException("复制列表时发生错误", e);
}
return targetList;
}
/**
* (实验性) 尝试进行深度复制对象的属性。
* 注意:这是一个简化的实现,可能无法处理所有情况(如循环引用、复杂集合类型等)。
* 在生产环境中,建议使用成熟的序列化库 (如 Jackson/Gson 配合反序列化) 或专门的深度复制库。
*
* @param source 源对象
* @param target 目标对象
*/
public static void copyPropertiesDeep(Object source, Object target) {
// 1. 先进行浅复制
BeanUtils.copyProperties(source, target);
// 2. 遍历目标对象的属性,查找需要深度复制的属性 (非基本类型、非 String、非 Date 等)
PropertyDescriptor[] targetPds = BeanUtils.getPropertyDescriptors(target.getClass());
for (PropertyDescriptor targetPd : targetPds) {
Method writeMethod = targetPd.getWriteMethod();
if (writeMethod == null) continue; // 目标属性不可写
PropertyDescriptor sourcePd = BeanUtils.getPropertyDescriptor(source.getClass(), targetPd.getName());
if (sourcePd == null) continue; // 源对象没有同名属性
Method readMethod = sourcePd.getReadMethod();
if (readMethod == null) continue; // 源属性不可读
try {
Object sourceValue = readMethod.invoke(source); // 获取源属性值
if (sourceValue != null) {
Class<?> sourceType = sourceValue.getClass();
// 判断是否需要深度复制
boolean needsDeepCopy = !(sourceType.isPrimitive() ||
sourceType.getName().startsWith("java.lang.") || // String, Integer, etc.
sourceType.getName().startsWith("java.util.Date") ||
sourceType.getName().startsWith("java.time.") || // Java 8 时间 API
sourceType.isEnum());
if (needsDeepCopy) {
// 尝试创建目标属性类型的新实例
Object targetValue = targetPd.getPropertyType().getDeclaredConstructor().newInstance();
// 递归调用深度复制
copyPropertiesDeep(sourceValue, targetValue);
// 将深度复制后的新对象设置到目标对象
writeMethod.invoke(target, targetValue);
}
// 注意:此简化实现未处理集合、Map 的深度复制,需要更复杂的逻辑(见下文注释)
/*
// 更完善的深度复制需要处理集合和 Map:
else if (sourceValue instanceof Collection) {
Collection newCollection = createNewCollectionDeep((Collection)sourceValue);
writeMethod.invoke(target, newCollection);
} else if (sourceValue instanceof Map) {
Map newMap = createNewMapDeep((Map)sourceValue);
writeMethod.invoke(target, newMap);
}
*/
}
} catch (Exception e) {
// 异常处理:记录日志或根据策略决定是否抛出
System.err.println("深度复制属性 '" + targetPd.getName() + "' 时出错: " + e.getMessage());
}
}
}
// 深度复制集合和 Map 的辅助方法需要递归处理元素/值,此处省略以保持示例简洁。
// 实际应用中,使用序列化/反序列化通常是更可靠的深度复制方式。
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# 4.4 在项目中统一使用自定义工具类
在项目中引入自定义的 CustomBeanUtils
,可以在需要的地方方便地调用其提供的扩展功能。
// 假设环境:Spring Boot 项目
import org.springframework.beans.BeanUtils; // 仍然可能用到 Spring 的基础功能
// import CustomBeanUtils; // 引入自定义工具类
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.persistence.EntityNotFoundException;
import java.util.Date;
import java.util.List;
// 假设存在 ProductRepository, Product, ProductDTO, ProductUpdateDTO 类
/**
* 产品服务类,演示统一使用自定义 BeanUtils 工具类
*/
@Service
public class ProductService {
private final ProductRepository productRepository;
@Autowired
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
/**
* 获取所有产品信息,并转换为 DTO 列表
* @return 产品 DTO 列表
*/
public List<ProductDTO> getAllProducts() {
List<Product> productEntities = productRepository.findAll();
// 使用自定义工具类的 copyList 方法批量转换
return CustomBeanUtils.copyList(productEntities, ProductDTO.class);
}
/**
* 更新产品信息(部分更新)
* @param productId 要更新的产品 ID
* @param updateRequest DTO,包含需要更新的字段(可能为 null)
* @return 更新后的产品 DTO
*/
public ProductDTO updateProduct(Long productId, ProductUpdateDTO updateRequest) {
// 1. 查找现有产品实体
Product existingProduct = productRepository.findById(productId)
.orElseThrow(() -> new EntityNotFoundException("产品不存在,ID: " + productId));
// 2. 使用自定义工具类的 copyNonNullProperties 方法
// 只将 updateRequest 中非 null 的属性值复制到 existingProduct
CustomBeanUtils.copyNonNullProperties(updateRequest, existingProduct);
// 3. 设置更新时间戳
existingProduct.setUpdateTime(new Date());
// 4. 保存更新后的实体
Product updatedProduct = productRepository.save(existingProduct);
// 5. 将更新后的实体转换为 DTO 返回
ProductDTO resultDTO = new ProductDTO();
BeanUtils.copyProperties(updatedProduct, resultDTO); // 这里用 Spring 的基础复制即可
return resultDTO;
}
// 模拟 ProductRepository
interface ProductRepository {
Optional<Product> findById(Long id);
Product save(Product product);
List<Product> findAll();
}
// 模拟 Product, ProductDTO, ProductUpdateDTO
@Data static class Product { private Long id; private String name; private Double price; private String description; private Date createTime; private Date updateTime; }
@Data static class ProductDTO { private Long id; private String name; private Double price; private String description; }
@Data static class ProductUpdateDTO { private String name; private Double price; private String description; } // 用于更新,字段可能为 null
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
通过封装统一的工具类,可以提高代码的可维护性和一致性。
# 五、BeanUtils 的局限性与替代方案
尽管 BeanUtils
非常方便,但了解其局限性并适时选择替代方案同样重要。
# 5.1 BeanUtils 的主要局限性回顾
- 性能瓶颈: 基于反射的实现,相比直接
getter/setter
调用有显著的性能开销,尤其是在大量或频繁复制时。 - 名称强依赖: 核心功能依赖于源和目标对象具有相同名称的属性。无法自动处理名称不匹配的情况(如
userId
vsuser_id
)。 - 有限的类型转换:
- Spring BeanUtils 要求类型兼容,不支持复杂的自动转换。
- Apache Commons BeanUtils 支持自动转换,但其转换逻辑可能不符合预期,且增加了性能开销和潜在的运行时错误风险。
- 缺乏自定义转换逻辑: 无法在复制过程中嵌入自定义的转换、计算或格式化逻辑(例如,将
Date
转换为特定格式的String
)。 - 浅复制: 默认只复制属性引用(地址),而不是递归地创建嵌套对象的新实例。如果源对象的嵌套属性被修改,目标对象的对应属性也会受影响。实现深度复制通常需要额外的工作或使用其他机制。
- 类型安全问题: 基于字符串的属性名称(如
ignoreProperties
)无法在编译时进行检查,容易因拼写错误导致 Bug。
# 5.2 强大的替代方案:MapStruct
对于需要克服上述局限性的复杂映射场景,MapStruct 是一个非常优秀的选择。
MapStruct 简介:
- 编译时代码生成: MapStruct 是一个基于注解处理器的代码生成器。它在 Java 编译阶段 分析映射接口(用
@Mapper
注解定义),并自动生成高效、类型安全的映射实现类(纯 Java 代码,无反射)。 - 高性能: 生成的代码是直接的
getter/setter
调用,性能几乎等同于手动编写。 - 类型安全: 所有映射配置都在接口和注解中定义,编译时进行检查,能及早发现错误。
- 强大映射功能:
- 通过
@Mapping
注解轻松处理不同名称的字段 (source
->target
)。 - 支持复杂的类型转换(包括自定义转换方法)。
- 支持常量、表达式映射。
- 支持集合、嵌套对象的映射。
- 与 CDI、Spring 等依赖注入框架良好集成。
- 通过
示例:使用 MapStruct 进行对象映射
首先,添加 MapStruct 依赖(通常包括 mapstruct
和 mapstruct-processor
):
<!-- Maven 配置 -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.3.Final</version> <!-- 使用最新稳定版 -->
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.3.Final</version>
<scope>provided</scope> <!-- 注解处理器仅编译时需要 -->
</dependency>
<!-- 如果使用 Lombok,可能需要配置 mapstruct-processor 与 lombok 协同工作 -->
2
3
4
5
6
7
8
9
10
11
12
13
然后,定义一个映射接口:
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings; // 用于多个 Mapping
import org.mapstruct.factory.Mappers; // 用于获取 Mapper 实例
import java.util.Date;
import java.util.List;
// 假设 User 和 UserDTO 类定义如前
/**
* 使用 MapStruct 定义 User 和 UserDTO 之间的映射接口
*/
@Mapper(componentModel = "spring") // componentModel="spring" 让 MapStruct 生成 Spring Bean
public interface UserMapper {
// 获取 Mapper 实例 (如果是非 Spring 环境)
// UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
/**
* 将 User 实体对象映射到 UserDTO 数据传输对象。
*
* @param user 源 User 对象
* @return 映射后的 UserDTO 对象
*/
@Mappings({
// 示例:如果 DTO 中需要格式化日期字符串
@Mapping(source = "createTime", target = "createTimeFormatted", dateFormat = "yyyy-MM-dd HH:mm:ss"),
// 示例:如果 DTO 中需要不同的字段名
// @Mapping(source = "name", target = "userName"),
// 示例:忽略某个字段
@Mapping(target = "sensitiveDataField", ignore = true) // 假设 DTO 有个敏感字段需要忽略
})
// MapStruct 会自动映射同名同类型的字段 (id, name, age, email, createTime)
UserDTO userToUserDTO(User user);
/**
* 将 UserDTO 数据传输对象映射回 User 实体对象。
*
* @param userDTO 源 UserDTO 对象
* @return 映射后的 User 对象
*/
@Mappings({
// 示例:将格式化的日期字符串或不同的字段名映射回来 (如果需要)
// @Mapping(source = "userName", target = "name"),
// 示例:在映射时设置默认值或计算值
@Mapping(target = "updateTime", expression = "java(new java.util.Date())"), // 设置更新时间为当前时间
@Mapping(target = "password", ignore = true) // 假设 DTO 可能包含密码,但转回 Entity 时忽略
})
// MapStruct 会自动映射同名同类型的字段
User userDTOToUser(UserDTO userDTO);
/**
* 映射 User 列表到 UserDTO 列表。
* MapStruct 会自动处理集合的映射。
*
* @param userList 源 User 列表
* @return 映射后的 UserDTO 列表
*/
List<UserDTO> usersToUserDTOs(List<User> userList);
}
// 假设 UserDTO 增加了 formattedCreateTime 和 sensitiveDataField 字段
@Data
class UserDTO {
private Integer id;
private String name;
private Integer age;
private String email;
private Date createTime;
private String createTimeFormatted; // 新增:格式化时间字符串
private String sensitiveDataField; // 新增:敏感字段示例
}
// User 类保持不变或按需调整
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
使用 Mapper:
在 Service 中注入 UserMapper
并调用其方法即可完成映射。
@Service
public class UserServiceWithMapStruct {
private final UserRepository userRepository;
private final UserMapper userMapper; // 注入 MapStruct Mapper
@Autowired
public UserServiceWithMapStruct(UserRepository userRepository, UserMapper userMapper) {
this.userRepository = userRepository;
this.userMapper = userMapper;
}
public UserDTO getUserDetails(Integer userId) {
User userEntity = userRepository.findById(userId)
.orElseThrow(() -> new EntityNotFoundException("用户不存在,ID: " + userId));
// 使用 MapStruct 进行转换
return userMapper.userToUserDTO(userEntity);
}
public UserDTO createUser(UserDTO createUserRequestDTO) {
// 使用 MapStruct 进行转换
User newUserEntity = userMapper.userDTOToUser(createUserRequestDTO);
// newUserEntity.setCreateTime(new Date()); // createTime 可能由 DTO 传入或在 Mapper 中设置
User savedUserEntity = userRepository.save(newUserEntity);
// 转换回 DTO
return userMapper.userToUserDTO(savedUserEntity);
}
// ... 其他方法 ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 5.3 如何选择合适的工具?
选择哪种工具取决于项目的具体需求和复杂度:
场景描述 | 推荐工具 | 理由 |
---|---|---|
简单场景:对象结构类似,属性名和类型大多相同 | Spring BeanUtils | 简单易用,性能可接受,与 Spring 无缝集成。 |
需要自动类型转换(如 String 转数字/日期) | Apache Commons BeanUtils | 内置丰富的类型转换器(但需注意性能和潜在问题)。 |
复杂映射:属性名不同、需要自定义转换逻辑、性能要求高 | MapStruct | 编译时生成代码,高性能,类型安全,功能强大。 |
性能极其敏感的核心路径 | 手动编写 | 极致性能,但开发和维护成本最高。 |
需要运行时动态配置映射规则 | ModelMapper | 灵活性高,但基于反射,性能不如 MapStruct,配置可能复杂。 |
需要深度复制整个对象图(包括嵌套对象) | 序列化/反序列化(如 Jackson/Gson) 或专门的深度复制库(如 Apache Commons Lang SerializationUtils ) | 能可靠地创建完全独立的对象副本(但性能开销较大)。 |
总结: 对于大多数 Spring 应用中的 DTO/VO/Entity 转换,Spring BeanUtils 是一个足够好的起点。当映射逻辑变得复杂(名称/类型不匹配、需要自定义逻辑)或性能成为瓶颈时,强烈推荐迁移到 MapStruct。避免过度使用 Apache Commons BeanUtils,除非确实需要其独特的类型转换功能。