开篇
把请求对象转成另一个对象,看起来只是复制同名属性。但源字段是 null 时,目标默认值可能被覆盖;复制后再改 list,还可能连原对象一起改。下面分别看这两种情况,给出可控制的写法。
环境:jdk 21、spring framework 6。beanconverter 是应用自有工具,依赖 spring 的属性解析及 guava 等组件,不是 spring beanutils 的别名。
坑点
坑点一:源字段没值,目标默认值也没了
例如目标对象的 name 默认是“匿名”,源对象的 name 是 null。直接把同名属性复制过去,未必会保留这个默认值。下面的演示模型供调用示例共用:
public static class user {
private string name = "匿名";
private list<string> tags = new arraylist<>();
public string getname() { return name; }
public void setname(string name) { this.name = name; }
public list<string> gettags() { return tags; }
public void settags(list<string> tags) { this.tags = tags; }
}坑点二:新建了对象,里面的集合却没有复制
新对象与新集合是两回事。属性直接赋值时,两个对象可能持有同一个 list,向副本中添加元素也会影响源对象。
正确写法
避坑一:跳过 null,保留新对象的默认值
beanconverter 的属性循环先取得源值,然后执行这段判断:
// 规则1:源对象属性值为null,跳过
if (sourcevalue == null) {
continue;
}目标对象由无参构造创建,再复制属性:
public static <t> t bean2bean(object source, class<t> targetclass, propertyconverter propertyconverter) {
paramnotnull(source, "source");
paramnotnull(targetclass, "targetclass");
try {
// 使用缓存的构造函数创建目标对象
t target = getconstructor(targetclass).newinstance();
return copyproperty(source, target, propertyconverter);
} catch (throwable throwable) {
throw wrapasruntimeexception(throwable, "bean2bean error, source: %s,target: %s",
source.getclass().getsimplename(), targetclass.getsimplename());
}
}这意味着它保留的是新对象的初始化值,不是读取数据库旧记录后替您做局部更新。这个公开入口也没有接收已有目标对象。
调用对照:
user source = new user(); source.setname(null); user ordinary = new user(); beanutils.copyproperties(source, ordinary); system.out.println(ordinary.getname()); // null user copy = beanconverter.bean2bean(source, user.class); system.out.println(copy.getname()); // 匿名 system.out.println(copy == source); // false
如果业务需要用 null 清空某个字段,就不能依赖这里的跳过规则。自定义转换器也处理不到这些 null,因为代码在调用转换器之前已经 continue。
避坑二:需要独立集合时,明确复制集合
同名属性的后续处理顺序如下。自定义转换器先获得机会;没有返回转换值时,再检查类型并直接赋值:
// 获取源属性类型
class<?> sourcepropertytype = sourcevalue.getclass();
// 获取目标属性类型
class<?> targetpropertytype = getpropertytype(targetclass, propertyname);
// 尝试调用自定义转换器, 如果自定义转换器明确返回null将继续尝试内部转换
if (propertyconverter != null) {
object customvalue = propertyconverter.convert(propertyname, sourcevalue, targetpropertytype);
if (customvalue != null) {
setpropertyvalue(target, targetclass, propertyname, customvalue);
continue;
}
}
// 规则2:源对象属性值不为null,且类型兼容,直接赋值
if (isassignable(targetpropertytype, sourcepropertytype)) {
setpropertyvalue(target, targetclass, propertyname, sourcevalue);
continue;
}
// 规则3:源对象属性值不为null,但类型不兼容,直接抛异常提示类型不一致无法转换。
throw wrapasruntimeexception("copyproperty error, 源对象: %s和目标对象: %s 的同名属性: %s类型不一致, 前者类型: %s, 后者类型: %s",
sourceclass.getsimplename(),
targetclass.getsimplename(),
propertyname,
sourcepropertytype.getsimplename(),
targetpropertytype.getsimplename());直接赋值没有递归复制对象。用下面的调用就能看到共享引用:
user source = new user();
source.gettags().add("java");
user copy = beanconverter.bean2bean(source, user.class);
system.out.println(copy.gettags() == source.gettags());
// true
copy.gettags().add("redis");
system.out.println(source.gettags());
// [java, redis]只需要独立的 list 容器时,可以通过 propertyconverter 复制这一项:
user source = new user();
source.gettags().add("java");
user copy = beanconverter.bean2bean(source, user.class,
(name, value, targettype) -> {
if ("tags".equals(name)) {
return new arraylist<>((list<?>) value);
}
return null;
});
system.out.println(copy.gettags() == source.gettags());
// false
copy.gettags().clear();
system.out.println(source.gettags());
// [java]返回 null 的意思是“继续默认转换”,不是“把目标字段设成 null”,也不是“跳过这个字段”。
这里复制的是容器,list 中的元素仍共享。例子用的是不可变 string;如果元素是可变的订单对象,修改元素内部字段仍可能影响源数据,需按实际模型继续复制。
适用条件
适合字段名相同、目标类型明确、希望保留默认值的对象转换。源值非空但类型不兼容时会报错,不会自动把字符串变成数字。父类或接口能够接收的实现类型,则可能直接赋值,例如 arraylist 赋给 list。
模型应有可访问的 getter/setter 和无参构造。实际实现遇到找不到 setter 会直接返回,因此只读属性不适合按“已经成功赋值”处理。复制结果仍需要业务校验,不能直接充当数据库更新指令。
总结
先定 null 的含义,再定引用是否要独立。跳过 null 可以保留新对象默认值;要修改副本里的集合,就显式复制需要独立的那一层。不要把“创建新对象”当成“完成深拷贝”。
如果这篇文章对您有用,欢迎关注。后续会继续拆解 java 后端的实用代码,讲清写法、原理和使用时要注意的细节。
到此这篇关于java 对象拷贝避坑:如何避免空值覆盖与数据误改的文章就介绍到这了,更多相关java 对象拷贝内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论