Hutool BeanUtil.copyProperties 使用注意事项(踩坑版本)
一、概述
BeanUtil.copyProperties(source, target) 是 Hutool 工具库中用于 对象属性拷贝 的核心方法之一,功能类似于 Spring 的 BeanUtils.copyProperties,但更灵活、性能更好,并支持更多转换场景。
注意:Hutool 的 copyProperties 默认是 浅拷贝,且 只拷贝同名字段。
二、基本用法
- 引入依赖(Maven)
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.22</version> <!-- 推荐使用最新稳定版 -->
</dependency>
- 简单示例
public class UserDTO {
private String name;
private Integer age;
// getter/setter
}
public class UserVO {
private String name;
private String age; // 注意:类型不同!
// getter/setter
}
// 使用
UserDTO dto = new UserDTO();
dto.setName("张三");
dto.setAge(25);
UserVO vo = new UserVO();
BeanUtil.copyProperties(dto, vo);
三、核心特性
特性 说明
- 同名字段自动映射 源对象和目标对象字段名相同即尝试拷贝
- 类型自动转换 支持常见类型互转(如 Integer ↔ String, Date ↔ String 等)
- 忽略 null 值(可选) 可通过 BeanCopyOptions 控制是否跳过 null
- 忽略大小写(可选) 支持字段名忽略大小写匹配
- 自定义转换器 可注册 Converter 处理特殊类型
- 支持泛型 对 List、Map 等容器内元素也可转换(需配合配置)
四、高级用法:BeanCopyOptions
通过 BeanCopyOptions 精细控制拷贝行为:
BeanCopyOptions options = BeanCopyOptions.create()
.setIgnoreNullValue(true) // 忽略 source 中为 null 的字段
.setIgnoreCase(true) // 字段名忽略大小写(如 userName ↔ username)
.setTransientSupport(true) // 是否拷贝 transient 字段(默认 false)
.setFieldMapping(Map.of("oldName", "newName")); // 自定义字段映射(非同名)
BeanUtil.copyProperties(source, target, options);
注意:BeanCopyOptions 是线程安全的,可复用。
五、重点问题:字段名相同但类型不同(笔者实际遇到的问题)
这是实际开发中最常见的痛点。Hutool 的处理逻辑如下:
- 默认行为:尝试自动转换
Hutool 内置了丰富的 Converter,能自动处理以下常见转换:
源类型 → 目标类型 是否支持
Integer → String
String → Integer (如 “123” → 123)
Date → String (格式:yyyy-MM-dd HH:mm:ss)
String → Date (支持多种格式自动识别)
Boolean ↔ String (“true”/“false”)
Long ↔ String
BigDecimal ↔ String
成功示例:
source.setAge(25); // Integer
// copy to target.age (String)
// 结果:target.getAge() == "25"
- 转换失败时的行为
如果无法转换(如 “abc” → Integer),抛出 ConvertException;
转换过程不会静默失败,确保数据一致性。
六、解决“同名不同型”问题的策略
策略 1:利用 Hutool 内置转换能力(推荐)
只要数据格式合法,Hutool 能自动完成大部分基础类型互转。
适用场景:String ↔ 数值/日期/布尔 等标准类型。
策略 2:注册自定义 Converter
当内置转换不满足需求时,可自定义转换器:
// 示例:将 String "男"/"女" 转为 Boolean(true=男)
ConverterRegistry.getInstance().putCustom(String.class, Boolean.class, (value, targetType) -> {
if ("男".equals(value)) return true;
if ("女".equals(value)) return false;
throw new IllegalArgumentException("性别值非法: " + value);
});
// 使用
BeanUtil.copyProperties(userDTO, userVO); // 自动应用自定义转换
注意:ConverterRegistry 是全局单例,注册一次全局生效。
策略 3:使用字段映射(Field Mapping)绕过冲突
如果某字段类型差异太大(如 List → String),可重命名目标字段,并通过 BeanCopyOptions 映射:
public class Target {
private String tagListStr; // 原本想叫 tags,但类型不同
}
BeanCopyOptions options = BeanCopyOptions.create()
.setFieldMapping(Map.of("tags", "tagListStr"));
BeanUtil.copyProperties(source, target, options);
然后在 setter 中手动处理逻辑。
策略 4:分步拷贝 + 手动设置 (笔者个人及其不推荐)
对特殊字段单独处理:
// 先拷贝通用字段
BeanUtil.copyProperties(source, target);
// 再手动处理冲突字段
target.setCreateTimeStr(DateUtil.format(source.getCreateTime(), "yyyy-MM-dd"));
target.setStatusDesc(getStatusDesc(source.getStatus()));
优点:清晰可控;❌ 缺点:代码量增加。
策略 5:使用中间 DTO 层
设计一个与 source 结构一致的中间对象,先拷贝到中间对象,再手动转为目标对象。
适用于复杂转换场景。
七、重要注意事项(避坑指南)
| 问题 | 说明 | 建议 |
|---|---|---|
| 不拷贝 static/final 字段 | Hutool 默认跳过 | 正常行为,无需担心 |
| 不支持嵌套对象深度拷贝 | 默认只拷贝第一层引用 | 如需深拷贝,用 BeanUtil.toBean 或序列化方式 |
| getter/setter 必须存在 | 依赖 JavaBean 规范 | 确保字段有 public getter/setter |
| List ↔ List 可能失败 | 泛型擦除影响集合转换 | 避免跨类型集合直接拷贝 |
| 日期格式依赖系统默认 | String→Date 使用内置格式集 | 如需指定格式,建议手动转换 |
| 性能 vs 功能权衡 | 反射+转换有一定开销 | 高频场景可考虑 MapStruct 等编译期方案 |
八、替代方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Hutool BeanUtil | 简单、灵活、内置转换强 | 运行时反射,性能一般 | 快速开发、中小项目 |
| Spring BeanUtils | 无额外依赖 | 不支持类型转换 | Spring 项目简单拷贝 |
| MapStruct | 编译期生成代码,性能极高 | 需要注解、学习成本 | 高性能、大型项目 |
| Dozer | 支持复杂映射 | 重量级、已停止维护 | 老项目迁移 |
| ModelMapper | 配置灵活 | 性能较差 | 动态映射需求 |
九、总结
BeanUtil.copyProperties 是 强大且易用 的对象拷贝工具;
同名不同型 问题可通过 自动转换 + 自定义 Converter 解决;
遇到转换异常时,不要忽略,应检查数据合法性或注册转换器;
复杂场景建议 组合使用:自动拷贝 + 手动补全;
对于高频、高性能要求场景,考虑 MapStruct 等编译期方案。
更多推荐



所有评论(0)