面对 Set 的循环依赖时的 Java 序列化错误

标签 java hibernate serialization

我的项目是 EJB3 上的 java 项目,使用 Hibernate 和 Weblogic 服务器。

为了方便起见(据我所知,hibernate 很典型),一些实体包含循环依赖(父知道子,子知道父)。此外,对于某些子类 - hashCode()equals() 方法取决于它们的父类(因为它是唯一键)。

在工作时,我看到了一个奇怪的行为 - 从服务器返回到客户端的一些 Set,虽然包含正确的元素,但表现得好像它们什么都不包含一样。例如,一个像这样的简单测试:set.contains(set.toArray()[0]) 虽然 hashCode() 返回了 false > 方法不错。

经过大量调试后,我能够生成 2 个重现问题的简单类(我可以向您保证,这两个类中的 hashCode() 函数都是自反、传递和对称):

package test;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;

public class ClientTest implements Serializable {
    public static void main(String[] args) throws Exception {
        SerializableClass serializationTest = new SerializableClass();
        FieldOfSerializableClass hashMember = new FieldOfSerializableClass();
        hashMember.setParentLink(serializationTest);
        serializationTest.setHashCodeField("Some string");
        serializationTest
                .setSomeSet(new HashSet<FieldOfSerializableClass>());
        serializationTest.getSomeSet().add(hashMember);
        System.out.println("Does it contain its member? (should return true!) "
                + serializationTest.getSomeSet().contains(hashMember));
        new ObjectOutputStream(new FileOutputStream("temp"))
                .writeObject(serializationTest);
        SerializableClass testAfterDeserialize = (SerializableClass) new ObjectInputStream(
                new FileInputStream(new File("temp"))).readObject();
        System.out.println("Does it contain its member? (should return true!) "
                + testAfterDeserialize.getSomeSet().contains(hashMember));

        for (Object o : testAfterDeserialize.getSomeSet()) {
            System.out.println("Does it contain its member by equality? (should return true!) "+ o.equals(hashMember));
        }

    }

    public static class SerializableClass implements Serializable {
        private Set<FieldOfSerializableClass> mSomeSet;
        private String mHashCodeField;

        public void setSomeSet(Set<FieldOfSerializableClass> pSomeSet) {
            mSomeSet = pSomeSet;
        }

        public Set<FieldOfSerializableClass> getSomeSet() {
            return mSomeSet;
        }

        public void setHashCodeField(String pHashCodeField) {
            mHashCodeField = pHashCodeField;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;

            System.out.println("In hashCode - value of mHashCodeField: "
                    + mHashCodeField);
            result = prime
                    * result
                    + ((mHashCodeField == null) ? 0 : mHashCodeField.hashCode());
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            SerializableClass other = (SerializableClass) obj;

            if (mHashCodeField == null) {
                if (other.mHashCodeField != null) {
                    return false;
                }
            } else if (!mHashCodeField.equals(other.mHashCodeField))
                return false;
            return true;
        }

        private void readObject(java.io.ObjectInputStream in)
                throws IOException, ClassNotFoundException {
            System.out.println("Just started serializing");
            in.defaultReadObject();
            System.out.println("Just finished serializing");
        }
    }

    public static class FieldOfSerializableClass implements Serializable {
        private SerializableClass mParentLink;

        public void setParentLink(SerializableClass pParentLink) {
            mParentLink = pParentLink;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result
                    + ((mParentLink == null) ? 0 : mParentLink.hashCode());

            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            FieldOfSerializableClass other = (FieldOfSerializableClass) obj;
            if (mParentLink == null) {
                if (other.mParentLink != null) {
                    return false;
                }
            } else if (!mParentLink.equals(other.mParentLink))
                return false;
            return true;
        }
    }

}

这产生了以下输出:

    In hashCode - value of mHashCodeField: Some string
    In hashCode - value of mHashCodeField: Some string
    Does it contain its member? (should return true!) true
    Just started serializing
    In hashCode - value of mHashCodeField: null
    Just finished serializing
    In hashCode - value of mHashCodeField: Some string
    Does it contain its member? (should return true!) false
    Does it contain its member by equality? (should return true!) true

This tells me that the order in which Java serializes the object is wrong! It starts serializing the Set before the String, and thus causing the above problem.

What should I do in this situation? Is there any option (aside from implementing readResolve for many entities...) to direct java to serialize a class in a certain order? Also, is it fundamentally wrong for an entity to base its hashCode on its parent?

Edit: A solution was suggested by a colleague - Because I'm using Hibernate, every entity has a unique long ID. I know that Hibernate specify not to use this ID in the equals method - but what about hashCode? Using this unique ID as hashcode seems to solve the above problem with a minimal risk of performance issues. Are there any other implications to using the ID as hashcode?

SECOND EDIT: I went and implemented my partial solution (All of the enteties now use the ID field for the hashCode() function and no longer relay on other enteties for it) but, alas, Serialization bugs still continue to plague me! Below is a sample code with another serialization bug. What I think is happening is this - ClassA start deserializing, sees it has a ClassB to deserialize and BEFORE it deserializes its ID, it start deserializing the ClassB. B start to deserialize and Sees it has a Set of ClassA. The ClassA instance is partialy deserialized, but even though ClassB adds it to the Set (using the missing ID of ClassA), completes the deserializning, ClassA then completes and the bug occurs.

What can I do to solve this?! Circular dependencies is a very used practice in Hibernate and I just can't accept it that i'm the only one with this problem.

Another possible solution is to have a dedicated variable for the hashCode (will be calculated by the object's ID) and make sure (view readObject and writeObject) that it will be read BEFORE VERY OTHER OBJECT. What do you think? Are there any disadvantages to this solution?

The sample code:

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;

public class Test implements Serializable
{
    public static void main(String[] args) throws Exception
    {
        ClassA aClass = new ClassA();
        aClass.setId(Long.valueOf(321));

        ClassB bClass = new ClassB();
        bClass.setId(Long.valueOf(921));

        Set<ClassA> set = new HashSet<ClassA>();
        set.add(aClass);

        bClass.setSetfield(set);
        aClass.setBField(bClass);

        Set<ClassA> goodClassA = aClass.getBField().getSetfield();
        Set<ClassA> badClassA = serializeAndDeserialize(aClass).getBField().getSetfield();

        System.out.println("Does it contain its member? (should return true!) " + goodClassA.contains(goodClassA.toArray()[0]));
        System.out.println("Does it contain its member? (should return true!) " + badClassA.contains(badClassA.toArray()[0]));
    }

    public static ClassA serializeAndDeserialize(ClassA s) throws Exception
    {
        new ObjectOutputStream(new FileOutputStream(new File("temp"))).writeObject(s);
        return (ClassA) new ObjectInputStream(new FileInputStream(new File("temp"))).readObject();
    }

    public static class ClassB implements Serializable
    {
        private Long mId;
        private Set<ClassA> mSetfield = new HashSet<ClassA>();
        public Long getmId() {
            return mId;
        }
        public void setId(Long mId) {
            this.mId = mId;
        }
        public Set<ClassA> getSetfield() {
            return mSetfield;
        }
        public void setSetfield(Set<ClassA> mSetfield) {
            this.mSetfield = mSetfield;
        }
        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((mId == null) ? 0 : mId.hashCode());
            return result;
        }
        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            ClassB other = (ClassB) obj;
            if (mId == null) {
                if (other.mId != null)
                    return false;
            } else if (!mId.equals(other.mId))
                return false;
            return true;
        }       
    }

    public static class ClassA implements Serializable
    {
        private Long mId;
        private ClassB mBField;
        public Long getmId() {
            return mId;
        }
        public void setId(Long mId) {
            this.mId = mId;
        }
        public ClassB getBField() {
            return mBField;
        }
        public void setBField(ClassB mBField) {
            this.mBField = mBField;
        }
        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((mId == null) ? 0 : mId.hashCode());
            return result;
        }
        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            ClassA other = (ClassA) obj;
            if (mId == null) {
                if (other.mId != null)
                    return false;
            } else if (!mId.equals(other.mId))
                return false;
            return true;
        }
    }
}

最佳答案

因此,在我阅读它时,您将 FieldOfSerializableClass 的 hashCode 基于父对象。这似乎是您问题的最终原因,也是一个非常值得怀疑的设计。 hashCode()equals() 方法处理对象身份,根本不应该与包含它们的父对象相关。对象的身份根据拥有它的父对象而改变的想法至少对我来说是非常陌生的,这是您的代码不起作用的最终原因。

虽然其他答案有一些解决问题的方法,但我认为解决此问题的最简单方法是为 FieldOfSerializableClass 类提供自己的标识。您可以将 mHashCodeFieldSerializableClass 复制到 FieldOfSerializableClass。在对象上设置父级后,您可以获取其 mHashCodeField 并将其存储在本地。

public void setParentLink(SerializableClass pParentLink) {
    this.mHashCodeField = pParentLink.mHashCodeField;
    mParentLink = pParentLink;
}

然后 hashcode(和 equals)方法看起来类似于 SerializableClass 的方法。

@Override
public int hashCode() {
    return ((mHashCodeField == null) ? 0 : mHashCodeField.hashCode());
}

但实际上您应该考虑更改代码,以降低亲子关系的耦合度。考虑一下如果您在一个字段上调用 ​​setParentLink() 而它已经在另一个 SerializableClass 集合中会发生什么。突然之间,原始类甚至无法在其集合中找到该项目,因为它的身份已更改。就 Java 对象而言,为 FieldOfSerializableClass 类分配一些与父类不同的排序标识是此处的最佳模式。

如果您不能使用 FieldOfSerializableClass 作为正确的标识。但我会使用 Hibernate 提供给您的自动生成的 ID。您只需要确保该对象在被放入另一个对象的集合之前已被插入到数据库中。

关于面对 Set 的循环依赖时的 Java 序列化错误,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/7901006/

相关文章:

java - 如何在clojure中将java类序列化为json

java - 在 Java 中构建/运行流式 Weka 文本分类器

java - 在 Apache Camel 中,如果端点不存在,我如何收到错误?

java - 用于去除日期和值的正则表达式

java - Hibernate 逆向工程组合键创建额外的 ID 类

java - Vaadin + TabSheet + Grails Service = 当前线程没有 session

java - Hibernate 和 Apache Tomcat 的问题

php - 分离和序列化学说实体

java - Gson,带有泛型的 ClassCastException

java - BigDecimal ROUND_HALF_EVEN 舍入的输出是否错误?