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

Posted

技术标签:

【中文标题】如何解决休眠双向映射导致的json序列化器中的循环引用?【英文标题】:How to solve circular reference in json serializer caused by hibernate bidirectional mapping? 【发布时间】:2011-03-21 09:48:01 【问题描述】:

我正在编写一个序列化程序来将 POJO 序列化为 JSON,但遇到了循环引用问题。在休眠双向一对多关系中,父引用子和子引用返回父,这里我的序列化器死了。 (参见下面的示例代码) 如何打破这个循环?我们能否获取对象的所有者树以查看对象本身是否存在于其自己的所有者层次结构中的某个位置?任何其他方法来查找引用是否将是循环的?或任何其他想法来解决这个问题?

【问题讨论】:

您是要粘贴一些代码让我们帮助您解决问题吗? eugene 的基于注解的解决方案是可以的,但是在这种情况下不需要额外的注解和 ExclusionStrategy 实现。只需使用 Java 'transient' 关键字即可。它适用于标准 Java 对象序列化,但也适用于 Gson respects it。 【参考方案1】:

我依靠Google JSON来处理此类问题,使用该功能

Excluding Fields From Serialization and Deserialization

假设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”方法的最佳方法是什么。现在我研究了马特的想法并解决了我的问题,但仍然持怀疑态度,将来这个解决方案可能会在某些情况下中断。 这通过删除循环引用来“解决”循环引用。无法从生成的 JSON 中重建原始数据结构。【参考方案2】:

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

您当然可以排除已经使用大多数 JSON 处理包的父链接的序列化(jackson、gson 和 flex-json 至少支持它),但真正的技巧是如何将其反序列化(重新创建父链接),而不仅仅是处理序列化方面。虽然现在听起来只是排除可能对您有用。

编辑(2012 年 4 月):Jackson 2.0 现在支持真正的 identity references (Wayback Snapshot),因此您也可以通过这种方式解决它。

【讨论】:

当你的方向并不总是相同时如何让它工作..我尝试将两个注释都放在两个字段上但它没有工作:A类@JsonBackReference(“abc” ) @JsonManagedReference("xyz") 私人 B b; 类 B @JsonManagedReference("abc") @JsonBackReference("xyz") 私人 A a; 如上所述,Object Id (@JsonIdentityInfo) 是使一般引用起作用的方法。托管/反向引用确实需要某些指示,因此它们不适用于您的情况。【参考方案3】:

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

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

【讨论】:

我做了同样的事情,但它可以工作,但不确定它是否适用于所有映射场景。至少现在我已经准备好了,并且会继续考虑有更优雅的想法 当然可以——只是没有本地数据类型或结构。但任何东西都可以用 XML、JSON 或大多数其他数据格式表示。 我很好奇 - 你会如何在 JSON 中表示循环引用? 多种方式:请记住,通常不仅仅是 JSON,而是 JSON 和一些元数据的组合,最常见的类定义用于绑定到 JSON 或从 JSON 绑定。对于 JSON,它只是使用某种对象标识还是重新创建链接的问题(例如,Jackson lib 具有表示父/子链接的特定方式)。【参考方案4】:

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

    创建一个注释类以用于您希望排除的字段 定义一个实现 Google 的 ExclusionStrategy 接口的类 创建一个使用 GsonBuilder 生成 GSON 对象的简单方法(类似于 Arthur 的解释) 根据需要注释要排除的字段 将序列化规则应用于您的 com.google.gson.Gson 对象 序列化您的对象

代码如下:

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

【讨论】:

这会阻止子对象被处理,我可以包含子json然后停止循环【参考方案5】:

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

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

【讨论】:

@JsonIgnore 使我的JpaRepository 无法映射属性,但@JsonBackReference 解决了循环引用并仍然允许对有问题的属性进行正确映射【参考方案6】:

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

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

并在json中我需要的字段使用@Exposegson注解,其他字段被排除在外。

【讨论】:

谢谢老兄。这对我有用。经过长时间的研究,我终于找到了摆脱这个错误的方法。【参考方案7】:

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

【讨论】:

【参考方案8】:

如果您使用的是 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 属性以指向实际对象(如果它使用带有 @ 的命名引用(如本例中))轻松地重新创建具有循环引用的实际对象。

【讨论】:

【参考方案9】:

这就是我最终解决的方法。这至少适用于 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;
 

【讨论】:

【参考方案10】:

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

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;

如您所见:Why does Java have transient fields?

Java中的transient关键字用来表示一个字段不应该被序列化。

【讨论】:

【参考方案11】:

Jackson 提供JsonIdentityInfo 注解来防止循环引用。您可以查看教程here

【讨论】:

【参考方案12】:

如果使用 GSON 将 Java 类转换为 JSON 可以避免导致循环引用和不定式循环的字段,只需将注解 @Expose 放在要出现在 JSON 中的字段中,并且没有注解 @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格式中不存在字段路由和点赞,只有字段名。因此,避免了循环引用。

如果我们想使用它,我们必须以特定方式创建一个对象 GSON。

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

最后,我们使用创建的conversor GSON转换hibernate用户模型的java类。

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

【讨论】:

【参考方案13】:

答案数字 8 更好,我认为如果您知道哪个字段引发错误,您只需将字段设置为 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.***Error [请求处理失败;嵌套异常是 org.springframework.http.converter.HttpMessageNotWritableException:无法写入 JSON:无限递归 (***Error)(通过引用链:com.service.pegazo.bo.RequestMessageProfessional["requestMessage"]->com.service.pegazo。 bo.RequestMessage["requestMessageProfessionals"]

【讨论】:

没有“8号答案”,最好给出参考答案的作者姓名。您在此处发布的文字无法阅读,请查看答案的发布方式并尝试将它们整齐地布置。最后,我不明白这是如何回答原始问题的。请添加更多详细信息以解释答案。【参考方案14】:

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

    从数据源中检索结果。 迭代列表并确保serialBean不为null,然后 设置productBean.serialBean.productBean = null; 然后尝试使用gson.toJson();

应该可以解决问题

【讨论】:

以上是关于如何解决休眠双向映射导致的json序列化器中的循环引用?的主要内容,如果未能解决你的问题,请参考以下文章

流畅的休眠映射,可双向保存

休眠双向多对多映射建议!

@OneToMany 映射休眠中的集合

如何在 Kotlin 中解析 JSON?

Json.NET如何避免循环引用

出现$ref的解决原因