cn.hutool.core.bean.BeanUtil#copyProperties(java.lang.Object, java.lang.Class<T>, java.lang.String...) 这个工具的拷贝是深拷贝,还是浅拷贝;字段名相同,类型不同的对象,怎么处理?
一、BeanUtil.copyProperties 的拷贝类型:浅拷贝
Hutool 的 BeanUtil.copyProperties 本质是基于反射的属性值复制,仅复制对象的表层属性值:
- 对于基本类型/包装类型(如
int、String),会复制值本身; - 对于引用类型(如自定义对象、
List、Map),仅复制引用地址,不会递归复制嵌套对象的内容。
例:若源对象有一个List<User>,复制后目标对象的List<User>与源的List指向同一个内存地址,修改其中一个会影响另一个。
二、字段名相同但类型不同的处理方式:尝试类型转换,失败则抛异常
当源字段与目标字段名称相同但类型不同时,Hutool 会通过内置的 Convert 工具类尝试自动类型转换,规则如下:
- 可转换场景:若类型之间存在合理的转换路径(如
String→Integer、Long→String、Date→LocalDateTime),会自动转换并赋值。
例:源字段age = "18"(String),目标字段age为int,会转换为18。 - 不可转换场景:若类型无兼容转换逻辑(如
User→Order、List→Map),会抛出ConversionException异常。 - 特殊说明:若字段是引用类型且类型完全不同(如源是
A类,目标是B类),即使字段名相同,也不会自动复制属性,而是直接尝试转换(通常失败)。
补充建议
- 若需深拷贝,可结合 Hutool 的
ObjectUtil.cloneByStream(基于序列化,要求对象实现Serializable)或手动递归复制嵌套对象; - 若需跳过类型不匹配的字段,可通过第三个参数
ignoreProperties忽略该字段,或自定义CopyOptions配置类型转换策略(如禁用转换)。
参考 Hutool 官方文档:BeanUtil.copyProperties。
浅拷贝 对象类型不同,同一个类,包名不同也不能复制
你说得完全正确,这是 BeanUtil.copyProperties 的一个关键限制。
核心结论
是的,即使是完全相同的类名(Class Name),只要包名(Package)不同,BeanUtil.copyProperties 也无法直接复制。
这是因为 Java 中判断两个类是否相同,必须同时满足“类名相同 + 包名相同 + 类加载器相同”。只要包名不同,它们在 JVM 看来就是两个毫无关系的 Class。
详细机制解析
1. 为什么不能复制?
BeanUtil.copyProperties 的工作流程是:
- 遍历源对象的 Getter 方法(如
getUserName())。 - 根据属性名(如
userName)去目标 Class 中寻找对应的 Setter 方法(如setUserName())。 - 检查类型兼容性:调用
sourceValue.getClass()和targetSetter.getParameterTypes()[0]进行比对。
当包名不同时:
- 源字段类型是
com.pkgA.User - 目标字段类型是
com.pkgB.User - 虽然都叫
User,但在 Java 类型系统中它们是完全不兼容的类型。 - Hutool 的
Convert工具无法将com.pkgA.User自动转换为com.pkgB.User。 - 结果:抛出
ConversionException或IllegalArgumentException(类型不匹配)。
2. 这与“浅拷贝”无关
你提到的“浅拷贝”在这里是干扰项。无论深拷贝还是浅拷贝,前提都是源和目标类型要兼容。
- 浅拷贝:只决定引用对象是否新建(Hutool 不新建)。
- 类型不同:直接连赋值这一步都进不去。
解决方案
如果你确实需要在不同包的同名类之间拷贝属性(常见于 DTO/DO/VO 分层,且包名规范不一致),有以下几种方案:
方案一:使用 JSON 作为中转(最常用)
这是最简单粗暴但有效的办法,绕过 Java 类型系统。
// 1. 源对象 -> JSON String
String json = JSONUtil.toJsonStr(source);
// 2. JSON String -> 目标对象
TargetObj target = JSONUtil.toBean(json, TargetObj.class);
- 优点:无需关心包名,字段名匹配即可。
- 缺点:性能开销大,且会丢失瞬态(transient)信息。
方案二:自定义 Converter(高级)
实现 Converter 接口,手动处理特定类型的转换。
Convert.register(new Converter<SourceUser, TargetUser>() {
@Override
public TargetUser convert(SourceUser source, TargetUser target) {
// 手动 new 目标对象并 copy 基本属性
if (target == null) target = new TargetUser();
BeanUtil.copyProperties(source, target, CopyOptions.create().ignoreError());
return target;
}
}, SourceUser.class);
方案三:使用 Map 中转
// 1. 对象转 Map
Map<String, Object> map = BeanUtil.beanToMap(source);
// 2. Map 填充到新对象
TargetObj target = BeanUtil.mapToBean(map, TargetObj.class, false);
方案四:重构代码(最佳实践)
强烈建议:如果是自己维护的代码,尽量统一使用单一来源的类定义,或者通过 Maven 依赖引入公共的 DTO 模块,避免同名不同包的“伪相同”类。
总结对比表
| 场景 | BeanUtil.copyProperties 行为 |
|---|---|
| 同包同名类 | ✅ 正常复制(浅拷贝) |
| 同包不同名 | ❌ 字段名不同时忽略,相同时尝试类型转换 |
| 不同包同名类 | ❌ 直接报错(类型不兼容) |
| 基本类型 vs 包装类型 | ✅ 自动拆箱/装箱 |
| String vs Integer | ✅ 尝试字符串转数字 |
一句话总结:Hutool 的 BeanUtil 是基于 Java 反射和类型系统 的工具,它严格遵守 JVM 的类隔离规则,不同包的类哪怕长得一模一样,也绝不互认。遇到这种情况,请使用 JSON 中转 或 重构代码。
基于json或序列化的拷贝,叫什么名字,比深拷贝和彻底
这是一个非常好的进阶问题。基于 JSON 或序列化的拷贝,在行业内通常被称为 “序列化拷贝”(Serialization Copy) 或 “基于序列化的深拷贝”。
为了让你更清晰地理解它的定位,我们需要把它放在“拷贝深度”的谱系里看:
拷贝深度的谱系
| 拷贝方式 | 别名 | 原理 | 特点 |
|---|---|---|---|
| 浅拷贝 (Shallow Copy) | - | 复制对象本身 + 引用地址 | 快,但嵌套对象共享内存(Hutool BeanUtil 默认行为)。 |
| 深拷贝 (Deep Copy) | 递归拷贝 | 递归地复制所有嵌套对象 | 慢,但完全独立。难点在于循环引用(A 包含 B,B 又包含 A)。 |
| 序列化拷贝 (Serialization Copy) | 你问的这种 | 将对象转为字节流/字符串,再还原 | 最彻底、最暴力。不仅能解决深拷贝,还能解决跨类加载器、跨包名的问题。 |
核心概念:为什么它叫“序列化拷贝”?
- 序列化 (Serialization):把内存中的对象转换成一种可存储或可传输的格式(如 JSON 字符串、二进制字节流)。这个过程会把对象的所有层级结构“拍扁”成一种线性格式。
- 反序列化 (Deserialization):把这种线性格式重新构建成一个新的内存对象。
因为在这个过程中,JVM 完全抛弃了原来的对象引用,而是根据数据重新创建了一个全新的对象树,所以它具有以下几个独特的性质:
1. 它是“绝对深拷贝”
它不仅复制了对象,还复制了对象里的所有对象,以及那些对象里的所有对象……彻底切断了与原对象的一切联系。
2. 它是“无视血缘关系”的拷贝
正如你之前遇到的痛点:不同包的同名类无法复制。 使用序列化拷贝时,JVM 不再关心“这个类来自哪个 ClassLoader 或哪个 Package”,它只关心:“目标类的字段名和 JSON 里的 Key 是否匹配”。
- JSON 拷贝:
JSON.parse(JSON.stringify(obj))或 Hutool 的JSONUtil。 - 二进制序列化拷贝:Java 原生的
ObjectOutputStream/ObjectInputStream(要求implements Serializable)。
具体叫法汇总
在不同的语境下,你可能会听到以下几种叫法,其实指的都是同一类东西:
- 序列化深拷贝 (Serialized Deep Copy):最准确的学术叫法。
- 基于流的深拷贝 (Stream-based Deep Copy):特指 Java 原生序列化(通过
ByteArrayOutputStream等)。 - 克隆通过序列化 (Cloning via Serialization):Apache Commons Lang 库中的
SerializationUtils.clone()就是这个原理。 - JSON 深拷贝 (JSON Deep Copy):前端和 Web 开发中最常用的说法(利用
JSON.stringify和JSON.parse)。
针对你场景的最佳实践
既然你遇到了 “不同包、同名类” 的问题,我推荐使用 JSON 序列化拷贝。
Hutool 代码示例:
import cn.hutool.json.JSONUtil;
// 源对象 (com.pkgA.User)
com.pkgA.User sourceUser = new com.pkgA.User();
sourceUser.setName("张三");
// 1. 序列化为 JSON 字符串
String jsonStr = JSONUtil.toJsonStr(sourceUser);
// 2. 反序列化为目标对象 (com.pkgB.User)
com.pkgB.User targetUser = JSONUtil.toBean(jsonStr, com.pkgB.User.class);
注意事项(代价):
- 性能:这是最慢的方式(相比反射),不适合高频循环。
- 类型丢失:JSON 无法完美保留某些特定类型(如
LocalDateTime可能需要指定格式,BigDecimal可能被转为数字精度丢失)。 - 静态变量/Transient:
transient修饰的字段不会被序列化。
总结
你寻找的那个词就是 “序列化拷贝”。它是解决 “浅拷贝不够深” 和 “同构类无法互拷” 这两个问题的终极手段。