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

5

我有一个实体,如下所示:

@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;
    }
}

AND:

@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 (StackOverflowError) (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"]->

有些问题的回答中涉及到使用Jackson特定的注解,例如这个问题。我的项目要求严格使用Java EE特定的框架,是否存在一种解决方案可以防止无限递归,而不使用Jackson注解?如果没有,我们能否创建一个配置文件(XML文件等)来替代Jackson注解呢?原因是该应用程序不仅必须绑定Wildfly特定的库。


那么在你的整个项目中,你完全没有使用任何第三方库,只是使用了javaee-api吗? - Paul Samsotha
@peeskillet 是的。我相信这是可能的,而这个项目将证明它。 - Buhake Sindi
2个回答

3
我认为你有以下几个选择:
  1. 使用 transient 关键字或@XmlTransient 注解,让 JAX-RS 忽略某些属性 / 字段(它们不会被序列化)。
  2. 使用 DTO 更好地反映用户端的数据结构;随着时间推移,你的实体、它在 RDBMS 中的建模方式以及你向用户返回的内容之间的差异将越来越大。
  3. 综合上述两种选项,将某些字段标记为瞬态,并同时提供其他“JAX-RS友好”的访问器,例如只返回类别的 ID 而不是整个对象。

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

  • Jackson 视图 -- 可以使用 @JsonView 以更灵活的方式实现相同的功能(例如,它允许你定义何时返回没有依赖关系的简化对象(只返回相关对象的 ID),何时返回整个对象;你可以在 JAX-RS 入口点上指定要使用的视图)。
  • 对象标识,它将在序列化对象时识别循环依赖关系并防止无限递归(第一次命中该对象意味着将其作为整个对象放置,每次命中同一个对象意味着仅放置其 ID)。

我相信还有其他解决方案,但是针对上述内容,我个人会选择 DTO。你可以使用一些自动映射解决方案(如 Dozer)来帮助你完成这个繁琐的重复工作。
话虽如此,最好将你向用户展示和接受的数据与你的内部数据分开处理。


2
是的。创建一个专门的数据结构(例如数据传输对象或DTO),并从您的HTTP端点映射要发送的字段。
你正在混淆关注点,这通常会导致问题。
JPA实体是您向数据结构提供API的方式,REST表示(JSON或XML DTO)是REST API提供的数据有效载荷。

我没有混淆任何东西。我的RESTful WS返回一个“Collection”,JBoss使用“RESTEasy”(JAX-RS实现),它在内部使用Jackson将我的实体转换为JSON字符串。这是在容器级别完成的,我无法控制它。 - Buhake Sindi
@BuhakeSindi,你可能误解了答案。基本上,答案的意思是:“使用DTO”,一般来说,我必须同意这个答案。 - Paul Samsotha
DTO似乎是解决这个问题的完美方案。 - Buhake Sindi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接