跳到主要內容

初探 Jackson JSON 的無限遞迴問題

情境


在 JPA 中處理 多對多 (Many-to-Many) 關係, 不使用 @ManyToMany 註解方式, 而是將這個關係拆解為兩個一對多(One-to-Many)的單向關係,並為中間表創建一個獨立的Entity.

代碼如下:


@Entity

@Getter

@Setter

@NoArgsConstructor

@AllArgsConstructor

@Builder

@Table(name = "users")

public class User {


    @Id

    @GeneratedValue(strategy = GenerationType.IDENTITY)

    private Long id;


    @Column(name = "username", nullable = false)

    private String username;


    @Column(name = "password", nullable = false)

    private String password;


    @Column(name = "first_name", nullable = false)

    private String firstName;


    @Column(name = "last_name")

    private String lastName;


    @Column(name = "email", nullable = false, unique = true)

    private String email;


    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)

    private Set<UserRole> roles = new HashSet<>();


    public User(String username, String password, String firstName, String lastName, String email) {

        this.username = username;

        this.password = password;

        this.firstName = firstName;

        this.lastName = lastName;

        this.email = email;

    }


}



@Entity

@Getter

@Setter

@NoArgsConstructor

@AllArgsConstructor

@Builder

@Table(name = "roles")

public class Role {


    @Id

    @GeneratedValue(strategy = GenerationType.IDENTITY)

    private Long id;


    @Column(length = 20)

    private String name;


    public Role(String name) {

        this.name = name;

    }


    @OneToMany(mappedBy = "role", cascade = CascadeType.ALL, orphanRemoval = true)

    private Set<UserRole> userRoles = new HashSet<>();


}



@Data

@Embeddable

public class UserRoleId implements Serializable {


    // 與 UserRole.java 中 @MapsId 的名稱一致

    @Column(name = "user_id")

    private Long userId;


    @Column(name = "role_id")

    private Long roleId;


    public UserRoleId() {

    }


    public UserRoleId(Long userId, Long roleId) {

        this.userId = userId;

        this.roleId = roleId;

    }


    @Override

    public boolean equals(Object o) {

        if (this == o)

            return true;

        if (o == null || getClass() != o.getClass())

            return false;

        UserRoleId that = (UserRoleId) o;

        return Objects.equals(userId, that.userId) && Objects.equals(roleId, that.roleId);

    }


    @Override

    public int hashCode() {

        return Objects.hash(userId, roleId);

    }

}



@Entity

@Data

@NoArgsConstructor

@AllArgsConstructor

@Builder

@Table(name = "users_roles")

public class UserRole implements Serializable {


    // 複合主鍵的 ID

    @EmbeddedId

    private UserRoleId id;


    // ManyToOne 關係到 User,  userId 對映到 UserRoleId 中的 userId

    @ManyToOne(fetch = FetchType.LAZY)

    @MapsId("userId")

    @JoinColumn(name = "user_id")

    private User user;


    // ManyToOne 關係到 Role

    @ManyToOne(fetch = FetchType.LAZY)

    @MapsId("roleId")

    @JoinColumn(name = "role_id")

    private Role role;


    @Column(name = "assigned_at")

    private LocalDateTime assignedAt;


}



當我們 序列化 User 實例時,Jackson會拋出JsonMappingException異常


顯示 Exception 如下:















這個錯誤發生在 Jackson 嘗試將您的 JPA 實體 User 序列化為 JSON 字串時,Jackson 序列化器仍然發現了一個循環


無限遞歸序列化 (Infinite Recursion) 錯誤原因:


循環序列化的多對多結構 User <-> UserRole <-> Role


  1. Jackson 序列化 User
  2. 在序列化 User 的屬性時,遇到 roles 集合 (Set<UserRole>)。
  3. 序列化 UserRole 時,遇到 User 實體 (@ManyToOne private User user;)。
  4. Jackson 再次嘗試序列化這個 User 物件,回到步驟 1,形成無限循環


‘’’

  1. Jackson 序列化 User
  2. 在序列化 User 的屬性時,遇到 roles 集合 (Set<UserRole>)。
  3. 序列化 UserRole 實體。
  4. 在序列化 UserRole 時,遇到 User 實體 (@ManyToOne private User user;)。
  5. Jackson 再次嘗試序列化這個 User 物件,回到步驟 1,形成無限循環

‘’’


  註: Jackson 預設的最大遞歸深度是 1000 層,當達到這個限制時,它會拋出這個錯誤以避免堆棧溢出(StackOverflowError)。




任務

針對 User 序列化為 JSON 字串時,Jackson JSON 的無限遞迴問題,探討處理雙向關係的方法



處理動作


步驟一. 首先建立一個測試案例測試:


@Transactional // 確保每個測試都回滾

@SpringBootTest

public class UserRoleRelationshipTest {


    @Test

    void testReadUserRoleRelationship() {

        try {


            List<User> users = userRepository.findAll(); // 獲取所有用戶

    System.out.println("****** 獲取所有用戶: ******");


            ObjectMapper objectMapper = new ObjectMapper();

            objectMapper.registerModule(new JavaTimeModule());

            String jsonArray = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(users); 


            System.out.println(jsonArray);


        } catch (JsonProcessingException e) {

            e.printStackTrace();

        }

    }



步驟二. 預備測試資料,已存DB

測試用

Table users




Table roles




Table users_roles











步驟三. 實作方案


使用@JsonIgnore

不想序列化某個屬性,使用 @JsonIgnore 註解來忽略關係中的某個屬性


選項1

public class User {

. . .


    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)

    @JsonIgnore

    private Set<UserRole> roles = new HashSet<>();


. . .

}


測試結果

% mvn test

















選項2

public class UserRole implements Serializable {

. . .


    @ManyToOne(fetch = FetchType.LAZY)

    @MapsId("userId") // 映射到 UserRoleId 中的 userId 屬性

    @JoinColumn(name = "user_id")

    @JsonIgnore

    private User user;


    @ManyToOne(fetch = FetchType.LAZY)

    @MapsId("roleId") // 映射到 UserRoleId 中的 roleId 屬性

    @JoinColumn(name = "role_id")

    @JsonIgnore

    private Role role;


. . .

}


測試結果

% mvn test


















使用@JsonManagedReferences@JsonBackReferences

@JsonManagedReferences 用於引用父 Object ,@JsonBackReferences 用於標記子 Object。



    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)

    @JsonManagedReference

    private Set<UserRole> roles = new HashSet<>();



    @OneToMany(mappedBy = "role", cascade = CascadeType.ALL, orphanRemoval = true)

    @JsonManagedReference

    private Set<UserRole> userRoles = new HashSet<>();



    @ManyToOne(fetch = FetchType.LAZY)

    @MapsId("userId") // 映射到 UserRoleId 中的 userId 屬性

    @JoinColumn(name = "user_id")

    @JsonBackReference

    private User user;


    @ManyToOne(fetch = FetchType.LAZY)

    @MapsId("roleId") // 映射到 UserRoleId 中的 roleId 屬性

    @JoinColumn(name = "role_id")

    @JsonBackReference

    private Role role;


執行結果:




留言

這個網誌中的熱門文章

初探 Vue 呼叫 API 出現 CORS 跨來源資源共享 問題原因

提要:   在 {初探Vue 與 Spring boot 的對話} 專案 ,前端 Vue 應用程式 串接 後端 API 伺服器 ,axios 呼叫 API 時出現以下,”無法取得回應內容 (No 'Access-Control-Allow-Origin' header is present on the requested resource):” 錯誤訊息,根據查找相關資料 ,出現以下原因。 瀏覽器開發工具 錯誤訊息 畫面 錯誤原因: “ Access to XMLHttpRequest at ” from origin ‘http://localhost:8080’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource” 瀏覽器為了安全考量,實施了同源政策。 當您的前端應用程式 (http://localhost:8080) 嘗試呼叫一個不同來源 (不同協議、不同域名或不同埠號) 的 API 伺服器 (http://localhost:8088) 時,瀏覽器會主動阻止這個請求,除非伺服器明確地允許這個跨來源的存取。 同源政策限制(Same-Origin Policy): 同源政策限制了程式碼和不同網域資源間的互動,同源是指兩份網頁具備相同協定、埠號(如果有指定)以及主機位置 範例: 表列哪些 URL 與 URL http://www.example.com/api/p1 屬於同源: URL                                                   | 結果   | 原因 --------------------------------------------------------------------- http://www.example.com/api/p2     |...

初探 Vue 與 Spring boot 的對話之Frontend (Vue-Frontend)

  Front-end Vue 使用 REST API 建立 Vite 專案 可參考 { Vue 3 初探}  文章 danny@Danny-Yu projects % npm create vite@latest Need to install the following packages: create-vite@8.2.0 Ok to proceed? (y) y > npx > "create-vite" │ ◇   Project name: │   vue-frontend │ ◇   Select a framework: │   Vue │ ◇   Select a variant: │   TypeScript │ ◇   Use rolldown-vite (Experimental)?: │   No │ ◇   Install with npm and start now? │   Yes │ ◇   Scaffolding project in /Users/danny/Desktop/projects/vue-frontend... │ ◇   Installing dependencies with npm... added 47 packages, and audited 48 packages in 27s 6 packages are looking for funding   run `npm fund` for details found 0 vulnerabilities │ ◇   Starting dev server... > vue-frontend@0.0.0 dev > vite   VITE v7.2.4   ready in 411 ms   ➜   Local:   http://localhost:5173/   ➜   Network: use --host to expose   ➜   press h + enter to show...

初探 Spring 中的循環依賴

原因: 當兩個或多個 bean 直接或間接地相互依賴時, 就會出現 Circular Dependency (循環依賴) 如: Bean A -> Bean B -> Bean A import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class BeanA {          @Autowired     private BeanB beanB;     public String sayHi() {         return "Hi! 我是 Class A.";     } } import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class BeanB {          @Autowired     private BeanA beanA;     public String sayHi() {         return "Hi! 我是 Class B.";     } } 編譯時不會出現問題 danny@Danny-Yu demo % mvn clean install -Dmaven.test.skip=true                           ... ... [INFO] Installing /Users/danny/Desktop/projects/demo/target/dem...