Problem Discovery

Today this topic is still relatively easy, and many of you may have encountered this problem.

The @RestController, @ResponseBody and other annotations are the ones we deal with most when writing web applications, and we often have the need to return an object to the client, which SpringMVC helps us serialize into JSON objects. And today I want to share the topic is not something profound, it is the return of an object when there is a circular reference to explore the problem.

The problem is very simple and easy to reproduce, so let’s go straight to the code.

Prepare two objects with circular references.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12

@Data
public class Person {
    private String name;
    private IdCard idCard;
}

@Data
public class IdCard {
    private String id;
    private Person person;
}

Return objects with circular references directly in the SpringMVC controller.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@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;
    }
}

Requesting curl localhost:8080/hello was found to throw a StackOverFlowError directly.

StackOverFlowError

Problem Analysis

It is not difficult to understand what happened in between, from the stack and common sense should understand the fact that SpringMVC uses jackson as HttpMessageConverter by default, so that when we return the object, it will be serialized into json string by jackson’s serializer, and another fact is that jackson is unable to resolve circular references in java, nesting type of parsing, which eventually led to StackOverFlowError.

Some people may say, “Why do you have circular references? God knows how odd the business scenario, since Java does not limit the existence of circular references, there must be a reasonable scenario for the existence of the possibility, if you have an interface on the line has been running smoothly until one day, encountered an object containing circular references, you look at the StackOverFlowError printed out of the stack, and begin to doubt life, is which What kind of idiot did this!

We first assume the existence of circular references to the reasonableness of how to solve the problem? The simplest solution: maintain the association in one direction, referring to the idea of one-way mapping in the OneToMany association in Hibernate, which requires killing the Person member variable in IdCard. Or, with the help of the annotations provided by jackson, specify the fields that ignore circular references, e.g., like this

1
2
3
4
5
6
@Data
public class IdCard {
    private String id;
    @JsonIgnore
    private Person person;
}

Of course, I also looked through some sources to try to find a more elegant solution to jackson, such as these two annotations.

1
2
@JsonManagedReference
@JsonBackReference

But in my opinion, they don’t seem to be of much use.

Of course, you can also choose to use FastJsonHttpMessageConverter to replace the default implementation of jackson if you don’t mind the frequent security vulnerabilities of fastjson, like the following.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@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);
}

You can customize some features for json conversion, but today I’m mainly concerned with the SerializerFeature.DisableCircularReferenceDetect property, which allows fastjson to handle circular references by default, as long as the feature is not shown to be turned on.

After configuring as above, let’s see the result.

1
{"idCard":{"id":"xxx19950102xxx","person":{"$ref":".."}},"name":"kirito"}

has been returned normally, fastjson uses "$ref":"..." This identifier solves the circular reference problem, and if you continue to use fastjson deserialization, you can still resolve to the same object.

Using FastJsonHttpMessageConverter completely circumvents the circular reference problem, which is very helpful in scenarios where the return type is not fixed, whereas @JsonIgnore only works on objects with fixed structure of circular references.

Questions to ponder

It’s worth mentioning why the standard JSON library doesn’t focus so much on circular references. fastjson seems to be a special case, but I think the main reason is that the JSON serialization format is meant to be universal, and contract information like $ref is not defined by the JSON specification. fastjson can ensure that $ref can be parsed properly when serializing and deserializing. ref` can be resolved properly when serializing and deserializing, but if it is a cross-framework, cross-system, cross-language scenario, this is all an unknown. In the end, it’s a gap between the Java language’s circular references and the JSON general specification’s lack of this concept (maybe the JSON specification describes this feature, but I haven’t found it, so please correct me if there’s a problem).

Should I go with @JsonIgnore or use FastJsonHttpMessageConverter? After thinking about the above, I think you should be able to choose the right solution according to your own scenario.

To summarize, if you choose FastJsonHttpMessageConverter, the changes are larger, and if there are more stock interfaces, it is recommended to do a good regression to make sure that the circular reference problem is solved while not introducing other incompatible changes. And, you need to evaluate the solution based on your usage scenario, if there is a circular reference, fastjson will use $ref to record the reference information, please make sure your front-end or interface side can recognize the information, because this may not be the standard JSON specification. You can also choose @JsonIgnore for minimal changes, but be aware that if you deserialize again based on the serialization result, the reference information will not be automatically restored.

Reference https://mp.weixin.qq.com/s/CnyGY4PSkJKZ0hRTAH_saQ