使用Set面向循环依赖时的Java序列化错误

Ran*_*Ran 7 java serialization hibernate

我的项目是一个使用Hibernate和服务器的java项目.EJB3Weblogic

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

在工作时我看到了一个奇怪的行为 - 从服务器返回到客户端的一些集合虽然包含正确的元素,但它们的行为却没有包含任何内容.例如,一个简单的测试,例如:set.contains(set.toArray()[0])返回false虽然hashCode()方法很好.

经过大量的调试后,我能够生成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;
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

这产生了以下输出:

    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

这告诉我Java序列化对象的顺序是错误的!它开始序列化字符串之前的Set,从而导致上述问题.

在这种情况下我该怎么办?有没有选择(除了实现readResolve许多实体...)以指导java按特定顺序序列化一个类?此外,一个实体hashCode以其父母为基础是否从根本上是错误的?

编辑:一位同事提出了一个解决方案 - 因为我正在使用Hibernate,所以每个实体都有一个唯一的长ID.我知道Hibernate指定不在equals方法中使用此ID - 但是hashCode呢?使用此唯一ID作为哈希码似乎可以解决上述问题,同时将性能问题的风险降至最低.将ID用作哈希码是否还有其他影响?

第二次编辑:我去实现了我的部分解决方案(现在所有的enteties都使用了hashCode()函数的ID字段,并且不再继续使用其他enteties)但是,唉,序列化错误仍然困扰着我!下面是另一个序列化错误的示例代码.我认为发生了这种情况--ClassA开始反序列化,看到它有一个ClassB来反序列化,在它反序列化它的ID之前,它开始反序列化ClassB.B开始反序列化并且看到它有一组ClassA.ClassA实例是部分反序列化的,但即使ClassB将它添加到Set(使用ClassA的缺失ID),完成deserializning,然后ClassA完成并发生错误.

我该怎么做才能解决这个问题?!循环依赖是Hibernate中一个非常常用的实践,我不能接受它,我是唯一有这个问题的人.

另一种可能的解决方案是为hashCode设置一个专用变量(将由对象的ID计算)并确保(查看readObject和writeObject)它将在非常其他对象之前读取.你怎么看?这个解决方案有什么缺点吗?

示例代码:

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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

pgr*_*ras 0

equals 方法必须是自反、传递和对称的......

hashCode 方法必须具有以下属性

hashCode的通用约定是:

每当在 Java 应用程序执行期间对同一对象多次调用 hashCode 方法时,只要对象上的 equals 比较中使用的信息没有被修改,hashCode 方法就必须始终返回相同的整数。从应用程序的一次执行到同一应用程序的另一次执行,该整数不需要保持一致。

如果根据 equals(Object) 方法两个对象相等,则对这两个对象调用 hashCode 方法必须产生相同的整数结果。

如果两个对象根据 equals(java.lang.Object) 方法不相等,则不要求对这两个对象中的每一个调用 hashCode 方法必须产生不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同的整数结果可能会提高哈希表的性能。

在这里,看起来反序列化期间用于将条目放入集合中的 hashCode 与 contains() 期间计算的 hashCode 不同。顺便说一句,正如您注意到该条目位于Set 中一样,您只是无法通过它的 hashCode 访问它,如果您循环 Set 的内容,您将找到元素。

可能的解决方案:

  • 有一个不依赖于父对象的 hashCode。
  • 使用不使用哈希码的数据结构(List、TreeSet...)
  • 不要在 Set 上使用 contains 方法...
  • 实现 ReadResolve 以在反序列化后重新创建 Set...

[编辑]:看起来你并不孤单bug_id=4957674