关于Java:如何解决由Hibernate双向映射引起的JSON序列化程序中的循环引用?

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

我正在编写一个序列化程序来将pojo序列化到json,但是陷入了循环引用问题。在Hibernate双向一对多关系中,父级引用子级,子级引用返回父级,在这里,序列化程序将终止。(见下面的示例代码)如何打破这个循环?我们可以让一个对象的所有者树来看看对象本身是否存在于它自己的所有者层次结构中?有没有其他方法来确定引用是否是圆形的?或者其他解决这个问题的方法?


我依靠GoogleJSON通过使用这个特性来处理这类问题。

Excluding Fields From Serialization and Deserialization

假设A和B类之间存在双向关系,如下所示

1
2
3
4
5
public class A implements Serializable {

    private B b;

}

和B

1
2
3
4
5
public class B implements Serializable {

    private A a;

}

现在使用gsonbuilder获取自定义gson对象,如下所示(注意setExclusionStrategies方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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();

现在我们的循环参考

1
2
3
4
5
6
7
8
A a = new A();
B b = new B();

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

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

看看gsonbuilder类


Jackson1.6(2010年9月发布)对处理此类父/子链接具有特定的基于注释的支持,请参阅http://wiki.fasterxml.com/jacksonfeaturebidireferences。(返回快照)

当然,您已经可以排除已经使用大多数JSON处理包(Jackson、GSON和Flex JSON至少支持它)的父链接序列化,但真正的诀窍是如何反序列化它(重新创建父链接),而不仅仅是处理序列化端。虽然现在听起来,排除可能对你有用。

编辑(2012年4月):Jackson 2.0现在支持真正的身份引用(Wayback Snapshot),所以您也可以用这种方式解决它。


在解决这个问题时,我采用了以下方法(标准化整个应用程序的流程,使代码清晰且可重用):

  • 创建要在要排除的字段上使用的批注类
  • 定义一个实现Google的ExclusionStrategy接口的类
  • 创建一个使用gsonbuilder生成gson对象的简单方法(类似于Arthur的解释)
  • 根据需要注释要排除的字段
  • 将序列化规则应用于com.google.gson.gson对象
  • 序列化对象
  • 代码如下:

    1)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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)

    1
    2
    3
    4
    5
    static Gson createGsonFromBuilder( ExclusionStrategy exs ){
        GsonBuilder gsonbuilder = new GsonBuilder();
        gsonbuilder.setExclusionStrategies(exs);
        return gsonbuilder.serializeNulls().create();
    }

    4)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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)

    在第一种情况下,向构造函数提供了空值,您可以指定要排除的另一个类-这两个选项都添加在下面

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

    6)

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

    或者,排除日期对象

    1
    String jsonRepresentation = _gsonObj.toJson(_myobject);


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

    处理遍历对象图时处理循环的一种方法是跟踪迄今为止看到的对象(使用标识比较),以防止自己遍历无限循环。


    如果使用jackon进行序列化,只需将@jsonbackreference应用于双向映射它将解决循环参考问题。

    注意:@jsonbackreference用于解决无限递归(stackoverflowerror)


    我用了一个类似于亚瑟的解决方案,但我用的不是setExclusionStrategies

    1
    2
    3
    Gson gson = new GsonBuilder()
                    .excludeFieldsWithoutExposeAnnotation()
                    .create();

    对于JSON中需要的字段,使用@Exposegson注释,其他字段除外。


    这就是我最终解决这个问题的方法。这至少适用于GSON和Jackson。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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;
    }

    如果您使用的是javascript,那么使用JSON.stringify()方法的replacer参数有一个非常简单的解决方案,您可以通过函数来修改默认的序列化行为。

    以下是您使用它的方法。考虑下面的例子,循环图中有4个节点。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    // 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属性来指向实际对象,从而轻松地用循环引用重新创建实际对象,前提是它使用的是与@类似的命名引用。


    Jackson提供了JsonIdentityInfo注释以防止循环引用。您可以在这里查看教程。


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

    1
    2
    3
    4
    5
    6
    7
    class object1{
        private object2 o2;
    }

    class object2{
        private object1 o1;
    }

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

    1
    2
    3
    java.lang.IllegalStateException: circular reference error

    Offending field: o1

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

    1
    2
    3
    4
    5
    6
    7
    class object1{
        private object2 o2;
    }

    class object2{
        transient private object1 o1;
    }

    正如你在这里看到的:为什么Java有瞬态字段?

    The transient keyword in Java is used to indicate that a field should not be serialized.


    答案8是更好的,我想如果你知道哪个字段抛出了一个错误,你只需将fild设置为空并解决。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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 [Request processing failed; nested exception is org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Infinite recursion (StackOverflowError) (through reference chain: com.service.pegazo.bo.RequestMessageProfessional["requestMessage"]->com.service.pegazo.bo.RequestMessage["requestMessageProfessionals"]


    例如,productbean得到了serialbean。映射将是双向关系。如果我们现在尝试使用gson.toJson(),它将以循环引用结束。为了避免该问题,可以执行以下步骤:

  • 从数据源检索结果。
  • 迭代列表并确保seriabean不为空,然后
  • 设置productBean.serialBean.productBean = null;
  • 然后尝试使用gson.toJson();
  • 这应该解决问题