如何解决由休眠双向映射导致的json序列化器中的循环引用?


81

我正在编写一个序列化程序以将POJO序列化为JSON,但陷入循环引用问题。在休眠的双向一对多关系中,父级引用子级,而子级引用又回到父级,此时我的序列化程序死亡。(请参见下面的示例代码)
如何打破这个循环?我们可以获取对象的所有者树以查看对象本身是否存在于其所有者层次结构中的某个位置吗?还有其他方法可以找到参考是否为圆形吗?或任何其他想法来解决这个问题?


您是要为我们粘贴一些代码来帮助您解决问题吗?
罗素

2
eugene基于注释的解决方案是可以的,但在这种情况下,无需其他注释和ExclusionStrategy实现。只需使用Java'transient '关键字即可。它适用于标准Java对象序列化,但Gson尊重它
MeTTeO 2012年

Answers:


11

双向关系甚至可以用JSON表示吗?某些数据格式不适用于某些类型的数据建模。

处理遍历对象图时处理周期的一种方法是跟踪您到目前为止已看到的对象(使用身份比较),以防止自己遍历无限循环。


我做了同样的事情,它可以工作,但不确定它是否可以在所有映射方案中工作。至少就目前而言,我已经做好了准备,并将继续考虑拥有更优雅的想法
WSK 2010年

他们当然可以-只是没有本机数据类型或结构。但是任何东西都可以用XML,JSON或大多数其他数据格式表示。
StaxMan 2010年

4
我很好奇-您如何在JSON中表示循环引用?
马特b 2010年

1
多种方式:请记住,通常它不仅是JSON,而且是JSON和一些元数据的组合,最常见的是您用于绑定到JSON或从JSON绑定的类定义。对于JSON而言,这只是是否使用某种对象身份或重新创建链接的问题(例如,Jackson lib有一种表示父/子链接的特定方法)。
StaxMan 2010年

46

我靠 Google JSON通过使用The feature处理此类问题

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

假设A类和B类之间的双向关系如下

public class A implements Serializable {

    private B 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


感谢亚瑟(Arthur)的善意建议,但实际的问题是构建所谓的“ shouldSkipClass”方法的最佳方法是什么。目前,我研究了Matt的想法并解决了我的问题,但仍然持怀疑态度,将来这种解决方案可能会在某些情况下失效。
WSK

8
这通过删除循环引用来“解决”它们。无法从生成的JSON重建原始数据结构。
Sotirios Delimanolis 2015年

34

Jackson 1.6(于2010年9月发布)具有基于注释的特定支持,可以处理此类父/子链接,请参阅http://wiki.fasterxml.com/JacksonFeatureBiDirReferences。(回溯快照

您当然可以使用大多数JSON处理程序包(已经至少支持jackson,gson和flex-json)来排除父链接的序列化,但是真正的窍门在于如何反序列化它(重新创建父链接),只需处理序列化方面。尽管现在看来,排除可能对您有用。

编辑(2012年4月):杰克逊2.0现在支持真实的身份引用Wayback Snapshot),因此您也可以通过这种方式解决它。


当您的方向并不总是相同时,如何使此工作正常。.我尝试将两个注释都放在两个字段上,但不起作用:A类{私人B b; } B类{@JsonManagedReference(“ abc”)@JsonBackReference(“ xyz”)私有A a; }
azi 2014年

如上所述,对象ID(@JsonIdentityInfo)是使常规引用起作用的方式。托管/后退引用确实需要某些指导,因此它们不适用于您的情况。
StaxMan 2014年

12

为了解决这个问题,我采用了以下方法(对我的应用程序进行标准化处理,使代码清晰可重用):

  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);

1
这样可以防止子对象被处理,我可以添加子json并停止循环

3

如果您正在使用Jackon进行序列化,只需将@JsonBackReference应用于双向映射即可解决循环引用问题。

注意:@JsonBackReference用于解决无限递归(StackOverflowError)


@JsonIgnore使我JpaRepository无法映射该属性,但@JsonBackReference解决了循环引用问题,并且仍然允许对有问题的属性进行适当的映射
Bramastic

2

使用了类似于Arthur的解决方案,但setExclusionStrategies我没有使用

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

@Expose为json中我需要的字段使用了gson注释,其他字段除外。


多谢,伙计。这对我有用。经过长时间的研究,我终于找到了摆脱这种错误的方法。
kepy97'3

1

如果您使用的是Javascript,则可以使用replacer参数JSON.stringify()method,您可以在其中传递函数来修改默认的序列化行为。

使用方法如下。考虑下面的示例,在循环图中有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属性以指向实际对象(如果@在此示例中使用带有类似符号的命名引用),轻松地使用循环引用重新创建实际对象。


1

这就是我最终解决此问题的方式。至少与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;
} 

1

如果您使用的是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;

0

当您有两个对象时,可能会出现此错误:

class object1{
    private object2 o2;
}

class object2{
    private object1 o1;
}

使用GSon进行序列化时,出现了以下错误:

java.lang.IllegalStateException: circular reference error

Offending field: o1

要解决这个问题,只需添加关键字transient:

class object1{
    private object2 o2;
}

class object2{
    transient private object1 o1;
}

如您在这里所看到的:为什么Java具有瞬态字段?

Java中的瞬态关键字用于指示不应序列化字段。



-3

我认为答案数字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”]


1
没有“答案编号8”,您最好提供所引用答案的作者姓名。您在此处发布的文字不可读,请查看答案的发布方式,并尝试将其整洁地布置。最后,我不明白这如何回答原始问题。请添加更多详细信息以解释答案。
AdrianHHH 2015年

-11

例如,ProductBean获得了serialBean。映射将是双向关系。如果现在尝试使用gson.toJson(),它将以循环引用结尾。为了避免该问题,您可以按照以下步骤操作:

  1. 从数据源检索结果。
  2. 遍历列表,并确保serialBean不为null,然后
  3. productBean.serialBean.productBean = null;
  4. 然后尝试使用 gson.toJson();

那应该解决问题

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.