簡體   English   中英

如何解決hibernate雙向映射導致的json序列化器中的循環引用?

[英]How to solve circular reference in json serializer caused by hibernate bidirectional mapping?

我正在編寫一個序列化程序來將 POJO 序列化為 JSON,但遇到了循環引用問題。 在休眠雙向一對多關系中,父引用子引用和子引用回到父引用,這里我的序列化器死了。 (見下面的示例代碼)
如何打破這個循環? 我們可以獲取對象的所有者樹以查看對象本身是否存在於其自己的所有者層次結構中的某處嗎? 任何其他方法來查找引用是否將是循環的? 或任何其他想法來解決這個問題?

我依靠Google JSON通過使用該功能來處理此類問題

從序列化和反序列化中排除字段

假設A和B類之間的雙向關系如下

public class A implements Serializable {

    private B b;

}

和乙

public class B implements Serializable {

    private A a;

}

現在使用 GsonBuilder 來獲取自定義的 Gson 對象如下(注意setExclusionStrategies方法)

Gson gson = new GsonBuilder()
    .setExclusionStrategies(new ExclusionStrategy() {

        public boolean shouldSkipClass(Class<?> clazz) {
            return (clazz == B.class);
        }

        /**
          * Custom field exclusion goes here
          */
        public boolean shouldSkipField(FieldAttributes f) {
            return false;
        }

     })
    /**
      * Use serializeNulls method if you want To serialize null values 
      * By default, Gson does not serialize null values
      */
    .serializeNulls()
    .create();

現在我們的循環引用

A a = new A();
B b = new B();

a.setB(b);
b.setA(a);

String json = gson.toJson(a);
System.out.println(json);

看看GsonBuilder

Jackson 1.6(2010 年 9 月發布)對處理此類父/子鏈接具有特定的基於注釋的支持,請參閱http://wiki.fasterxml.com/JacksonFeatureBiDirReferences 回溯快照

您當然可以排除已經使用大多數 JSON 處理包(jackson、gson 和 flex-json 至少支持它)的父鏈接的序列化,但真正的技巧是如何反序列化它(重新創建父鏈接),而不是只是處理序列化方面。 雖然現在聽起來只是排除可能對你有用。

編輯(2012 年 4 月): Jackson 2.0現在支持真實身份引用Wayback Snapshot ),因此您也可以通過這種方式解決。

甚至可以用 JSON 表示雙向關系嗎? 某些數據格式不適合某些類型的數據建模。

在處理遍歷對象圖時處理循環的一種方法是跟蹤您到目前為止看到的對象(使用身份比較),以防止自己遍歷無限循環。

在解決這個問題時,我采用了以下方法(在我的應用程序中標准化流程,使代碼清晰可重用):

  1. 創建一個注釋類以用於您想要排除的字段
  2. 定義一個實現 Google 的 ExclusionStrategy 接口的類
  3. 創建使用GsonBuilder生成GSON對象的簡單方法(類似於Arthur的解釋)
  4. 根據需要注釋要排除的字段
  5. 將序列化規則應用於您的 com.google.gson.Gson 對象
  6. 序列化你的對象

這是代碼:

1)

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface GsonExclude {

}

2)

import com.google.gson.ExclusionStrategy;
import com.google.gson.FieldAttributes;

public class GsonExclusionStrategy implements ExclusionStrategy{

    private final Class<?> typeToExclude;

    public GsonExclusionStrategy(Class<?> clazz){
        this.typeToExclude = clazz;
    }

    @Override
    public boolean shouldSkipClass(Class<?> clazz) {
        return ( this.typeToExclude != null && this.typeToExclude == clazz )
                    || clazz.getAnnotation(GsonExclude.class) != null;
    }

    @Override
    public boolean shouldSkipField(FieldAttributes f) {
        return f.getAnnotation(GsonExclude.class) != null;
    }

}

3)

static Gson createGsonFromBuilder( ExclusionStrategy exs ){
    GsonBuilder gsonbuilder = new GsonBuilder();
    gsonbuilder.setExclusionStrategies(exs);
    return gsonbuilder.serializeNulls().create();
}

4)

public class MyObjectToBeSerialized implements Serializable{

    private static final long serialVersionID = 123L;

    Integer serializeThis;
    String serializeThisToo;
    Date optionalSerialize;

    @GsonExclude
    @ManyToOne(fetch=FetchType.LAZY, optional=false)
    @JoinColumn(name="refobj_id", insertable=false, updatable=false, nullable=false)
    private MyObjectThatGetsCircular dontSerializeMe;

    ...GETTERS AND SETTERS...
}

5)

在第一種情況下,向構造函數提供 null,您可以指定要排除的另一個類 - 下面添加了兩個選項

Gson gsonObj = createGsonFromBuilder( new GsonExclusionStrategy(null) );
Gson _gsonObj = createGsonFromBuilder( new GsonExclusionStrategy(Date.class) );

6)

MyObjectToBeSerialized _myobject = someMethodThatGetsMyObject();
String jsonRepresentation = gsonObj.toJson(_myobject);

或者,排除 Date 對象

String jsonRepresentation = _gsonObj.toJson(_myobject);

如果您使用 Jackon 進行序列化,只需將@JsonBackReference應用於您的雙向映射即可解決循環引用問題。

注意:@JsonBackReference 用於解決無限遞歸(StackOverflowError)

使用了類似於 Arthur 的解決方案,但我使用的不是setExclusionStrategies

Gson gson = new GsonBuilder()
                .excludeFieldsWithoutExposeAnnotation()
                .create();

並為我在 json 中需要的字段使用了@Expose gson 注釋,其他字段被排除在外。

如果您使用的是 Spring Boot,Jackson 在從循環/雙向數據創建響應時會拋出錯誤,因此請使用

@JsonIgnoreProperties

忽略循環

At Parent:
@OneToMany(mappedBy="dbApp")
@JsonIgnoreProperties("dbApp")
private Set<DBQuery> queries;

At child:
@ManyToOne
@JoinColumn(name = "db_app_id")
@JsonIgnoreProperties("queries")
private DBApp dbApp;

如果您使用的是 Javascript,那么使用JSON.stringify()方法的replacer參數有一個非常簡單的解決方案,您可以在其中傳遞一個函數來修改默認序列化行為。

這是您如何使用它。 考慮以下循環圖中有 4 個節點的示例。

// node constructor
function Node(key, value) {
    this.name = key;
    this.value = value;
    this.next = null;
}

//create some nodes
var n1 = new Node("A", 1);
var n2 = new Node("B", 2);
var n3 = new Node("C", 3);
var n4 = new Node("D", 4);

// setup some cyclic references
n1.next = n2;
n2.next = n3;
n3.next = n4;
n4.next = n1;

function normalStringify(jsonObject) {
    // this will generate an error when trying to serialize
    // an object with cyclic references
    console.log(JSON.stringify(jsonObject));
}

function cyclicStringify(jsonObject) {
    // this will successfully serialize objects with cyclic
    // references by supplying @name for an object already
    // serialized instead of passing the actual object again,
    // thus breaking the vicious circle :)
    var alreadyVisited = [];
    var serializedData = JSON.stringify(jsonObject, function(key, value) {
        if (typeof value == "object") {
            if (alreadyVisited.indexOf(value.name) >= 0) {
                // do something other that putting the reference, like 
                // putting some name that you can use to build the 
                // reference again later, for eg.
                return "@" + value.name;
            }
            alreadyVisited.push(value.name);
        }
        return value;
    });
    console.log(serializedData);
}

稍后,您可以通過解析序列化數據並修改next屬性以指向實際對象,如果它使用帶有@類的命名引用,則可以輕松地使用循環引用重新創建實際對象。

這就是我最終在我的情況下解決它的方式。 這至少適用於 Gson & Jackson。

private static final Gson gson = buildGson();

private static Gson buildGson() {
    return new GsonBuilder().addSerializationExclusionStrategy( getExclusionStrategy() ).create();  
}

private static ExclusionStrategy getExclusionStrategy() {
    ExclusionStrategy exlStrategy = new ExclusionStrategy() {
        @Override
        public boolean shouldSkipField(FieldAttributes fas) {
            return ( null != fas.getAnnotation(ManyToOne.class) );
        }
        @Override
        public boolean shouldSkipClass(Class<?> classO) {
            return ( null != classO.getAnnotation(ManyToOne.class) );
        }
    };
    return exlStrategy;
} 

當您有兩個對象時,可能會出現此錯誤:

class object1{
    private object2 o2;
}

class object2{
    private object1 o1;
}

使用 GSon 進行序列化時,出現此錯誤:

java.lang.IllegalStateException: circular reference error

Offending field: o1

要解決這個問題,只需添加關鍵字瞬態:

class object1{
    private object2 o2;
}

class object2{
    transient private object1 o1;
}

正如您在這里看到的: 為什么 Java 有瞬態字段?

Java 中的transient 關鍵字用於指示不應序列化字段。

Jackson 提供了JsonIdentityInfo注釋來防止循環引用。 你可以在這里查看教程。

如果使用GSON將Java類轉成JSON可以避免出現循環引用和不定式循環的字段,只需要在希望在JSON中出現的字段加上@Expose注解,沒有的字段注釋 @Expose 沒有出現在 JSON 中。

例如,如果我們嘗試使用類 Route 的字段路由序列化類 User,並且類 Route 具有類 User 的字段 user,則會出現循環引用,然后 GSON 嘗試序列化類 User 並嘗試序列化路由時,序列化類 Route 並在類 Route 中嘗試序列化字段 user,並再次嘗試序列化類 User,有一個循環引用會引發不定式循環。 我展示了提到的 User 和 Route 類。

import com.google.gson.annotations.Expose;

類用戶

@Entity
@Table(name = "user")
public class User {
    
@Column(name = "name", nullable = false)
@Expose
private String name;

@OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
@OnDelete(action = OnDeleteAction.CASCADE)
private Set<Route> routes;

@ManyToMany(fetch = FetchType.EAGER)
@OnDelete(action = OnDeleteAction.CASCADE)
@JoinTable(name = "like_", joinColumns = @JoinColumn(name = "id_user"),
        inverseJoinColumns = @JoinColumn(name = "id_route"),
        foreignKey = @ForeignKey(name = ""),
        inverseForeignKey = @ForeignKey(name = ""))
private Set<Route> likes;

課程路線

  @Entity
  @Table(name = "route")
  public class Route {
      
  @ManyToOne()
  @JoinColumn(nullable = false, name = "id_user", foreignKey = 
  @ForeignKey(name = "c"))    
  private User user;

為了避免不定式循環,我們使用提供 GSON 的注解 @Expose。

我以 JSON 格式顯示了使用 GSON 類 User 序列化的結果。

{
    "name": "ignacio"  
}

我們可以看到 JSON 格式中不存在字段 route 和 likes,只有字段名稱。 因此,避免循環引用。

如果我們想使用它,我們必須以特定的方式創建一個對象 GSON。

Gson converterJavaToJson = new GsonBuilder().setPrettyPrinting().excludeFieldsWithoutExposeAnnotation().create();

最后,我們使用創建的conversor GSON轉換hibernate用戶模型的java類。

 User user = createUserWithHibernate();
 String json = converterJavaToJson.toJson(user);

答案數字 8 更好,我認為如果您知道哪個字段引發錯誤,您只需將 fild 設置為 null 並解決。

List<RequestMessage> requestMessages = lazyLoadPaginated(first, pageSize, sortField, sortOrder, filters, joinWith);
    for (RequestMessage requestMessage : requestMessages) {
        Hibernate.initialize(requestMessage.getService());
        Hibernate.initialize(requestMessage.getService().getGroupService());
        Hibernate.initialize(requestMessage.getRequestMessageProfessionals());
        for (RequestMessageProfessional rmp : requestMessage.getRequestMessageProfessionals()) {
            Hibernate.initialize(rmp.getProfessional());
            rmp.setRequestMessage(null); // **
        }
    }

為了使代碼可讀,將大注釋從注釋// **移到下面。

java.lang.StackOverflowError [請求處理失敗; 嵌套異常是 org.springframework.http.converter.HttpMessageNotWritableException:無法寫入 JSON:無限遞歸(StackOverflowError)(通過參考鏈:com.service.pegazo.bo.RequestMessageProfessional["requestMessage"]->com.service.pegazo。 bo.RequestMessage["requestMessageProfessionals"]

例如,ProductBean 有 serialBean。 映射將是雙向關系。 如果我們現在嘗試使用gson.toJson() ,它將以循環引用結束。 為了避免這個問題,您可以按照以下步驟操作:

  1. 從數據源檢索結果。
  2. 迭代列表並確保serialBean 不為空,然后
  3. 設置productBean.serialBean.productBean = null;
  4. 然后嘗試使用gson.toJson();

那應該可以解決問題

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM