如果一个对象与另一个对象之间存在互相引用(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; } public class Card { private Long id; private Customer customer; } 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
| 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; } public class Card { private Long id; @JsonIgnore private Customer customer; }
|
该注解忽略card对象的customer属性,从而避免了无限递归的问题。需要说明的是JsonIgnore注解的设计初衷并不是用于解决该问题,但由于该方法简单粗暴易于理解,因此相当一部分程序员还是选择了该方法。下面说说我不推荐使用的理由:
- 使用了该注解后,如果你希望单独序列化一个card对象,那么json的接收端无法得知关于customer的任何信息
- 当服务端接受到客户端发送过来的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") private List<Card> cardList; } public class Card { private Long id; @JsonBackReference("customer-card") private Customer customer; }
|
@JsonManagedReference与@JsonBackReference生来就是为了解决Infinite Recursion问题。它与@JsonIgnore最大的不同在于反序列化时,标注JsonIgnore注解的属性始终为null,而使用这对注解的反序列化结果为两个互相引用的对象。然而这对注解同样不推荐使用,以下是原因:
- 相信你也看到了,它的序列化结果与使用@JsonIgnore一模一样,当你需要单独序列化card对象时,Json接收端无法知道该card属于哪个customer。
- 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; } @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id", scope = Card.class) public class Card { private Long id; private Customer customer; }
|
效果简直完美,当该注解检测到本次序列化中有重复对象时,使用指定的生成器生成一个替代对象(上例中的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] } } ] [ { "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; } public class Card { public interface CardWithCustomer{} @JsonView({Customer.CustomerWithCard.class, CardWithCustomer.class}) private Long id; @JsonView(CardWithCustomer.class) private Customer customer; } 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(); objectMapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false); 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
| { "id": 1, "cardList": [ { "id": 1 }, { "id": 2 } ] } [ { "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
以上每种方法都有各自的弊端。推荐使用者优先考虑后两种方案。如果我对以上方法的理解存在偏差,或者有人对于该问题有更好的解决方案,欢迎与我讨论。
参考链接: