Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java serialization bug when facing circular dependency with a Set

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;
        }
    }
}
like image 848
Ran Avatar asked Oct 26 '11 09:10

Ran


1 Answers

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.

like image 197
Gray Avatar answered Sep 30 '22 14:09

Gray