如果一个对象与另一个对象之间存在互相引用(bi-directional relationship),序列化该对象就会出现stackoverflow异常,这篇文章主要介绍使用jackson序列化工具处理该问题的几种解决方案。

一.Infinite Recursion

首先不要被文章标题所误导,该问题并不是因为使用了jpa/hibernate的双向关联关系才会出现,简书上这篇文章说该异常根本原因在于hibernate的lazyload,我认为是一本正经的胡说八道,先来看一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Customer {
private Long id;
private List<Card> cardList;
//constructor getter setter...
}
public class Card {
private Long id;
private Customer customer;
//constructor getter setter...
}
//测试环境 junit, assertj
public class CustomerTest {
@Test
public void serialize() throws Exception {
Customer customer = new Customer();
Card card = new Card();
//相互引用
card.setCustomer(customer);
customer.addCard(card);
ObjectMapper objectMapper = new ObjectMapper();
Assertions.assertThatThrownBy(() -> objectMapper.writeValueAsString(customer))
.hasMessageStartingWith("Infinite recursion (StackOverflowError)");
}
}

上例中消费者与信用卡之间存在双向的一对多关系,序列化其中任意一个对象都会导致StackOverflowError,换句话说只要两个对象之间存在互相引用,就无法直接序列化,即便是在js中也不例外,看代码:

1
2
3
4
5
6
7
8
9
10
11
//测试环境 mocha,shouldjs
var should = require('should');
describe('json', function () {
it('如果两个对象互相引用,则无法直接序列化', function () {
var foo = {};
var bar = {};
foo.bar = bar;
bar.foo = foo;
should(function(){ JSON.stringify(foo);}).throw('Converting circular structure to JSON');
});
});

综上所述,这锅hibernate不接。问题介绍完毕,下面来看看当前热门的序列化工具jackson提供了哪些方法来处理该问题,以下讨论的方法截至jackson2.8.4版本。

二.@JsonIgnore

应用JsonIgnore注解后,代码看起来像这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Customer {
private Long id;
private List<Card> cardList;
//constructor getter setter...
}
public class Card {
private Long id;
@JsonIgnore
private Customer customer;
//constructor getter setter...
}
//serialize customer, result should like
// {
// "id": 1,
// "cardList": [
// {
// "id": 1
// }
// ]
// }

该注解忽略card对象的customer属性,从而避免了无限递归的问题。需要说明的是JsonIgnore注解的设计初衷并不是用于解决该问题,但由于该方法简单粗暴易于理解,因此相当一部分程序员还是选择了该方法。下面说说我不推荐使用的理由:

  1. 使用了该注解后,如果你希望单独序列化一个card对象,那么json的接收端无法得知关于customer的任何信息
  2. 当服务端接受到客户端发送过来的json字符串,并希望反序列化为card对象时,其customer属性永远为null

换言之,当你感觉某个属性需要使用JsonIgnore注解的时候,你应该先考虑是否可以直接删除该属性。拿上面的例子来说,也许你需要的只是在Card类中删除customer属性。JsonIgnore属性正确的应用场景应该是实体中类似于createAt,updateAt这样的字段,这些字段仅由服务端维护,且不需要发送至客户端,(注:password字段也不应该使用该注解,应该使用@JsonProperty(access = JsonProperty.Access.WRITE_ONLY),因为你可能希望接受客户端发送的password)

三.@JsonManagedReference与@JsonBackReference

使用该方法后,代码看起来像这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Customer {
private Long id;
@JsonManagedReference("customer-card") //当一个类中存在多个@JsonManagedReference注解时,其value值才是必须的
private List<Card> cardList;
//constructor getter setter...
}
public class Card {
private Long id;
@JsonBackReference("customer-card") //同上
private Customer customer;
//constructor getter setter...
}
//serialize customer, result should like
// {
// "id": 1,
// "cardList": [
// {
// "id": 1
// }
// ]
// }

@JsonManagedReference与@JsonBackReference生来就是为了解决Infinite Recursion问题。它与@JsonIgnore最大的不同在于反序列化时,标注JsonIgnore注解的属性始终为null,而使用这对注解的反序列化结果为两个互相引用的对象。然而这对注解同样不推荐使用,以下是原因:

  1. 相信你也看到了,它的序列化结果与使用@JsonIgnore一模一样,当你需要单独序列化card对象时,Json接收端无法知道该card属于哪个customer。
  2. JsonBackReference只能使用在一对多关系中"一"的一端,这意味着当你有Card与CardCategory两个类时,关系的维护端在CardCategory类,序列化信用卡类别可以得到该类别的所有信用卡,而序列化信用卡却无法得到该信用卡的类别,这显然不是你想要的。

综上所述,然并卵。

四.@JsonIdentityInfo

使用该方法后,代码看起来像这样

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
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id", scope = Customer.class)
public class Customer {
private Long id;
private List<Card> cardList;
//constructor getter setter...
}
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id", scope = Card.class)
public class Card {
private Long id;
private Customer customer;
//constructor getter setter...
}
//为了更好的演示该注解,我在customer对象中添加了两张card
//serialize customer, result should like
// {
// "id": 1,
// "cardList": [
// {
// "id": 1,
// "customer": 1
// },
// {
// "id": 2,
// "customer": 1
// }
// ]
// }

效果简直完美,当该注解检测到本次序列化中有重复对象时,使用指定的生成器生成一个替代对象(上例中的id),序列化card对象不会丢失customer信息,反序列化时能够完整的将对象恢复到序列化前的状态,顺便完美的规避的数据冗余,看上去非常美好。(注:要开始装B了!)但是通信原理告诉我们,数据格式的信息冗余度越低,则对其编解码需求的运算量越大,同时在信息传输过程中,越容易受到噪声干扰而失真(参见香农信息论与回答老鼠喝药问题的正确姿势)。(注:装B结束!)下面是应用了该注解后,序列化customer.cardList的结果:

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
// 理想
[
{
"id": 1,
"customer": {
"id": 1,
"cardList": [1, 2]
}
},
{
"id": 2,
"customer": {
"id": 1,
"cardList": [1, 2]
}
}
]
// 现实(WTF)
[
{
"id": 1,
"customer": {
"id": 1,
"cardList": [
1,
{
"id": 2,
"customer": 1
}
]
}
},
2
]

从上面惨不忍睹的现实中可以看出,jackson序列化对象时使用深度优先遍历(Depth-first-traversal),在card1.customer.cardList中写入了card2的信息,后面出现的card2则被替换成其id。也许你会说丑是丑了点,但信息量并没有减少,在json的接收端经过一些特殊处理依然可以将其反序列化至原来的状态。这也正如我前面所说的,该方法增加了编码与解码所需的运算量,这意味着你可能需要为该注解引入额外的libarary来进行序列化与反序列化(例如JSOG),考虑到json的接收端可能是各种平台(android,ios,web),因此该方案的实用性依然非常有限。

五.@JsonView

使用该方法后,代码看起来像这样

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
public class Customer {
public interface CustomerWithCard{}
@JsonView({CustomerWithCard.class, Card.CardWithCustomer.class})
private Long id;
@JsonView(CustomerWithCard.class)
private List<Card> cardList;
//constructor getter setter...
}
public class Card {
public interface CardWithCustomer{}
@JsonView({Customer.CustomerWithCard.class, CardWithCustomer.class})
private Long id;
@JsonView(CardWithCustomer.class)
private Customer customer;
//constructor getter setter...
}
public class CustomerTest {
@Test
public void serialize() throws Exception {
Customer customer1 = new Customer(1L);
Card card1 = new Card(1L, customer1);
Card card2 = new Card(2L, customer1);
customer1.addCard(card1);
customer1.addCard(card2);
List<Card> list = Arrays.asList(card1, card2);
ObjectMapper objectMapper = new ObjectMapper();
//序列化时不包括没有@JsonView注解的属性
objectMapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false);
//序列化需要先指定View
Assertions.assertThat(objectMapper.writerWithView(Customer.CustomerWithCard.class).writeValueAsString(customer))
.isNotEmpty();
Assertions.assertThat(objectMapper.writerWithView(Card.CardWithCustomer.class).writeValueAsString(list))
.isNotEmpty();
}
}

下面是序列化的结果

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
//serialize customer use CustomerWithCard view
{
"id": 1,
"cardList": [
{
"id": 1
},
{
"id": 2
}
]
}
//serialize card use CardWithCustomer view
[
{
"id": 1,
"customer": {
"id": 1
}
},
{
"id": 2,
"customer": {
"id": 1
}
}
]

这种方法的缺点是配置麻烦了一些,但总算勉强能用,下面顺带提一下如何在springmvc(4.3.4版本)中使用该方法,使用了该方法的controller看起来像这样:

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
@RestController
@RequestMapping(value = "/api", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public class CustomerController {
private CustomerService customerService;
public CustomerController(CustomerService customerService) {
this.customerService = customerService;
}
@GetMapping("/customer/{id}")
@JsonView(Customer.CustomerWithCard.class)
public Customer getCustomerById(@PathVariable Long id){
return customerService.findById(id);
}
@GetMapping("/customer/{customerId}/card")
@JsonView(Card.CardWithCustomer.class)
public List<Card> listCardByCustomerId(@PathVariable Long customerId){
return customerService.listCardByCustomerId(customerId);
}
@GetMapping("/customer/{customerId}/card")
@JsonView(Card.CardWithCustomer.class)
public Page<Card> listCardByCustomerIdWithPage(@PathVariable Long customerId, Pageable pageable){
return customerService.listCardByCustomerIdWithPage(customerId, pageable);
}
}

需要注意的是springmvc默认配置MapperFeature.DEFAULT_VIEW_INCLUSION为false,因此如果你在控制器方法上指定了要返回的JsonView,返回的实体类中所有没有该JsonView注解的属性都不会被序列化,这往往正是我们需要的效果,然而也正因为如此,上面代码中带物理分页的listCardByCustomerIdWithPage方法只会返回空对象,因为spring data的PageImpl类中的属性是没有JsonView注解的,我们还需要针对它做一些额外的配置,代码如下:

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
/**
* 用于解决使用json view,并且 MapperFeature.DEFAULT_VIEW_INCLUSION 为false的情况下
* spring data的page对象无法正确序列化的问题
* @return
*/
@Bean
public Module springDataPageModule() {
return new SimpleModule().addSerializer(Page.class, new JsonSerializer<Page>() {
@Override
public void serialize(Page page, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeStartObject();
gen.writeFieldName("size");
gen.writeNumber(page.getSize());
gen.writeFieldName("number");
gen.writeNumber(page.getNumber());
gen.writeFieldName("totalElements");
gen.writeNumber(page.getTotalElements());
gen.writeFieldName("last");
gen.writeBoolean(page.isLast());
gen.writeFieldName("totalPages");
gen.writeNumber(page.getTotalPages());
gen.writeObjectField("sort", page.getSort());
gen.writeFieldName("first");
gen.writeBoolean(page.isFirst());
gen.writeFieldName("numberOfElements");
gen.writeNumber(page.getNumberOfElements());
gen.writeFieldName("content");
serializers.defaultSerializeValue(page.getContent(),gen);
gen.writeEndObject();
}
});
}

六.Conclusion

以上每种方法都有各自的弊端。推荐使用者优先考虑后两种方案。如果我对以上方法的理解存在偏差,或者有人对于该问题有更好的解决方案,欢迎与我讨论。

参考链接: