Fastjson反序列化丢失属性的问题
2019-05-05 本文已影响1人
jerrik
【问题描述】:
由于线上某个服务请求量非常大(每天的量10亿以上),需要频繁的调用xx服务,而且xx服务返回的数据不会经常发生变化.所以我就在调用方做了一层缓存;将xx服务响应的结果缓存到了redis中,然后第二次请求来的时候直接从缓存里获取。但是观察日志,发现从redis中获取的value反序列化后只保留了ip属性。代码如下:
String cacheKey = this.buildCacheKey(moduleName, moduleExtInfo, currentIP);
String cacheValue = this.loadCache(cacheKey);
if (StringUtils.isNotBlank(cacheValue)) {
return;
}
//限制相同数据并发加入到Queue
limitConcurrentAddQueue(cacheKey);
//对应STEP2
T resBean = this.processIpInfo(currentIP);
//STEP2
ResCommonBean processIpInfo(final String currentIP) {
ResCacheListenerExecution<ResCommonBean> execution = new ResCacheListenerExecution<ResCommonBean>();
//STEP3中的方法
execution.execute(new CacheListener<ResCommonBean>() {
@Override
public String cacheKey() {
return AttributeKey.COMMON_IP_CACHE_KEY + currentIP;
}
@Override
public ResCommonBean execute() {
ResCommonBean commBean = resolveCommonBean(currentIP);
return commBean;
}
});
return execution.getCacheOrReloadBean();
}
//STEP3
public void execute(CacheListener<T> listener) {
this.listener = listener;
try {
String cacheKey = listener.cacheKey();
String cacheValue = RedisUtil.get(cacheKey);
if (StringUtils.isNotBlank(cacheValue)) {
setRealClassType(listener);
//这里做的反序列化
this.instance = (T) JSON.parseObject(cacheValue, this.realClazz);
} else {
this.instance = this.listener.execute();
if (null != this.instance) {
RedisUtil.setWithExpire(cacheKey, JSON.toJSONString(this.instance), this.expireRange());
}
}
} catch (Exception e) {
throw new CacheListenerExecuteException(e.getMessage());
}
}
//ResCommonBean 实体类
public class ResCommonBean {
private String idc;
private String omp;
private String set;
private String cell;
private String yq;
private String ip;
public ResCommonBean(String ip) {
super();
this.ip = ip;
}
...省略getter、setter
}
其实稍微有点经验的coder一眼就知道是构造函数的问题,但是开发的时候有太多非主观因素,所以很容易走进死胡同。而且这套代码存在了很多年,想当然的以为一切都是正常的。为了验证这个问题,还是在本地重现一下,顺便简单剖析一下fastjson怎么处理这类情况的。
一、问题重现
- 新建一个User类
public class User {
private Integer id;
private String userName;
private int age;
public User(Integer id) {
this.id = id;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this);
}
}
- 序列化与反序列测试
User user1 = new User(3);
user1.setId(3);
user1.setUserName("jerrik");
user1.setAge(23);
System.out.println(JSON.toJSONString(user1));
String json = "{\"age\":23,\"id\":3,\"userName\":\"jerrik\"}";
User user2 = JSON.parseObject(json,User.class);
System.out.println(JSON.toJSONString(user2));
输出结果:
{"age":23,"id":3,"userName":"jerrik"}
{"age":0,"id":3}
确实丢失了userName和age属性(0是默认值)。
二、DEBUG
我们直接定位到JavaBeanDeserializer:
try {
if (hasNull && beanInfo.kotlinDefaultConstructor != null) {
object = beanInfo.kotlinDefaultConstructor.newInstance(new Object[0]);
for (int i = 0; i < params.length; i++) {
final Object param = params[i];
if (param != null && beanInfo.fields != null && i < beanInfo.fields.length) {
FieldInfo fieldInfo = beanInfo.fields[i];
fieldInfo.set(object, param);
}
}
} else {
object = beanInfo.creatorConstructor.newInstance(params);
}
} catch (Exception e) {
throw new JSONException("create instance error, " + paramNames + ", "
+ beanInfo.creatorConstructor.toGenericString(), e);
}
如果对应的bean(这里是User)默认构造函数不为空,就利用无参构造器创建一个对象,然后获取该对象所有的Field,通过反射来设置属性。重点是else
方法,如果没有默认构造函数,则直接创建对象,没有使用反射来为Field赋值。
由于这里的User
对象没有无参构造函数,所以就只保留了构造函数中的参数属性。
三、fastjson为何要这样做?
个人猜想也是处于代码规范的角度,谁叫你不定义无参构造呢?话又说回来,如果没有无参构造,那只能通过显示的有参构造来创建对象,如果还为其中的Field设置属性,则会将构造函数中设置的值覆盖掉。其实fastjson也可以记录构造函数中已经设置过的值,然后再针对性的setter。可能在设计层面怕引入过多的复杂度,所以做了妥协。