1. 概述
本文我们将学习如何处理Jackson双向引用导致的无限递归问题。
2. 无限递归
让我们来看看Jackson的无限递归问题。在下面的例子中,我们有两个实体,“用户”(User)和“项目”(Item),它们之间有一个简单的“一对多”关系:
用户实体(User):
public class User {
public int id;
public String name;
public List<Item> userItems;
}
项目实体(Item):
public class Item {
public int id;
public String itemName;
public User owner;
}
当我们尝试序列化一个Item实例时,Jackson会抛出一个JsonMappingException
异常:
@Test(expected = JsonMappingException.class)
public void givenBidirectionRelation_whenSerializing_thenException()
throws JsonProcessingException {
User user = new User(1, "John");
Item item = new Item(2, "book", user);
user.addItem(item);
new ObjectMapper().writeValueAsString(item);
}
完整的异常如下:
com.fasterxml.jackson.databind.JsonMappingException:
Infinite recursion (StackOverflowError)
(through reference chain:
org.baeldung.jackson.bidirection.Item["owner"]
->org.baeldung.jackson.bidirection.User["userItems"]
->java.util.ArrayList[0]
->org.baeldung.jackson.bidirection.Item["owner"]
->…..
下面我们将学习如何解决这个问题。
3. 使用@JsonManagedReference 和 @JsonBackReference
首先,我们需要使用@JsonManagedReference
和@JsonBackReference
注解来让Jackson更好地处理这种关系:
用户实体(User):
public class User {
public int id;
public String name;
@JsonManagedReference
public List<Item> userItems;
}
项目实体(Item):
public class Item {
public int id;
public String itemName;
@JsonBackReference
public User owner;
}
现在,让我们测试新的实体:
@Test
public void givenBidirectionRelation_whenUsingJacksonReferenceAnnotationWithSerialization_thenCorrect() throws JsonProcessingException {
final User user = new User(1, "John");
final Item item = new Item(2, "book", user);
user.addItem(item);
final String itemJson = new ObjectMapper().writeValueAsString(item);
final String userJson = new ObjectMapper().writeValueAsString(user);
assertThat(itemJson, containsString("book"));
assertThat(itemJson, not(containsString("John")));
assertThat(userJson, containsString("John"));
assertThat(userJson, containsString("userItems"));
assertThat(userJson, containsString("book"));
}
Item对象序列化后输出如下:
{
"id":2,
"itemName":"book"
}
User序列化结果:
{
"id":1,
"name":"John",
"userItems":[{
"id":2,
"itemName":"book"}]
}
注意:
-
@JsonManagedReference
是引用的前半部分,正常进行序列化。 -
@JsonBackReference
是引用的后半部分,在序列化时会被省略。 - 序列化的“Item”对象不包含对“User”对象的引用。
此外,我们不能互换这两个注解的使用。对于序列化,以下操作是可行的:
@JsonBackReference
public List<Item> userItems;
@JsonManagedReference
public User owner;
但在反序列化时,它会抛出异常,因为@JsonBackReference
不能用于集合。
如果我们希望序列化的“Item”对象包含对“User”的引用,我们需要使用@JsonIdentityInfo
。我们将在下一节中介绍这一点。
4. 使用@JsonIdentityInfo
现在,让我们了解如何使用@JsonIdentityInfo
帮助序列化具有双向关系的实体。
我们在“User”实体上添加类级别的注解:
@JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
public class User { ... }
同样在“Item”实体上:
@JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
public class Item { ... }
现在进行测试:
@Test
public void givenBidirectionRelation_whenUsingJsonIdentityInfo_thenCorrect()
throws JsonProcessingException {
User user = new User(1, "John");
Item item = new Item(2, "book", user);
user.addItem(item);
String result = new ObjectMapper().writeValueAsString(item);
assertThat(result, containsString("book"));
assertThat(result, containsString("John"));
assertThat(result, containsString("userItems"));
}
序列化的输出如下:
{
"id":2,
"itemName":"book",
"owner":
{
"id":1,
"name":"John",
"userItems":[2]
}
}
5. 使用@JsonIgnore
另一种选择是使用@JsonIgnore
注解来简单地忽略关系的一方,从而打破循环。
在下面的例子中,我们通过忽略“用户”属性“userItems”的序列化来防止无限递归:
用户实体(User):
public class User {
public int id;
public String name;
@JsonIgnore
public List<Item> userItems;
}
测试如下:
@Test
public void givenBidirectionRelation_whenUsingJsonIgnore_thenCorrect()
throws JsonProcessingException {
User user = new User(1, "John");
Item item = new Item(2, "book", user);
user.addItem(item);
String result = new ObjectMapper().writeValueAsString(item);
assertThat(result, containsString("book"));
assertThat(result, containsString("John"));
assertThat(result, not(containsString("userItems")));
}
最终的序列化输出:
{
"id":2,
"itemName":"book",
"owner":
{
"id":1,
"name":"John"
}
}
6. 使用@JsonView
我们还可以使用较新的@JsonView
注解来排除关系的一方。
在以下示例中,我们将使用两个JSON视图,“公共”(Public)和“内部”(Internal),其中Internal继承自Public:
public class Views {
public static class Public {}
public static class Internal extends Public {}
}
我们在Public视图中包含所有“用户”和“项目”字段,但排除“用户”字段“userItems”,它将包含在Internal视图中:
用户实体(User):
public class User {
@JsonView(Views.Public.class)
public int id;
@JsonView(Views.Public.class)
public String name;
@JsonView(Views.Internal.class)
public List<Item> userItems;
}
项目实体(Item):
public class Item {
@JsonView(Views.Public.class)
public int id;
@JsonView(Views.Public.class)
public String itemName;
@JsonView(Views.Public.class)
public User owner;
}
当使用Public视图进行序列化时,由于我们排除了“userItems”,所以它能正确工作:
@Test
public void givenBidirectionRelation_whenUsingPublicJsonView_thenCorrect()
throws JsonProcessingException {
User user = new User(1, "John");
Item item = new Item(2, "book", user);
user.addItem(item);
String result = new ObjectMapper().writerWithView(Views.Public.class)
.writeValueAsString(item);
assertThat(result, containsString("book"));
assertThat(result, containsString("John"));
assertThat(result, not(containsString("userItems")));
}
但如果使用Internal视图,会抛出JsonMappingException
,因为所有字段都被包含:
@Test(expected = JsonMappingException.class)
public void givenBidirectionRelation_whenUsingInternalJsonView_thenException()
throws JsonProcessingException {
User user = new User(1, "John");
Item item = new Item(2, "book", user);
user.addItem(item);
new ObjectMapper()
.writerWithView(Views.Internal.class)
.writeValueAsString(item);
}
7. 使用自定义序列化器
接下来,我们将学习如何使用自定义序列化器来序列化具有双向关系的实体。
在以下示例中,我们将使用自定义序列化器来序列化“用户”属性“userItems”:
用户实体(User):
public class User {
public int id;
public String name;
@JsonSerialize(using = CustomListSerializer.class)
public List<Item> userItems;
}
以及自定义序列化器“CustomListSerializer”:
public class CustomListSerializer extends StdSerializer<List<Item>>{
public CustomListSerializer() {
this(null);
}
public CustomListSerializer(Class<List> t) {
super(t);
}
@Override
public void serialize(
List<Item> items,
JsonGenerator generator,
SerializerProvider provider)
throws IOException, JsonProcessingException {
List<Integer> ids = new ArrayList<>();
for (Item item : items) {
ids.add(item.id);
}
generator.writeObject(ids);
}
}
现在进行测试。如您所见,正在产生正确的输出:
@Test
public void givenBidirectionRelation_whenUsingCustomSerializer_thenCorrect()
throws JsonProcessingException {
User user = new User(1, "John");
Item item = new Item(2, "book", user);
user.addItem(item);
String result = new ObjectMapper().writeValueAsString(item);
assertThat(result, containsString("book"));
assertThat(result, containsString("John"));
assertThat(result, containsString("userItems"));
}
最终的序列化输出使用自定义序列化器:
{
"id":2,
"itemName":"book",
"owner":
{
"id":1,
"name":"John",
"userItems":[2]
}
}
8. 使用@JsonIdentityInfo反序列化
现在让我们看看如何使用@JsonIdentityInfo
反序列化具有双向关系的实体。
用户实体(User):
@JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
public class User { ... }
项目实体(Item):
@JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
public class Item { ... }
我们写一个快速测试,从一些手动JSON数据开始,最终构建正确的实体:
@Test
public void givenBidirectionRelation_whenDeserializingWithIdentity_thenCorrect()
throws JsonProcessingException, IOException {
String json =
"{\"id\":2,\"itemName\":\"book\",\"owner\":{\"id\":1,\"name\":\"John\",\"userItems\":[2]}}";
ItemWithIdentity item
= new ObjectMapper().readerFor(ItemWithIdentity.class).readValue(json);
assertEquals(2, item.id);
assertEquals("book", item.itemName);
assertEquals("John", item.owner.name);
}
9. 使用自定义反序列化器
最后,我们将使用自定义反序列化器来反序列化具有双向关系的实体。
在以下示例中,我们将使用自定义反序列化器来解析“用户”属性“userItems”:
用户实体(User):
public class User {
public int id;
public String name;
@JsonDeserialize(using = CustomListDeserializer.class)
public List<Item> userItems;
}
以及我们的自定义反序列化器“CustomListDeserializer”:
public class CustomListDeserializer extends StdDeserializer<List<Item>>{
public CustomListDeserializer() {
this(null);
}
public CustomListDeserializer(Class<?> vc) {
super(vc);
}
@Override
public List<Item> deserialize(
JsonParser jsonparser,
DeserializationContext context)
throws IOException, JsonProcessingException {
return new ArrayList<>();
}
}
最后,这是简单的测试:
@Test
public void givenBidirectionRelation_whenUsingCustomDeserializer_thenCorrect()
throws JsonProcessingException, IOException {
String json =
"{\"id\":2,\"itemName\":\"book\",\"owner\":{\"id\":1,\"name\":\"John\",\"userItems\":[2]}}";
Item item = new ObjectMapper().readerFor(Item.class).readValue(json);
assertEquals(2, item.id);
assertEquals("book", item.itemName);
assertEquals("John", item.owner.name);
}