要解决的问题
希望在反序列化 json 到 bean 时,对于 json 中未出现的字段,在 bean 中赋上默认值。
例如
Person 类如下:
1 |
|
json:1
{"name":"robert"}
反序列化后的 bean 为1
Person(name="robert", address="beijing")
但实际上,发序列化的结果为1
Person(name="robert", address=null)
解决过程
查看 maven 版本
项目中 jackson 的配置如下
1 | <dependency> |
配置升到最新后问题仍然存在。
debug json 反序列化过程,找到原因
json 反序列化是从1
com.fasterxml.jackson.databind.ObjectMapper#_readMapAndClose
这个方法调用开始的,里面的一段代码为:
1 | DeserializationConfig cfg = getDeserializationConfig(); |
在第 3 行找到的 JsonDeserializer 是 com.fasterxml.jackson.databind.deser.BeanDeserializer
从第 7 行代表进入 com.fasterxml.jackson.databind.deser.BeanDeserializer#deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext)
函数实现如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
// common case first
if (p.isExpectedStartObjectToken()) {
if (_vanillaProcessing) {
return vanillaDeserialize(p, ctxt, p.nextToken());
}
p.nextToken();
if (_objectIdReader != null) {
return deserializeWithObjectId(p, ctxt);
}
return deserializeFromObject(p, ctxt);
}
JsonToken t = p.getCurrentToken();
return _deserializeOther(p, ctxt, t);
}
vanillaDeserialize 为 false,最后走到了第 11 行,最后到了1
com.fasterxml.jackson.databind.deser.BeanDeserializer#_deserializeUsingPropertyBased
然后到1
com.fasterxml.jackson.databind.deser.impl.PropertyBasedCreator#build
在这个函数里有这样一段代码:1
Object bean = _valueInstantiator.createFromObjectWith(ctxt, buffer.getParameters(_allProperties));
调用的是1
com.fasterxml.jackson.databind.deser.ValueInstantiator#createFromObjectWith(com.fasterxml.jackson.databind.DeserializationContext, java.lang.Object[])
可以发现,createFromObjectWith 的第二个参数是数组,json 解出来的字段都放在了这个数组里。然后调用了 Person 类的全参构造函数,对于
缺失的字段自动补 null 值,这样就导致了 address 字段为 null。
解决方案
去掉 @AllArgsConstructor 时,没有问题了,因为此时找到的 com.fasterxml.jackson.databind.deser.BeanDeserializer
的 vanillaDeserialize 字段为 true,会调用 vanillaDeserialize(p, ctxt, p.nextToken());
,这个函数的实现非常明确:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23private final Object vanillaDeserialize(JsonParser p, DeserializationContext ctxt, JsonToken t) throws IOException {
final Object bean = _valueInstantiator.createUsingDefault(ctxt);
// [databind#631]: Assign current value, to be accessible by custom serializers
p.setCurrentValue(bean);
if (p.hasTokenId(JsonTokenId.ID_FIELD_NAME)) {
String propName = p.getCurrentName();
do {
p.nextToken();
SettableBeanProperty prop = _beanProperties.find(propName);
if (prop != null) { // normal case
try {
prop.deserializeAndSet(p, ctxt, bean);
} catch (Exception e) {
wrapAndThrow(e, bean, propName, ctxt);
}
continue;
}
handleUnknownVanilla(p, ctxt, bean, propName);
} while ((propName = p.nextFieldName()) != null);
}
return bean;
}
先用默认构造函数生成 bean,此时的 bean 是有默认值的,然后将 json 中出现的字段的值赋值给 bean,这样 address 就有值了。
根本原因
看上去是声明了全参构造函数导致的,所以想尝试自己写全参构造函数,在 address 为 null 时给其赋默认值。
写完如下:1
2
3
4
5
6
7public Person(String name, String address){
this.name = name;
this.address = address;
if(this.address == null){
this.address = "beijing";
}
}
继续走刚才 debug 的流程,发现居然没有请求这个全参构造函数。
那问题就是 @AllArgsConstructor 生成的全参函数有不同之处,jackson 能够识别出来并用于反序列化。查看 jar 包中 Person 类的代码发现其全参构造函数如下:1
2
3
4
5"name", "address"}) ({
public Person(String name, String address){
this.name = name;
this.address = address;
}
所以,区别就是 @ConstructorProperties({"name", "address"})
这个注解,这个注解的作用是指定构造函数参数的名字,Spring 可根据参数的名字注入 bean。
(补充自 liwei)
jackson 调用了全参构造函数的原因在于@AllArgsConstructor 的构造函数有ConstructorProperties ,jackson在选择构造函数的时候会调用BasicDeserializerFactory._addDeserialzerContructors方法,他首先选择无参构造函数,并遍历所有的构造函数,如果存在具有@ConstructProperties注解的构造函数,则把该构造函数作为默认创建bean的构造函数,如下:
可以通过设置 @AllArgsConstructor(suppressConstructorProperties=true) 来禁用 @ConstructorProperties.
结论
Lombok 的 @AllArgsConstructor 注解导致 Jackson 反序列化时调用了全参构造函数,将没有出现的字段都赋值为 null 了。
修改方式:
- 不使用 @AllArgsConstructor
- 使用 @AllArgsConstructor 但是不让其在全参构造函数上加入 ConstructorProperties 注解,声明方式改为 @AllArgsConstructor(suppressConstructorProperties = true)