在 JPA 实体序列化 (JSON) 上防止 JAX-RS 中的无限递归(不使用 Jackson 注释)

Preventing infinite recursion in JAX-RS on JPA Entity Serialization (JSON) (without using Jackson annotations)

我有一个实体如下:

@XmlRootElement
@Entity
@Table(name="CATEGORY")
@Access(AccessType.FIELD)
@Cacheable
@NamedQueries({
    @NamedQuery(name="category.countAllDeleted", query="SELECT COUNT(c) FROM Category c WHERE c.deletionTimestamp IS NOT NULL"),
    @NamedQuery(name="category.findAllNonDeleted", query="SELECT c from Category c WHERE c.deletionTimestamp IS NULL"),
    @NamedQuery(name="category.findByCategoryName", query="SELECT c FROM Category c JOIN c.descriptions cd WHERE LOWER(TRIM(cd.name)) LIKE ?1")
})
public class Category extends AbstractSoftDeleteAuditableEntity<Integer> implements za.co.sindi.persistence.entity.Entity<Integer>, Serializable {

    /**
     * 
     */
    private static final long serialVersionUID = 4600301568861226295L;

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Column(name="CATEGORY_ID", nullable=false)
    private int id;

    @ManyToOne
    @JoinColumn(name="PARENT_CATEGORY_ID")
    private Category parent;

    @OneToMany(cascade= CascadeType.ALL, mappedBy="category")
    private List<CategoryDescription> descriptions;

    public void addDescription(CategoryDescription description) {
        if (description != null) {
            if (descriptions == null) {
                descriptions = new ArrayList<CategoryDescription>();
            }

            descriptions.add(description);
        }
    }

    /* (non-Javadoc)
     * @see za.co.sindi.entity.IDBasedEntity#getId()
     */
    public Integer getId() {
        // TODO Auto-generated method stub
        return id;
    }

    /* (non-Javadoc)
     * @see za.co.sindi.entity.IDBasedEntity#setId(java.io.Serializable)
     */
    public void setId(Integer id) {
        // TODO Auto-generated method stub
        this.id = (id == null) ? 0 : id;
    }

    /**
     * @return the parent
     */
    public Category getParent() {
        return parent;
    }

    /**
     * @param parent the parent to set
     */
    public void setParent(Category parent) {
        this.parent = parent;
    }

    /**
     * @return the descriptions
     */
    public List<CategoryDescription> getDescriptions() {
        return descriptions;
    }

    /**
     * @param descriptions the descriptions to set
     */
    public void setDescriptions(List<CategoryDescription> descriptions) {
        this.descriptions = descriptions;
    }
}

和:

@XmlRootElement
@Entity
@Table(name="CATEGORY_DESCRIPTION")
@Access(AccessType.FIELD)
@Cacheable
public class CategoryDescription extends AbstractModifiableAuditableEntity<CategoryDescriptionKey> implements za.co.sindi.persistence.entity.Entity<CategoryDescriptionKey>, Serializable {

    /**
     * 
     */
    private static final long serialVersionUID = 4506134647012663247L;

    @EmbeddedId
    private CategoryDescriptionKey id;

    @MapsId("categoryId")
    @ManyToOne/*(fetch=FetchType.LAZY)*/
    @JoinColumn(name="CATEGORY_ID", insertable=false, updatable=false, nullable=false)
    private Category category;

    @MapsId("languageCode")
    @ManyToOne/*(fetch=FetchType.LAZY)*/
    @JoinColumn(name="LANGUAGE_CODE", insertable=false, updatable=false, nullable=false)
    private Language language;

    @Column(name="CATEGORY_NAME", nullable=false)
    private String name;

    @Column(name="DESCRIPTION_PLAINTEXT", nullable=false)
    private String descriptionPlainText;

    @Column(name="DESCRIPTION_MARKDOWN", nullable=false)
    private String descriptionMarkdown;

    @Column(name="DESCRIPTION_HTML", nullable=false)
    private String descriptionHtml;

    /* (non-Javadoc)
     * @see za.co.sindi.entity.IDBasedEntity#getId()
     */
    public CategoryDescriptionKey getId() {
        // TODO Auto-generated method stub
        return id;
    }

    /* (non-Javadoc)
     * @see za.co.sindi.entity.IDBasedEntity#setId(java.io.Serializable)
     */
    public void setId(CategoryDescriptionKey id) {
        // TODO Auto-generated method stub
        this.id = id;
    }

    /**
     * @return the category
     */
    public Category getCategory() {
        return category;
    }

    /**
     * @param category the category to set
     */
    public void setCategory(Category category) {
        this.category = category;
    }

    /**
     * @return the language
     */
    public Language getLanguage() {
        return language;
    }

    /**
     * @param language the language to set
     */
    public void setLanguage(Language language) {
        this.language = language;
    }

    /**
     * @return the name
     */
    public String getName() {
        return name;
    }

    /**
     * @param name the name to set
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * @return the descriptionPlainText
     */
    public String getDescriptionPlainText() {
        return descriptionPlainText;
    }

    /**
     * @param descriptionPlainText the descriptionPlainText to set
     */
    public void setDescriptionPlainText(String descriptionPlainText) {
        this.descriptionPlainText = descriptionPlainText;
    }

    /**
     * @return the descriptionMarkdown
     */
    public String getDescriptionMarkdown() {
        return descriptionMarkdown;
    }

    /**
     * @param descriptionMarkdown the descriptionMarkdown to set
     */
    public void setDescriptionMarkdown(String descriptionMarkdown) {
        this.descriptionMarkdown = descriptionMarkdown;
    }

    /**
     * @return the descriptionHtml
     */
    public String getDescriptionHtml() {
        return descriptionHtml;
    }

    /**
     * @param descriptionHtml the descriptionHtml to set
     */
    public void setDescriptionHtml(String descriptionHtml) {
        this.descriptionHtml = descriptionHtml;
    }   
}

当使用 JAX-RS 返回 Collection<Category> 并在 JBoss Wildfly 8.2.0-Final 上部署时,我得到以下堆栈跟踪:

Caused by: com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (WhosebugError) (through reference chain: za.co.sindi.unsteve.persistence.entity.Category["descriptions"]->org.hibernate.collection.internal.PersistentBag[0]->za.co.sindi.unsteve.persistence.entity.CategoryDescription["category"]->za.co.sindi.unsteve.persistence.entity.Category["descriptions"]->

this question 等问题的答案需要使用 Jackson 特定注释。我的项目要求 严格 坚持 Java EE 特定框架。有没有一种解决方案可以在不使用 Jackson 注释的情况下防止无限递归?如果没有,我们可以创建一个 Jackson 可以用来代替注释的配置文件(XML 文件等)吗?这样做的原因是应用程序不能只绑定到 Wildfly 特定库。

是的。创建专用数据结构(例如数据传输对象或 DTO)并映射要从 HTTP 端点发送的字段。

你把问题混为一谈,结果通常很糟糕。

JPA 实体是您的 API 数据结构,REST 表示(JSON 或 XML DTO)是您的 REST API 提供的数据负载。

我会说你在这里没有几个选择:

  1. 使用 transient 关键字或 @XmlTransient 让 JAX-RS 忽略一些属性/字段(它们不会被编组),
  2. 使用 DTO 可以更好地为最终用户反映您的数据结构;随着时间的推移,您的实体之间的差异、它在 RDBMS 中的建模方式以及您向用户return发送的内容将越来越大,
  3. 结合使用上述两个选项并将某些字段标记为瞬态,同时提供其他 "JAX-RS friendly" 访问器,例如return 只有 Category 的 ID 而不是整个对象。

除了 @JsonIgnore 之外还有一些 Jackson 特定的解决方案,例如:

  • Jackson views -- @JsonView 可用于以更灵活的方式实现相同的方式(例如,它允许您定义何时需要 return 简化对象依赖项(只是相关对象的 ID)以及何时 return 整个对象;您指定要使用的视图,例如在 JAX-RS 入口点,
  • Object Identity 将在编组对象时识别循环依赖并防止无限递归(对象的第一次命中意味着将其作为一个整体按原样放置,同一对象的每隔一次命中意味着仅放置其ID).

我敢肯定还有其他解决方案,但考虑到上述这些,我个人会长期使用 DTO-运行。您可以使用一些自动映射解决方案,例如 Dozer 来帮助您完成这项讨厌的重复工作。
话虽这么说,但最好将您提供给用户并从用户那里接受的数据与您的内部数据分开。