Hutool BeanUtil拷贝属性详解:浅拷贝与类型处理

小豆丁 6小时前 ⋅ 3 阅读
聊天记录
头像

cn.hutool.core.bean.BeanUtil#copyProperties(java.lang.Object, java.lang.Class<T>, java.lang.String...) 这个工具的拷贝是深拷贝,还是浅拷贝;字段名相同,类型不同的对象,怎么处理?

头像

一、BeanUtil.copyProperties 的拷贝类型:浅拷贝

Hutool 的 BeanUtil.copyProperties 本质是基于反射的属性值复制,仅复制对象的表层属性值

  • 对于基本类型/包装类型(如 intString),会复制值本身;
  • 对于引用类型(如自定义对象、ListMap),仅复制引用地址,不会递归复制嵌套对象的内容。
    例:若源对象有一个 List&lt;User&gt;,复制后目标对象的 List&lt;User&gt; 与源的 List 指向同一个内存地址,修改其中一个会影响另一个。

二、字段名相同但类型不同的处理方式:尝试类型转换,失败则抛异常

当源字段与目标字段名称相同但类型不同时,Hutool 会通过内置的 Convert 工具类尝试自动类型转换,规则如下:

  1. 可转换场景:若类型之间存在合理的转换路径(如 StringIntegerLongStringDateLocalDateTime),会自动转换并赋值。
    例:源字段 age = &quot;18&quot;String),目标字段 ageint,会转换为 18
  2. 不可转换场景:若类型无兼容转换逻辑(如 UserOrderListMap),会抛出 ConversionException 异常。
  3. 特殊说明:若字段是引用类型且类型完全不同(如源是 A 类,目标是 B 类),即使字段名相同,也不会自动复制属性,而是直接尝试转换(通常失败)。

补充建议

  • 若需深拷贝,可结合 Hutool 的 ObjectUtil.cloneByStream(基于序列化,要求对象实现 Serializable)或手动递归复制嵌套对象;
  • 若需跳过类型不匹配的字段,可通过第三个参数 ignoreProperties 忽略该字段,或自定义 CopyOptions 配置类型转换策略(如禁用转换)。

参考 Hutool 官方文档:BeanUtil.copyProperties

头像

浅拷贝 对象类型不同,同一个类,包名不同也不能复制

头像

你说得完全正确,这是 BeanUtil.copyProperties 的一个关键限制

核心结论

是的,即使是完全相同的类名(Class Name),只要包名(Package)不同,BeanUtil.copyProperties 也无法直接复制。

这是因为 Java 中判断两个类是否相同,必须同时满足“类名相同 + 包名相同 + 类加载器相同”。只要包名不同,它们在 JVM 看来就是两个毫无关系的 Class


详细机制解析

1. 为什么不能复制?

BeanUtil.copyProperties 的工作流程是:

  1. 遍历源对象的 Getter 方法(如 getUserName())。
  2. 根据属性名(如 userName)去目标 Class 中寻找对应的 Setter 方法(如 setUserName())。
  3. 检查类型兼容性:调用 sourceValue.getClass()targetSetter.getParameterTypes()[0] 进行比对。

当包名不同时:

  • 源字段类型是 com.pkgA.User
  • 目标字段类型是 com.pkgB.User
  • 虽然都叫 User,但在 Java 类型系统中它们是完全不兼容的类型
  • Hutool 的 Convert 工具无法将 com.pkgA.User 自动转换为 com.pkgB.User
  • 结果:抛出 ConversionExceptionIllegalArgumentException(类型不匹配)。

2. 这与“浅拷贝”无关

你提到的“浅拷贝”在这里是干扰项。无论深拷贝还是浅拷贝,前提都是源和目标类型要兼容

  • 浅拷贝:只决定引用对象是否新建(Hutool 不新建)。
  • 类型不同:直接连赋值这一步都进不去。

解决方案

如果你确实需要在不同包的同名类之间拷贝属性(常见于 DTO/DO/VO 分层,且包名规范不一致),有以下几种方案:

方案一:使用 JSON 作为中转(最常用)

这是最简单粗暴但有效的办法,绕过 Java 类型系统。

JAVA
// 1. 源对象 -> JSON String
String json = JSONUtil.toJsonStr(source);

// 2. JSON String -> 目标对象
TargetObj target = JSONUtil.toBean(json, TargetObj.class);
  • 优点:无需关心包名,字段名匹配即可。
  • 缺点:性能开销大,且会丢失瞬态(transient)信息。

方案二:自定义 Converter(高级)

实现 Converter 接口,手动处理特定类型的转换。

JAVA
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 中转

JAVA
// 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) 你问的这种 将对象转为字节流/字符串,再还原 最彻底、最暴力。不仅能解决深拷贝,还能解决跨类加载器、跨包名的问题。

核心概念:为什么它叫“序列化拷贝”?

  1. 序列化 (Serialization):把内存中的对象转换成一种可存储或可传输的格式(如 JSON 字符串、二进制字节流)。这个过程会把对象的所有层级结构“拍扁”成一种线性格式。
  2. 反序列化 (Deserialization):把这种线性格式重新构建成一个新的内存对象。

因为在这个过程中,JVM 完全抛弃了原来的对象引用,而是根据数据重新创建了一个全新的对象树,所以它具有以下几个独特的性质:

1. 它是“绝对深拷贝”

它不仅复制了对象,还复制了对象里的所有对象,以及那些对象里的所有对象……彻底切断了与原对象的一切联系

2. 它是“无视血缘关系”的拷贝

正如你之前遇到的痛点:不同包的同名类无法复制。 使用序列化拷贝时,JVM 不再关心“这个类来自哪个 ClassLoader 或哪个 Package”,它只关心:“目标类的字段名和 JSON 里的 Key 是否匹配”。

  • JSON 拷贝JSON.parse(JSON.stringify(obj)) 或 Hutool 的 JSONUtil
  • 二进制序列化拷贝:Java 原生的 ObjectOutputStream / ObjectInputStream(要求 implements Serializable)。

具体叫法汇总

在不同的语境下,你可能会听到以下几种叫法,其实指的都是同一类东西:

  1. 序列化深拷贝 (Serialized Deep Copy):最准确的学术叫法。
  2. 基于流的深拷贝 (Stream-based Deep Copy):特指 Java 原生序列化(通过 ByteArrayOutputStream 等)。
  3. 克隆通过序列化 (Cloning via Serialization):Apache Commons Lang 库中的 SerializationUtils.clone() 就是这个原理。
  4. JSON 深拷贝 (JSON Deep Copy):前端和 Web 开发中最常用的说法(利用 JSON.stringifyJSON.parse)。

针对你场景的最佳实践

既然你遇到了 “不同包、同名类” 的问题,我推荐使用 JSON 序列化拷贝

Hutool 代码示例:

JAVA
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 可能被转为数字精度丢失)。
  • 静态变量/Transienttransient 修饰的字段不会被序列化。

总结

你寻找的那个词就是 “序列化拷贝”。它是解决 “浅拷贝不够深”“同构类无法互拷” 这两个问题的终极手段。

全部评论: 0

    我有话说: