Java在使用Set时面临循环依赖时的序列化错误 [英] Java serialization bug when facing circular dependency with a Set

查看:135
本文介绍了Java在使用Set时面临循环依赖时的序列化错误的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我的项目是使用Hibernate和 Weblogic EJB 项目的 java 项目。 c $ c>服务器。为了方便起见(据我所知,通常是 hibernate ),一些实体包含循环依赖(Parent知道孩子,孩子知道父母)。此外,对于一些子类 - hashCode() equals()方法依赖于它们的父类(As它是一个唯一的键)。

工作时,我看到一个奇怪的行为 - 从服务器返回给客户端的一些集合,尽管包含正确的元素,像他们没有包含例如,一个简单的测试如下: set.contains(set.toArray()[0])返回 false 虽然 hashCode()方法是一个很好的方法。

经过广泛的调试后,我能够生成2个简单的类来重现问题(我可以向你保证 hashCode()

  package test;在这两个类中,c> function都是自反的,传递的和对称的 

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实现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(它是否包含它的成员?(应该返回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(它是否包含它的成员?(应该返回true!)
+ testAfterDeserialize.getSomeSet()。contains(hashMember));

for(Object o:testAfterDeserialize.getSomeSet()){
System.out.println(它是否包含等于它的成员?(应该返回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(在hashCode中 - mHashCodeField的值:
+ mHashCodeField);
result = prime
* result
+((mHashCodeField == null)?0:mHashCodeField.hashCode());
返回结果;

$ b @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;
返回true;

$ b $ private void readObject(java.io.ObjectInputStream in)
throws IOException,ClassNotFoundException {
System.out.println(Just started serializing);
in.defaultReadObject();
System.out.println(刚完成序列化);



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

返回结果;

$ b @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;
返回true;

}

}

以下输出:

 
在hashCode中 - mHashCodeField的值:一些字符串
在hashCode中 - mHashCodeField的值:一些字符串
是否包含其成员? (应该返回true!)true
刚刚开始序列化
在hashCode中 - mHashCodeField的值:null
刚完成序列化
在hashCode中 - mHashCodeField的值:一些字符串
它是否包含其成员? (应该返回true!)false
是否通过平等包含它的成员? (应该返回true!)true

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



在这种情况下应该怎么做?除了为许多实体实现 readResolve 之外,是否有任何选项来指导java以某种顺序序列化类?
另外,一个实体将它的 hashCode 放在它的父类上是不是基本上是错误的?

编辑:一位同事建议解决方案 - 因为我使用Hibernate,所以每个实体都有一个唯一的长ID。我知道Hibernate指定不在equals方法中使用这个ID - 但是hashCode呢?使用这个唯一的ID作为散列码似乎可以解决上述问题,并且性能问题风险最小。使用ID作为散列码还有什么其他含义?



SECOND EDIT :我去实现了我的部分解决方案(现在所有的入门使用hashCode()函数的ID字段,不再继承其他入侵)但是,唉,序列化错误仍然继续困扰着我!下面是带有另一个序列化错误的示例代码。我认为正在发生的是 - ClassA开始反序列化,认为它有一个ClassB反序列化,并且在反序列化它的ID之前,它开始反序列化ClassB。 B开始反序列化并且看到它有一组ClassA。 ClassA实例是部分反序列化的,但即使ClassB将它添加到Set(使用缺少的ClassA ID),完成反序列化,ClassA便完成并发生错误。



我能做些什么来解决这个问题?循环依赖在Hibernate中是一个非常常用的做法,我不能接受它,我是唯一一个遇到这个问题的人。



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

示例代码:

pre $ 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实现Serializable
{
public static void main(String [] args)throws Exception
{
ClassA aClass = new ClassA() ;
Class.setId(Long.valueOf(321));

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

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

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

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

System.out.println(它是否包含它的成员?(应该返回true!)+ goodClassA.contains(goodClassA.toArray()[0]));
System.out.println(它是否包含它的成员?(应该返回true!)+ badClassA.contains(badClassA.toArray()[0]));


public static ClassA serializeAndDeserialize(ClassA s)throws Exception
{
new ObjectOutputStream(new FileOutputStream(new File(temp)))。writeObject(或多个);
return(ClassA)new ObjectInputStream(new FileInputStream(new File(temp)))。readObject();
}

public static class ClassB implements Serializable
{
private Long mId;
私人设置< ClassA> mSetfield = new HashSet< ClassA>();
public Long getmId(){
return mId;
}
public void setId(Long mId){
this.mId = mId;
}
public Set< ClassA> getSetfield(){
返回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());
返回结果;
}
@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;
返回true;



public static class ClassA implements Serializable
{
private Long mId;
私人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());
返回结果;
}
@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;
返回true;




解决方案

因此,当我读它时,您将基于 FieldOfSerializableClass 的hashCode在父对象上。这似乎是你的问题的最终原因和一个非常可疑的设计。 hashCode() equals()方法处理对象标识,并不应该与父类包含它们的东西有关。对象的身份根据哪个父对象拥有它而改变的想法对我而言至少是非常陌生的,并且是您的代码无法正常工作的最终原因。



尽管其他答案有一些方法可以解决这个问题,但我认为解决这个问题的最简单方法是给它们自己的标识 FieldOfSerializableClass 类。您可以将 mHashCodeField SerializableClass 复制到 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 类指派一些排序标识是最好的模式。



您可以在类中使用 UUID.randomUUID()或某些静态 AtomicInteger 每次如果您不能使用 FieldOfSerializableClass 中的其他字段作为正确的标识。但我会使用从Hibernate给你的自动生成的ID。您只需确保该对象已被插入数据库之后将其放入另一个对象的集合中。


My project is a java project over EJB3 using Hibernate and Weblogic server.

For convenience sake (and as far as I understand, is typical to hibernate), some of the entities contains circular dependency (Parent knows the child, child know the parent). Further, for some of the child classes - the hashCode() and equals() method depend on their parent (As it is a unique key).

When working I saw an odd behavior - Some of the Sets that returned from the server to the client, although containing the right elements, acted like they contained none. For example, a simple test such as this: set.contains(set.toArray()[0]) returned false although the hashCode() method is a good one.

After extensive debugging I was able to produce 2 simple classes that reproduce the problem (I can assure you the hashCode() function in both classes is reflexive, transitive and symmetrical):

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

}

This produced the following output:

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

解决方案

So as I read it, you are basing the hashCode of FieldOfSerializableClass on the parent object. This seems to be the ultimate cause of your problem and a very questionable design. hashCode() and equals() method deal with object identity and should not at all be related to what parent contains them. The idea that the identity of an object changes depending on which parent object owns it is very foreign to me at least and is the ultimate reason why your code doesn't work.

Although the other answers have some ways to work around the problem, I think the easiest way to fix this is to give the FieldOfSerializableClass class its own identity. You could copy the mHashCodeField from the SerializableClass to the FieldOfSerializableClass. When the parent is set on the object you can take its mHashCodeField and store it locally.

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

Then the hashcode (and equals) method looks similar to the one for SerializableClass.

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

But really you should consider changing the code so the parental relationship is less coupled. Consider for a second what happens if you call setParentLink() on a field while it is already in another SerializableClass set. All of a sudden the original class can't even find the item in its set since its identity has changed. Assigning some sort identity to the FieldOfSerializableClass class that is unique from the parent class is the best pattern here in terms of Java objects.

You could use UUID.randomUUID() or some static AtomicInteger on the class that give a new id each time if you can't use the other fields in FieldOfSerializableClass as a proper identity. But I'd use the auto-generated id given to you from Hibernate. You just need to make sure that the object has been inserted into the database before it gets put in another object's collection.

这篇关于Java在使用Set时面临循环依赖时的序列化错误的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆