Java serialization bug when facing circular dependency with a Set

[亡魂溺海] 提交于 2019-12-22 04:43:34

问题


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

回答1:


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.




回答2:


Deserailization reads values of both fields (mHashCodeField and mSomeSet) into temporary array and after both values are deserialized it sets the fields to the stored values.

Since HashSet recalculates hash codes of its elements during deserialization it will use mHashCodeField when it is still null.

Possible solution is to mark mSomeSet as transient and write/read it in writeObject/readObject.

@SuppressWarnings("unchecked")
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException
{
    System.out.println("Just started deserializing");
    in.defaultReadObject();
    mSomeSet=(Set<FieldOfSerializableClass>)in.readObject();
    System.out.println("Just finished deserializing");        
}
private void writeObject(java.io.ObjectOutputStream out) throws IOException
{
    System.out.println("Just started serializing");
    out.defaultWriteObject();
    out.writeObject(mSomeSet);
    System.out.println("Just finished serializing");        
}



回答3:


It is the equals method that has to be reflexive, transitive and symmetrical...

The hashCode method must have these properties:

The general contract of hashCode is:

Whenever it is invoked on the same object more than once during an execution of a Java application, the hashCode method must consistently return the same integer, provided no information used in equals comparisons on the object is modified. This integer need not remain consistent from one execution of an application to another execution of the same application.

If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result.

It is not required that if two objects are unequal according to the equals(java.lang.Object) method, then calling the hashCode method on each of the two objects must produce distinct integer results. However, the programmer should be aware that producing distinct integer results for unequal objects may improve the performance of hashtables.

Here, it looks like the hashCode used to put the entry in the set during deserialization is different from the one calculated during the contains(). BTW as you noticed the entry is in the Set you just cannot access it through it's hashCode, if you loop over the content of the Set you will find the elements.

Possible solutions:

  • have a hashCode not relying on the parent object.
  • use a datastructure that is not using hashcode (List, TreeSet...)
  • do not use the contains method on the Set...
  • implement ReadResolve to recreate the Set after desirialization...

[EDIT]: It looks like you are not alone bug_id=4957674




回答4:


I add another answer because it's very different from my first one:

Here is an implementation that works without a transient field, I found the necessary information here: Advanced Serialization and here.

By the way I also tried to use the serialPersistentFields attribute to force that mHashCodeFields gets serialized first but it didn't help...

    public static class SerializableClass implements Serializable {

    // this tells the serialization mechanism to serialize only mHasCodeField...
    private final static ObjectStreamField[]
            serialPersistentFields = {
              new ObjectStreamField(
              "mHashCodeField", String.class)
            };


    private String mHashCodeField;
    private Set<FieldOfSerializableClass> mSomeSet;


    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 writeObject(java.io.ObjectOutputStream out)
            throws IOException, ClassNotFoundException {
        System.out.println("Just started serializing");
        out.defaultWriteObject();
        out.writeObject(mSomeSet);


        System.out.println("In writeObject - value of mHashCodeField: "
                + mHashCodeField);
        System.out.println("Just finished serializing");
    }

    private void readObject(java.io.ObjectInputStream in)
            throws IOException, ClassNotFoundException {
        System.out.println("Just started deserializing");
        in.defaultReadObject();
        mSomeSet=(Set<FieldOfSerializableClass>)in.readObject();

        System.out.println("In readObject - value of mHashCodeField: "
                + mHashCodeField);
        System.out.println("Just finished deserializing");
    }
}



回答5:


Indeed Hibernate says not to use the id as hashcode, but I believe they're being too strict about it. This makes sense only if the id is autogenerated/autoincremented by Hibernate. In this case, you might have a bean that receives its id value only when Hibernate decides to actually persist it to the database, so in this situation you might get unpredictable behaviour from a hashcode and/or equals method that uses the id. However if the id is manually set, i.e. your application deals with populating this value then I believe it's perfectly ok to use it in your hashcode/equals methods. Is this the case for you?




回答6:


Seems to me this is a bug in java, not your sourcecode. While the answers above give good workaround options, the best solution would be for Java to fix how deserialization works to account for circular references and sets/hashmaps.

See here for making a new bug report: http://bugreport.sun.com/bugreport/

The more people who report this bug, the greater chance they will fix it. I too am getting this error in my project, and the work-arounds are far more effort than they are worth to me.

Also, here is a similar bug report I found: http://bugs.sun.com/view_bug.do;jsessionid=fb27da16bb769ffffffffebce29d31b2574e?bug_id=6208166




回答7:


I came across the same issue. I think you're right in your second edit about the cause. Here is my simplest replication of the issue:

public class Test {

    static class Thing implements Serializable {
        String name;
        Set<Thing> others = new HashSet<Thing>();

        @Override
        public int hashCode() {
            if (name == null) {
                System.out.println("hashcode called with null name!");
            }
            return name == null ? 0 : name.hashCode();
        }

        @Override
        public boolean equals(Object o) {
            return o instanceof Thing && ((Thing) o).name == name;
        }
    }

    @org.junit.Test
    public void testHashSetCircularDependencySerialization() throws Exception {
        Thing thing = new Thing();
        thing.name = "thing";
        Thing thing2 = new Thing();
        thing2.name = "thing2";
        thing.others.add(thing2);
        thing2.others.add(thing);
        assertTrue(thing2.others.contains(thing));
        Thing thingCopy = (Thing) serializeAndDeserialize(thing);
        Thing thing2Copy = thingCopy.others.iterator().next();
        assertTrue(thing2Copy.others.contains(thingCopy));
    }

    public static Object serializeAndDeserialize(Object other) throws Exception {
        ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
        new ObjectOutputStream(byteOutputStream).writeObject(other);
        ByteArrayInputStream byteInputStream = new ByteArrayInputStream(byteOutputStream.toByteArray());
        return new ObjectInputStream(byteInputStream).readObject();
    }
}

Output:

hashcode called with null name!

This test fails. The simplest solution I found was to keep a copy of the hashcode. Because it's a primative, it gets set when the object is initialized during deserialization, not later:

    int hashcode;

    @Override
    public int hashCode() {
        if (hashcode != 0) {
            return hashcode;
        }
        hashcode = name == null ? 0 : name.hashCode();
        return hashcode;
    }

The test now passes.



来源:https://stackoverflow.com/questions/7901006/java-serialization-bug-when-facing-circular-dependency-with-a-set

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!