问题:
用spring自定义Deserializer并且需要获取到当前解析字段所在对象实例时,某种情况会导出获取到的对象实例并非当前字段所在对象。
版本:
spring boot 2.1.4
jackson-databind 2.9.8
场景:
首先先看看定义的实体结构
public class Dept{
private User user;
private String deptName
}
public class User{
private String userName;
}
然后是自定义 Deserializer 的代码,重点关注图中的 currentValue
public class CustomDeserializer extends JsonDeserializer<IDictBean> {
@Override
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
JsonNode node = p.getCodec().readTree(p);
//当前解析的json字段名称
String currentName = p.currentName();
//当前字段所在对象的实体
Object currentValue = p.getCurrentValue();
return node.asText();
}
}
下面是 Controller 的方法代码
@RequestMapping(value = "", method = RequestMethod.POST)
public void cabinMapCreate(@RequestBody Dept dept) {
}
接下来是针对三种不同的 json 转换成 dept 并在绑定deptName字段时 currentValue 的区别
第一种 json : currentValue 为 Dept 的实例 ,此处无论 user 里的字段是 abc 或是 userName 都一样
{
"user":{
"abc":""
},
"deptName":"技术部"
}
第二种 json : currentValue 为 Dept 的实例
{
"user": null
"deptName":"技术部"
}
3.第三种 json : 解析 deptName 时则 currentValue 为 User 的实例,这也是问题所在,deptName是Dept的字段,currentValue却是User。
{
"user":{},
"deptName":"技术部"
}
原因分析:
首先,json解析的顺序是从上往下,如下面的json会先解析 user 字段,然后解析 user 里的 abc ,接着在解析deptName.
{
"user":{
"abc":""
},
"deptName":"技术部"
}
接下来会通过 BeanDeserializer 源码中的 deserialize 和 deserializeFromObject 方法分析上面三种情况的区别:(BeanDeserializer 是用于反序列化对象用的)
源码中的注释讲的都是解析 user 字段的过程
第一种json
@Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException{
//在解析 user 字段时, currentValue 是 Dept 的实例
//判断 json 中 user: 后面是不是 "{" 开头,是的话表示user是一个对象.则此处为ture
//此处p.isExpectedStartObjectToken() 为 true,走if里的逻辑
if (p.isExpectedStartObjectToken()) {
if (_vanillaProcessing) {
return vanillaDeserialize(p, ctxt, p.nextToken());
}
p.nextToken();
if (_objectIdReader != null) {
return deserializeWithObjectId(p, ctxt);
}
//此deserializeFromObject方法里会创建一个user实例,并会修改 currentValue 的值
return deserializeFromObject(p, ctxt);
}
return _deserializeOther(p, ctxt, p.getCurrentToken());
}
@Override
public Object deserializeFromObject(JsonParser p, DeserializationContext ctxt) throws IOException{
.......省略若干代码..........
//这里是会创建一个 User 对象,即 bean=user 对象
final Object bean = _valueInstantiator.createUsingDefault(ctxt);
//此处会让 currentValue由 dept 改成 user 实例.因此如果后续没有地方把currentValue改成 dept,则 currentValue 就会一直都是 user 的实例
p.setCurrentValue(bean);
if (p.canReadObjectId()) {
Object id = p.getObjectId();
if (id != null) {
_handleTypedObjectId(p, ctxt, bean, id);
}
}
if (_injectables != null) {
injectValues(ctxt, bean);
}
if (_needViewProcesing) {
Class<?> view = ctxt.getActiveView();
if (view != null) {
return deserializeWithView(p, ctxt, bean, view);
}
}
//此处会判断当前标识是不是字段名称,此处是 abc 字段,所以p.hasTokenId(JsonTokenId.ID_FIELD_NAME) 为 true
if (p.hasTokenId(JsonTokenId.ID_FIELD_NAME)) {
//此处拿到的 propName 是 abc
// 此处执行了 do while,而 while 里的p.nextFieldName()会在找不到下一个字段(此处指的是user内的下一个字段)的时候把 currentValue 设为dept的实例,因此 currentValue 又被改回dept了,所以解析下一个字段deptName的时候 currentValue=dept
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;
}
第二种json
@Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException{
//在解析 user 进入这个方法的时候 currentValue 是 Dept 的实例
//判断 json 中 user: 后面是不是 "{" 开头,是的话表示user是一个对象,则此处为true,否则为false
//user 后面是 null,所以此处为false
if (p.isExpectedStartObjectToken()) {
if (_vanillaProcessing) {
return vanillaDeserialize(p, ctxt, p.nextToken());
}
p.nextToken();
if (_objectIdReader != null) {
return deserializeWithObjectId(p, ctxt);
}
return deserializeFromObject(p, ctxt);
}
//这里会直接返回然后继续解析下一个 deptName 字段,因此解析 deptName 时 currentValue 是 Dept 的实例
return _deserializeOther(p, ctxt, p.getCurrentToken());
}
第三种json
@Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException{
//在解析 user 字段时, currentValue 是 Dept 的实例
//判断 json 中 user: 后面是不是 "{" 开头,是的话表示user是一个对象.则此处为ture
//此处p.isExpectedStartObjectToken() 为 true,走if里的逻辑
if (p.isExpectedStartObjectToken()) {
if (_vanillaProcessing) {
return vanillaDeserialize(p, ctxt, p.nextToken());
}
p.nextToken();
if (_objectIdReader != null) {
return deserializeWithObjectId(p, ctxt);
}
//此deserializeFromObject方法里会创建一个user实例,并会修改 currentValue 的值
return deserializeFromObject(p, ctxt);
}
return _deserializeOther(p, ctxt, p.getCurrentToken());
}
@Override
public Object deserializeFromObject(JsonParser p, DeserializationContext ctxt) throws IOException{
.......省略若干代码..........
//这里是会创建一个 User 对象,即 bean=user 对象
final Object bean = _valueInstantiator.createUsingDefault(ctxt);
//此处会让 currentValue由 dept 改成 user 实例.因此如果后续没有地方把currentValue改成 dept,则 currentValue 就会一直都是 user 的实例
p.setCurrentValue(bean);
if (p.canReadObjectId()) {
Object id = p.getObjectId();
if (id != null) {
_handleTypedObjectId(p, ctxt, bean, id);
}
}
if (_injectables != null) {
injectValues(ctxt, bean);
}
if (_needViewProcesing) {
Class<?> view = ctxt.getActiveView();
if (view != null) {
return deserializeWithView(p, ctxt, bean, view);
}
}
//此处会判断当前标识是不是字段名称,user 后面是 {} ,所以p.hasTokenId(JsonTokenId.ID_FIELD_NAME) 为 false
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);
}
//上面 while 里的p.nextFieldName()会在找不到下一个字段(此处指的是user内的下一个字段)的时候把 currentValue 改为 dept 的实例,因为没执行到 while ,所以 currentValue 没有被改回 dept ,所以解析下一个字段deptName的时候 currentValue=user
return bean;
}
总结:
综上所述,在解析user的时候
第一种json(user:{"${any filed}":""}) 会修改currentValue=user实例,但当解析完 user 后会把currentValue 还原为 dept实例
第二种json(user:null)不会修改 currentValue的指向,所以currentValue=dept的实例
第三种json (user:{}) 会修改currentValue=user实例,但最后却没有一个点可以把currentValue 还原为 dept实例,导致后续解析其他字段的时候会获取到错误的currentValue.
@Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException{
//在解析 user 进入这个方法的时候 currentValue 是 Dept 的实例
//判断 json 中 user: 后面是不是 "{" 开头,是的话表示user是一个对象.此处为ture,否则为false
//第一种和第三种json 为true,走 if 里的逻辑,第二种json为false
if (p.isExpectedStartObjectToken()) {
if (_vanillaProcessing) {
return vanillaDeserialize(p, ctxt, p.nextToken());
}
p.nextToken();
if (_objectIdReader != null) {
return deserializeWithObjectId(p, ctxt);
}
//此方法里会创建一个user实例,并会修改currentValue的值
//就是这方法里面逻辑导致了上述第一种和第三种 json 参数的差异
return deserializeFromObject(p, ctxt);
}
//上面第二种 json 种走的是这里的逻辑,会直接返回继续解析下一个 deptName 字段,因此解析 deptName 时 currentValue 是 Dept 的实例
return _deserializeOther(p, ctxt, p.getCurrentToken());
}
@Override
public Object deserializeFromObject(JsonParser p, DeserializationContext ctxt) throws IOException{
.......省略若干代码..........
//这里是会创建一个 User 对象,即 bean=user 对象
final Object bean = _valueInstantiator.createUsingDefault(ctxt);
//此处会让 currentValue=user实例.因此如果后续没有地方把currentValue改成 dept,则 currentValue 就会一直都是 user 的实例
p.setCurrentValue(bean);
if (p.canReadObjectId()) {
Object id = p.getObjectId();
if (id != null) {
_handleTypedObjectId(p, ctxt, bean, id);
}
}
if (_injectables != null) {
injectValues(ctxt, bean);
}
if (_needViewProcesing) {
Class<?> view = ctxt.getActiveView();
if (view != null) {
return deserializeWithView(p, ctxt, bean, view);
}
}
//此处会判断当前标识是不是字段名称,第一种json对应 abc 字段所以为true, 第三种json则为false
if (p.hasTokenId(JsonTokenId.ID_FIELD_NAME)) {
//此处拿到的 propName 是 abc
// while 里的p.nextFieldName()会在找不到下一个字段(此处指的是user里面的下一个字段)的时候会把 currentValue 设为dept的实例,然后返回继续解析下一个 deptName 的字段,因此第一种 json 拿到的 currentValue=dept实例
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);
}
//第三种json 跳过 if 里的逻辑,导致没有执行到 p.nextFieldName() ,于是在解析后续字段的时候拿到的 currentValue都会是 user 的实例,bug就这样产生了。报错也会报的莫名其妙。
return bean;
}