关于java:SpringMVC在循环引用时出现bug的问题探讨

48次阅读

共计 3312 个字符,预计需要花费 9 分钟才能阅读完成。

筹备两个存在循环援用的对象:
@Data
public class Person {

private String name;
private IdCard idCard;

}

@Data
public class IdCard {

private String id;
private Person person;

}
在 SpringMVC 的 controller 中间接返回存在循环援用的对象:
@RestController
public class HelloController {

@RequestMapping("/hello")
public Person hello() {Person person = new Person();
    person.setName("kirito");

    IdCard idCard = new IdCard();
    idCard.setId("xxx19950102xxx");

    person.setIdCard(idCard);
    idCard.setPerson(person);

    return person;
}

}
执行 curl localhost:8080/hello 发现,间接报了一个 StackOverFlowError:

问题分析
不难理解这两头产生了什么,从堆栈和常识中都该当理解到一个事实,SpringMVC 默认应用了 jackson 作为 HttpMessageConverter,这样当咱们返回对象时,会通过 jackson 的 serializer 序列化成 json 串,java 培训而另一个事实便是 jackson 是无奈解析 java 中的循环援用的,套娃式的解析,最终导致了 StackOverFlowError。
有人会说,为什么你会有循环援用呢?天知道业务场景有多奇葩,既然 Java 没有限度循环援用的存在,那就必定会有某一正当的场景存在该可能性,如果你在线上的一个接口始终安稳运行着,晓得有一天,碰到了一个蕴含循环援用的对象,你看着打印进去的 StackOverFlowError 的堆栈,开始狐疑人生,是哪个小(大)可(S)爱(B)干的这种事!
咱们先假如循环援用存在的合理性,如何解决该问题呢?最简略的解法:单向保护关联,参考 Hibernate 中的 OneToMany 关联中单向映射的思维,这须要干掉 IdCard 中的 Person 成员变量。或者,借助于 jackson 提供的注解,指定疏忽循环援用的字段,例如这样:
@Data
public class IdCard {

private String id;
@JsonIgnore
private Person person;

}
当然,我也翻阅了一些材料,尝试寻求 jackson 更优雅的解决形式,例如这两个注解:
@JsonManagedReference
@JsonBackReference
但在我看来,仿佛他们并没有什么大用场。
当然,你如果不厌弃常常出安全漏洞的 fastjson,也能够抉择应用 FastJsonHttpMessageConverter 替换掉 jackson 的默认实现,像上面这样:
@Bean
public HttpMessageConverters fastJsonHttpMessageConverters() {

//1、定义一个 convert 转换音讯的对象
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();

//2、增加 fastjson 的配置信息
FastJsonConfig fastJsonConfig = new FastJsonConfig();

SerializerFeature[] serializerFeatures = new SerializerFeature[]{
    //    输入 key 是蕴含双引号
    //                SerializerFeature.QuoteFieldNames,
    //    是否输入为 null 的字段, 若为 null 则显示该字段
    //                SerializerFeature.WriteMapNullValue,
    //    数值字段如果为 null,则输入为 0
    SerializerFeature.WriteNullNumberAsZero,
    //     List 字段如果为 null, 输入为[], 而非 null
    SerializerFeature.WriteNullListAsEmpty,
    //    字符类型字段如果为 null, 输入为 "", 而非 null
    SerializerFeature.WriteNullStringAsEmpty,
    //    Boolean 字段如果为 null, 输入为 false, 而非 null
    SerializerFeature.WriteNullBooleanAsFalse,
    //    Date 的日期转换器
    SerializerFeature.WriteDateUseDateFormat,
    //    循环援用
    //SerializerFeature.DisableCircularReferenceDetect,
};

fastJsonConfig.setSerializerFeatures(serializerFeatures);
fastJsonConfig.setCharset(Charset.forName("UTF-8"));

//3、在 convert 中增加配置信息
fastConverter.setFastJsonConfig(fastJsonConfig);

//4、将 convert 增加到 converters 中
HttpMessageConverter<?> converter = fastConverter;

return new HttpMessageConverters(converter);

}
你能够自定义一些 json 转换时的 feature,当然我明天次要关注 SerializerFeature.DisableCircularReferenceDetect 这一属性,只有不显示开启该个性,fastjson 默认就能解决循环援用的问题。
如上配置后,让咱们看看成果:
{“idCard”:{“id”:”xxx19950102xxx”,”person”:{“$ref”:”..”}},”name”:”kirito”}
曾经失常返回了,fastjson 应用了 ”$ref”:”..” 这样的标识,解决了循环援用的问题,如果持续应用 fastjson 反序列化,仍旧能够解析成同一对象,其实我在之前的文章中曾经介绍过这一个性了《gson 替换 fastjson 引发的线上问题剖析》。
应用 FastJsonHttpMessageConverter 能够彻底躲避掉循环援用的问题,这对于返回类型不固定的场景非常有帮忙,而 @JsonIgnore 只能作用于那些固定构造的循环援用对象上。
问题思考
值得一提的是,为什么个别规范的 JSON 类库并没有如此关注循环援用的问题呢?fastjson 看起来反而是个特例,我感觉次要还是 JSON 这种序列化的格局就是为了通用而存在的,$ref 这样的契约信息,并没有被 JSON 的标准去定义,fastjson 能够确保 $ref 在序列化、反序列化时可能失常解析,但如果是跨框架、跨零碎、跨语言等场景,这一切都是个未知数了。说到底,这还是 Java 语言的循环援用和 JSON 通用标准不蕴含这一概念之间的 gap(可能 JSON 标准形容了这一个性,但我没有找到,如有问题,烦请斧正)。
我到底应该抉择 @JsonIgnore 还是应用 FastJsonHttpMessageConverter 呢?经验了下面的思考,我感觉各位看官应该可能依据本人的场景抉择适合的计划了。
总结下,如果抉择 FastJsonHttpMessageConverter,改变较大,如果有较多的存量接口,倡议做好回归,以确认解决循环援用问题的同时,别引入了其余不兼容的改变。并且,须要基于你的应用场景评估计划,如果呈现了循环援用,fastjson 会应用 $ref 来记录援用信息,请确认你的前端或者接口方可能辨认该信息,因为这可能并不是规范的 JSON 标准。你也能够抉择 @JsonIgnore 来实现最小改变,但也同时须要留神,如果依据序列化的后果再次反序列化,援用信息可不会主动复原。

正文完
 0