Jackson Json – Đệ quy vô tận trong mối quan hệ 2 chiều

Khi 2 Java class có mối quan hệ 2 chiều (trong A chứa B, trong B chứa A) sẽ dẫn đến các vấn đề khi làm việc với Jackson.

Trong bài viết này, chúng ta sẽ tập trung vào vấn đề đệ quy vô tận trong mối quan hệ quan hệ 2 chiều và tìm ra giải pháp để giải quyết vấn đề này.

Đệ quy vô tận

Trước tiên, chúng ta cần tạo ra mối quan hệ 2 chiều giữa 2 class UserItem.

public class User {
    public int id;
    public String name;
    public List<Item> userItems;

    public User(int id, String name) {
        this.id = id;
        this.name = name;
        userItems = new ArrayList<>();
    }

    public void addItem(Item item) {
        this.userItems.add(item);
    }
}
public class Item {
    public int id;
    public String itemName;
    public User owner;

    public Item(int id, String itemName, User owner) {
        this.id = id;
        this.itemName = itemName;
        this.owner = owner;
    }
}

Khi chúng ta serialize một Item object, Jackson sẽ ném ra JsonMappingException exception.

public class Main {
    public static void main(String[] agrs) throws JsonProcessingException {
        User user = new User(1, "John");
        Item item = new Item(2, "book", user);
        user.addItem(item);

        new ObjectMapper().writeValueAsString(item);
    }
}

Output

Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError) (through reference chain: User["userItems"]->java.util.ArrayList[0]->Item["owner"]->..

Ngoại lệ trên đã cho chúng ta thấy rõ vấn đề, Khi Jackson serialize Item object chứa thuộc tính owner là 1 User instance chứa một danh sách các Item trong đó có cả Item instance hiện hành mà chúng ta đang serializer. Kế đến khi Jackson serialize owner chứa Item instance nó sẽ quay lại serialize Item instance, cứ như vậy chúng ta sẽ có đệ quy vô tận,

Ở các phần sau, chúng ta sẽ đi tìm ra giải pháp để giải quyết vấn đề này.

Sử dụng @JsonManagedReference, @JsonBackReference

Cách giải quyết đầu tiên, chúng ta sẽ sử dụng @JsonManagerReference@JsonBackReference để chú thích cho UserItem class.

Sử dụng @JsonManagerReference cho User class

public class User {
    public int id;
    public String name;
    @JsonBackReference
    public List<Item> userItems;

    public User(int id, String name) {
        this.id = id;
        this.name = name;
        userItems = new ArrayList<>();
    }

    public void addItem(Item item) {
        this.userItems.add(item);
    }
}

@JsonBackReference cho Item class

public class Item {
    public int id;
    public String itemName;
    @JsonManagedReference
    public User owner;

    public Item(int id, String itemName, User owner) {
        this.id = id;
        this.itemName = itemName;
        this.owner = owner;
    }
}

Chạy lại đoạn code serializer trên

public class Main {
    public static void main(String[] agrs) throws JsonProcessingException {
        User user = new User(1, "John");
        Item item = new Item(2, "book", user);
        user.addItem(item);

        String result = new ObjectMapper().writeValueAsString(item);
        System.out.println(result);
    }
}

Output:

{"id":2,"itemName":"book","owner":{"id":1,"name":"John"}}

Note

  • @JsonManagedReference được xem như là thành phần chính trong mối quan hệ trong serialization, vì vậy chúng sẽ được serialized bình thường
  • @JsonBackReference được xem như phần phụ trợ, và nó sẽ bị lược bỏ trong serialization để tránh tình trạng lặp vô tận.

Nếu muốn serializer User thay vì Item, chúng ta có thể hoán đổi vị trí @JsonManagedReference cho User class và @JsonBackReference cho Item. 

public class User {
   // ... 
   @JsonManagedReference
   public List<Item> userItems;
   // ... 
}

// --------------------------------
public class Item {
    // ...
    @JsonBackReference
    public User owner;
    // .... 
}

Serialize User Object

public class Main {
    public static void main(String[] agrs) throws JsonProcessingException {
        User user = new User(1, "John");
        Item item = new Item(2, "book", user);
        user.addItem(item);

        String result = new ObjectMapper().writeValueAsString(user);
        System.out.println(result);
    }
}

Output: 

{
     "id":1,
     "name":"John",
     "userItems":[{"id":2,"itemName":"book"}]
}

Sử dụng @JsonIndentityInfor

Với việc sử dụng @JsonManagedReference @JsonBackReference có một nhược điểm cực kỳ lớn đó là chúng ta phải thay đổi vị trí đặt @JsonManagedReference để có thể serialize chứa đầy đủ thông tin.

@JsonIndentityInfor sinh ra nhầm giải quyết vấn đề này, nó cho phép serialize theo mã định danh khi chúng gặp lại nhau lần thứ 2 trong serialization từ đó giải pháp đệ quy vô tận.

Cho ví dụ khi User object đã được tuần tự hóa có ID là 1, nó sẽ kéo theo Item Object có ID là 2 được serialize. Ở lần quay lại sau, chúng sẽ kiếm tra nếu đã tồn tại User Object có ID là 1 thì, hoặc Item ID 2 thì quá trình serialization sẽ ngừng lại.

@JsonIdentityInfo(
        generator = ObjectIdGenerators.PropertyGenerator.class,
        property = "id")
public class User {
    public int id;
    public String name;
    public List<Item> userItems;

    public User(int id, String name) {
        this.id = id;
        this.name = name;
        userItems = new ArrayList<>();
    }

    public void addItem(Item item) {
        this.userItems.add(item);
    }
}
// ----------------------------------------------------------
@JsonIdentityInfo(
        generator = ObjectIdGenerators.PropertyGenerator.class,
        property = "id")
public class Item {
    public int id;
    public String itemName;
    public User owner;

    public Item(int id, String itemName, User owner) {
        this.id = id;
        this.itemName = itemName;
        this.owner = owner;
    }
}

Note:

  • generator – được sử dụng để tạo ra Object indentifier cho các object (Item, User)
  • property – chỉ ra thuộc tính dùng để xác định duy nhất một tham chiếu. Ví dụ User định danh với thuộc tính id, khi Item object tham chiếu đến User object này sẽ thông qua id thay vì một tham chiếu đến nó.

Cuối cùng, chúng ta sẽ tiến hành serialize cả UserItem object.

public class Main {
    public static void main(String[] agrs) throws JsonProcessingException {
        ObjectMapper mapper = new ObjectMapper();
        User user = new User(1, "John");
        Item item = new Item(2, "book", user);
        user.addItem(item);

        String jsonUser = mapper.writeValueAsString(user);
        System.out.println("User Json");
        System.out.println(jsonUser);

        String jsonItem = mapper.writeValueAsString(item);
        System.out.println("Item Json");
        System.out.println(jsonItem);
    }
}

Output: 

User Json
{   "id":1,
    "name":"John",
    "userItems":[{"id":2,"itemName":"book","owner":1}]
}
Item Json
{   "id":2,
    "itemName":"book",
    "owner":{"id":1,"name":"John","userItems":[2]}
}

Sử dụng @JsonIgnore

Ngoài ra, chúng ta có thể sử dụng @JsonIgnore để loại bỏ thuộc tính trong mối quan hệ 2 chiều, từ đó sẽ ngắt liên kết giữa chúng. Như vậy đệ quy vô tận sẽ được giải quyết nhanh chóng. Tuy nhiên giải pháp này chỉ sử dụng được khi Json output không yêu cầu thông tin của object refenrence còn lại.

Ví dụ ngắt link giữa User với Item khi serialize User object

public class User {
   // ...
    @JsonIgnore
    public List<Item> userItems;
    // ....
}
public class Main {
    public static void main(String[] agrs) throws JsonProcessingException {
        User user = new User(1, "John");
        Item item = new Item(2, "book", user);
        user.addItem(item);

        String result = new ObjectMapper().writeValueAsString(user);
        System.out.println(result);
    }
}

Output

{"id":1,"name":"John"}

@JsonView

Tương tự @JsonIgnore, @JsonView cho phép nhóm các thuộc tính trong serialization, từ đó chúng ta có thể loại bỏ thuộc tính gây nên đệ quy vô tận.

Ví dụ tạo 2 nhóm JSON VIEW – Public và Internal 

public class Views {
    public static class Public {}
 
    public static class Internal extends Public {}
}

Chúng ta sẽ họp tất cả các thuộc tính trong UserItem trong Public View ngoại trừ owneruserItems

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;
    // ...
}
public class Item {
    @JsonView(Views.Public.class)
    public int id;
 
    @JsonView(Views.Public.class)
    public String itemName;
 
    @JsonView(Views.Public.class)
    public User owner;
    // ..... 
}

Khi chúng ta serialize sử dụng Public View chúng sẽ loại trừ các thuộc tính 2 chiều về chúng nằm trong Internal View, từ đó tránh được đệ quy vô tận.

public class Main {
    public static void main(String[] agrs) 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(user);
        System.out.println(result);
    }
}

Output

{"id":1,"name":"John"}

Tuy nhiên, nếu serialize với Internal View, Java sẽ ném ra ngoại lệ JsonMappingException vì tất cả các field được serialized, bao gồm các field gây nên đệ quy vô tận.

public class Main {
    public static void main(String[] agrs) throws JsonProcessingException {
        User user = new User(1, "John");
        Item item = new Item(2, "book", user);
        user.addItem(item);

        String result = new ObjectMapper()
                .writerWithView(Views.Internal.class)
                .writeValueAsString(user);
        System.out.println(result);
    }
}

Output: Exception in thread “main” com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError)

Custom Serializer

Nếu không muốn sử dụng các annotation mà Jackson đã cung cấp sẵn, chúng ta có thể custom một serializer riêng cho mình để phù hợp với yêu cầu của dự án.

Trong ví dụ này, chúng ta sẽ custom một serializer cho userItem của User class với @JsonSerialize

public class User {
    @JsonSerialize(using = CustomListSerializer.class)
    public List<Item> userItems;
    // ... 
}
public class CustomListSerializer extends StdSerializer<List<Item>> {

    public CustomListSerializer() {
        this(null);
    }

    public CustomListSerializer(Class<List<Item>> 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);
    }
}

Serialize ItemUser object

public class Main {
    public static void main(String[] agrs) throws JsonProcessingException {
        User user = new User(1, "John");
        Item item = new Item(2, "book", user);
        user.addItem(item);

        String userJson = new ObjectMapper()
                .writeValueAsString(user);
        System.out.println(userJson);

        String itemJson = new ObjectMapper()
                .writeValueAsString(item);
        System.out.println(itemJson);
    }
}

Output:


{   "id":1,
    "name":"John",
    "userItems":[{"id":2,"itemName":"book","owner":1}]
}

{   "id":2,
    "itemName":"book",
    "owner":{"id":1,"name":"John","userItems":[2]}
}

Deserialize với@JsonIdentityInfo

@JsonIdentityInfor là một trong cách chúng ta đã dùng ở trên để giải quyết đệ quy vô tận, trong phần này chúng ta sẽ kiểm tra nó sẽ hoạt động thế nào khi chúng deserialize Json sang Object.

Note: Chúng ta phải thêm default constructor cho User và Item class, mặc định Jackson sẽ sử dụng default constructor để khởi tạo Object.

@JsonIdentityInfo(
        generator = ObjectIdGenerators.PropertyGenerator.class,
        property = "id")
public class User {
    public int id;
    public String name;
    public List<Item> userItems;

    public User() {
    }

    public User(int id, String name) {
        this.id = id;
        this.name = name;
        userItems = new ArrayList<>();
    }

    public void addItem(Item item) {
        this.userItems.add(item);
    }
}
public class Item {
    public int id;
    public String itemName;
    public User owner;

    public Item() {
    }

    public Item(int id, String itemName, User owner) {
        this.id = id;
        this.itemName = itemName;
        this.owner = owner;
    }
}

Kiểm thử

public class Main {
    public static void main(String[] agrs) throws IOException {
        String json =  "{\"id\":2," +
                "\"itemName\":\"book\"," +
                "\"owner\":{\"id\":1,\"name\":\"John\",\"userItems\":[2]}}";
        Item item = new ObjectMapper().readerFor(Item.class).readValue(json);
        User user = item.owner;
        System.out.println(item.id + " - " + item.itemName);
        System.out.println(user.id + " - " + user.name);
    }
}

Output: 

2 – book
1 – John

Custom Deserializer

Cuối cùng, chúng ta sẽ custom deserializer nếu không muốn sử dụng @JsonIndentityInfo mà Jackson cung cấp sẵn. Trong ví dụ này, chúng ta sẽ custom deserializer for userItems trong User class.

public class User {
    public int id;
    public String name;
    @JsonDeserialize(using = CustomListDeserializer.class)
    public List<Item> userItems;
    // ... 
}
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) {

        return new ArrayList<>();
    }
}

Kiểm thử

public class Main {
    public static void main(String[] args) throws IOException {
        String json =
                "{\"id\":2,\"itemName\":\"book\",\"owner\":{\"id\":1,\"name\":\"John\",\"userItems\":[2]}}";

        Item item = new ObjectMapper().readerFor(Item.class).readValue(json);
        System.out.println(item.id + " - " + item.itemName);

        User user = item.owner;
        System.out.println(user.id + " - " + user.name);

    }
}

Output:

2 – book
1 – John

Tóm lược

Các mối quan hệ 2 chiều thường xuyên xảy ra với các Java class đại diện cho một table trong cơ sở dữ liệu, khi serialize chúng sang Json thì việc xảy ra đệ quy vô tận. Hy vọng qua bài viết này sẽ giúp các bạn tự tin hơn khi gặp các vấn đề tương tự, cũng như chọn ra giải pháp phù hợp nhất với từng yêu cầu cụ thể của dự án.

Nguồn tham khảo

https://www.logicbig.com/tutorials/misc/jackson/json-identity-info-annotation.html

https://www.baeldung.com/jackson-bidirectional-relationships-and-infinite-recursion

4.5 2 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x