工作当中我们可能需要频繁的转换对象属性来满足最终业务需要的数据结构,最麻烦的就是get旧对象后set到新对象中。当然工作中大家常用的基本都是Spring的BeanUtils的复制方法,但是有时不能满足业务需要。
如题,在工作中遇到了类拷贝问题,于是写下这篇文章记录一下。由于fastjson出过严重bug但是好像目前还有很多公司在用,本文以fastjson为案例提供思路,感觉其他工具的自定义转换可能方法也类似(Gson等...?)
1.问题起点
简易代码,只突出显示拷贝问题的属性值(省略 get set 构造器以及toString方法)
下文案例为 User类转换为UserDTO类 图省事List中的对象也使用的本类
User类:
public class User {
private Long id;
private String education;
List<User> userList;
}
UserDTO类
public class UserDTO {
private Long id;
private EducationEnum education;
List<UserDTO> userList;
}
EducationEnum枚举类
public enum EducationEnum {
/**
* 小学
*/
PRIMARY("primary"),
/**
* 初中
*/
MIDDLE("middle"),
/**
* 高中
*/
HIGH("high");
private String value;
EducationEnum(String value) {this.value = value;}
public String getValue() {return value;}
public void setValue(String value) {this.value = value;}
}
如上代码可以看出 User类的education属性为String,UserDTO的education属性为Enum,并且下方list中泛型对应也不一致,list中对象的属性值也存在差异。
2.测试BeanUtils的Copy
我们首先想到Spring的BeanUtils为我们提供的拷贝方法
下方构建数据代码使用lombok的@Builder注解在Bean上请自行添加依赖并且添加注解
List<User> userList =new ArrayList<>();
User user2 = User.builder().id(2L).education("primary").build();
User user3 = User.builder().id(3L).education("middle").build();
userList.add(user2);
userList.add(user3);
User user = User.builder().id(1L).education("high").userList(userList).build();
UserDTO dto = new UserDTO();
BeanUtils.copyProperties(user, dto);
System.out.println(dto);
上方代码我们构建User对象后使用BeanUtils的copyProperties方法来copy对象,那么结果是什么样的呢?
可以看出,我们copy失败了,即时字段名一致,string转换到枚举失败了,list也copy失败了。
3.测试fastjson的copy
有问题我们就要解决,其实使用传统的get属性再重新set进入新对象也是可以的,但是User类中有一个List,并且List中的泛型也不一致,需要双重循环解决String转换为Enum的问题,这就很头疼了。下面我们上菜--------fastjson。
首先我们需要添加一些代码-----序列化器与反序列化器,这就是我们定制copy的重点
*需要注意的是,fastjson需要User类有无参构造器 不然会报错,所以为了方便我们还是将无参构造和有参构造都加上吧~
EducationEnumSerializer序列化器
public class EducationEnumSerializer implements ObjectSerializer {
@Override
public void write(JSONSerializer serializer, Object object, Object fieldName, Type fieldType, int features) throws IOException {
EducationEnum educationEnum = (EducationEnum)object;
serializer.out.writeString(educationEnum.getValue());
}
}
EducationEnumDeserializer反序列化器
public class EducationEnumDeserializer implements ObjectDeserializer {
@SuppressWarnings("unchecked")
@Override
public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
// 解析值为字符串
String value = parser.parseObject(String.class);
// 遍历所有的枚举实例
for (EducationEnum education : EducationEnum.values()) {
if (education.getValue().equals(value)) {
// 成功匹配,返回实例
return (T) education;
}
}
// 没有匹配到,可以抛出异常或者返回null
return null;
}
@Override
public int getFastMatchToken() {
// 仅仅匹配字符串类型的值
return JSONToken.LITERAL_STRING;
}
}
UserDTO类修改
public class UserDTO {
private Long id;
@JSONField(serializeUsing = EducationEnumSerializer.class, deserializeUsing = EducationEnumDeserializer.class)
private EducationEnum education;
List<UserDTO> userList;
}
如上,我们添加了序列化器与反序列化器,目的是将String转换为Enum类型,添加完序列化器与反序列化器后,我们还需要在实体类上添加@JSONField注解并且指定序列化器与反序列化器
转换代码
List<User> userList =new ArrayList<>();
User user2 = User.builder().id(2L).education("primary").build();
User user3 = User.builder().id(3L).education("middle").build();
userList.add(user2);
userList.add(user3);
User user = User.builder().id(1L).education("high").userList(userList).build();
String json = JSON.toJSONString(user);
UserDTO userDTO = JSON.parseObject(json, UserDTO.class);
System.out.println(userDTO);
下面我们来看看结果
芜湖,DTO中的枚举类型有值,userlist也复制成功且userList中的education属性是枚举,成功~
4.题外话--深拷贝与浅拷贝
什么是浅拷贝
对当前对象进行克隆,并克隆该对象所包含的8种基本数据类型和String类型属性(拷贝一份该对象并重新分配内存,即产生了新的对象);但如果被克隆的对象中包含除8中数据类型和String类型外的其他类型的属性,浅克隆并不会克隆这些属性(即不会为这些属性分配内存,而是引用原来对象中的属性)。
Spring的BeanUils的copyProperties方法就是浅拷贝
如图,我们将user对象copy成newUser,我们来看看具体的地址
可以看出user与newUser中userList的地址相同,这意味着我们修改user对象userlist中的对象时,newUser的userList中也会修改。
什么是深拷贝
深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
json的copy就是深拷贝
我们看看结果
地址不同了,业务上如果有User类也不怕了~
5.总结
本文为自定义拷贝提供了一种思路,fastjson虽然出过严重bug但是普及性还是比较广,需要注意的是,想要拷贝的属性名必须相同,如果不同应该需要利用其他注解来绑定,感谢大家的浏览,如有问题欢迎指出!笔者也是在当前大环境下发育打怪的一个player