Мой проект - это java
проект поверх EJB3
с использованием Hibernate и сервер Weblogic
.
Для удобства (и, насколько я понимаю, это типично для 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;
}
}
}
Это привело к следующему выводу:
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 до String, что вызывает указанную выше проблему.
Что мне делать в этой ситуации? Есть ли какой-либо вариант (помимо реализации readResolve
для многих сущностей ...), чтобы указать Java для сериализации класса в определенном порядке?
Кроме того, фундаментально неправильно для сущности базировать его hashCode
его родительского элемента?
Edit : решение было предложено коллегой - поскольку я использую Hibernate, каждая сущность имеет уникальный длинный идентификатор. Я знаю, что Hibernate указывает не использовать этот идентификатор в методе equals, но как насчет hashCode? Использование этого уникального идентификатора в качестве хэш-кода, похоже, решает указанную выше проблему с минимальным риском проблем с производительностью. Есть ли какие-либо другие последствия использования идентификатора в качестве хэш-кода?
ВТОРОЙ РЕДАКТИРОВАНИЕ : Я пошел и реализовал свое частичное решение (все объекты теперь используют поле идентификатора для функции hashCode () и больше не передают другие enteties для этого) но, увы, ошибки сериализации все еще продолжают меня преследовать! Ниже приведен пример кода с другой ошибкой сериализации. Я думаю, что происходит следующее: ClassA начинает десериализацию, видит, что у него есть ClassB для десериализации, и ДО того, как он десериализует свой идентификатор, он начинает десериализацию ClassB. B начинает десериализацию и видит, что у него есть набор ClassA. Экземпляр ClassA частично десериализован, но даже несмотря на то, что ClassB добавляет его в Set (используя отсутствующий идентификатор ClassA), десериализация завершается, ClassA затем завершается, и возникает ошибка.
Что я могу сделать, чтобы решить эту проблему ?! Круговые зависимости - очень распространенная практика в Hibernate, и я просто не могу согласиться с тем, что я единственный, у кого эта проблема.
Другое возможное решение - создать специальную переменную для hashCode (будет вычисляться по идентификатору объекта) и убедиться (просмотрите 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;
}
}
}