在处理 POC 时,我们注意到特定 EJB 调用的返回值无法由较新的 Java 版本反序列化。
我已经成功创建了一个简化的重现器,其中仅包含尽可能简化的重现问题所需的类。可以在
在这里找到该复制器。
如果按照重现器中的方式构造特定对象图,并且当 Airport 对象是根时将其序列化为文件,则反序列化会失败并出现以下 NPE:
java.lang.NullPointerException
at edu.gozke.BaseCodeObject.hashCode(BaseCodeObject.java:57)
at java.base/java.util.HashMap.hash(HashMap.java:340)
at java.base/java.util.HashMap.put(HashMap.java:608)
at java.base/java.util.HashSet.readObject(HashSet.java:343)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at java.base/java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1046)
at java.base/java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2357)
at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2228)
at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1687)
at java.base/java.io.ObjectInputStream.defaultReadFields(ObjectInputStream.java:2496)
at java.base/java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2390)
at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2228)
at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1687)
at java.base/java.io.ObjectInputStream.defaultReadFields(ObjectInputStream.java:2496)
at java.base/java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2390)
at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2228)
at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1687)
at java.base/java.io.ObjectInputStream.defaultReadFields(ObjectInputStream.java:2496)
at java.base/java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2390)
at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2228)
at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1687)
at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:489)
at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:447)
at edu.gozke.SerializationTest.readObject(SerializationTest.java:47)
at edu.gozke.SerializationTest.deserializationFailsIfAirportIsTheRoot(SerializationTest.java:26)
相同的代码在 Java 8 上运行良好(不会抛出 NPE)。从 java 9 开始,出现上述错误。请参阅链接的存储库以了解其他尝试过的 JDK 版本。我对这个问题的疑问是:
assert
语句)
public class City extends BaseCodeObject {
private final Set<Airport> airports = new HashSet<>();
City(String iataCityCode) {
super(iataCityCode);
}
public void addAirport(Airport airport) {
airports.add(airport);
}
}
public abstract class BaseCodeObject implements Serializable, Comparable<BaseCodeObject> {
protected String code;
protected int hash;
protected BaseCodeObject(String code) {
assert (code != null);
this.code = code;
}
public final String getCode() {
return code;
}
@Override
public int compareTo(BaseCodeObject obj) {
return code.compareTo(obj.code);
}
public boolean equals(Object otherObject) {
if (otherObject == this) {
return true;
}
if (otherObject == null) {
return false;
}
if (!(otherObject instanceof BaseCodeObject)) {
return false;
}
BaseCodeObject otherBaseObject = (BaseCodeObject) otherObject;
return code.equals(otherBaseObject.code);
}
@Override
public int hashCode() {
if (hash == 0) {
hash = code.hashCode();
}
return hash;
}
}
public class Airport extends CodedBaseDataContainer {
Airport(String iataAirportCode) {
super(iataAirportCode);
}
void addAirportData(AirportData data) {
addBaseData(data);
}
}
public abstract class CodedBaseDataContainer extends BaseCodeObject {
private final static PeriodComparator COMPARATOR = new PeriodComparator();
protected final TreeMap<String, CodedPeriodBaseData> dataMap = new TreeMap<>(COMPARATOR);
protected CodedBaseDataContainer(String code) {
super(code);
}
protected final void addBaseData(CodedPeriodBaseData baseData) {
dataMap.put(baseData.getEffectivePeriod(), baseData);
}
private static class PeriodComparator implements Comparator<String>, Serializable {
public int compare(String period1, String period2) {
return period1.compareTo(period2);
}
}
}
public class AirportData extends CodedPeriodBaseData {
private final City city;
AirportData(Airport airport, City city, String effectivePeriod) {
super(airport, effectivePeriod);
this.city = city;
airport.addAirportData(this);
}
}
public abstract class CodedPeriodBaseData extends BaseCodeObject {
private final String effectivePeriod;
protected final CodedBaseDataContainer container;
protected CodedPeriodBaseData(CodedBaseDataContainer container, String effectivePeriod) {
super(container.getCode());
this.container = container;
this.effectivePeriod = effectivePeriod;
}
public String getEffectivePeriod() {
return effectivePeriod;
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (o == null) {
return false;
}
if (!(o instanceof CodedPeriodBaseData)) {
return false;
}
CodedPeriodBaseData data = (CodedPeriodBaseData) o;
return code.equals(data.code) && effectivePeriod.equals(data.effectivePeriod);
}
@Override
public int hashCode() {
if (hash == 0) {
hash = 17;
hash = 37 * hash + code.hashCode();
hash = 37 * hash + effectivePeriod.hashCode();
}
return hash;
}
@Override
public int compareTo(BaseCodeObject obj) {
CodedPeriodBaseData data = (CodedPeriodBaseData) obj;
int cmpCode = code.compareTo(data.code);
if (cmpCode != 0) {
return cmpCode;
}
return effectivePeriod.compareTo(data.effectivePeriod);
}
}
还有有问题的重现方法:
public void deserializationFailsIfAirportIsTheRoot() throws ClassNotFoundException, IOException {
City city = new City("BB");
Airport airport = new Airport("BB");
city.addAirport(airport);
new AirportData(airport, city, "dummyKey");
writeToFile(airport, "airport");
Object object = readObject("airport");
System.out.println(object.hashCode());
}
private static final Object readObject(String fileName) throws IOException, ClassNotFoundException {
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fileName))) {
return ois.readObject();
}
}
private static void writeToFile(Object obj, String fileName) throws FileNotFoundException, IOException {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(fileName))) {
oos.writeObject(obj);
}
}
answer,我开始探索另一个 StackOverflow 问题,它让我陷入了困境,在 openjdk 存储库中找到了引入此行为的提交。
此提交就在这里是引入相关更改的地方。从相关JBS票据的描述来看,作者并没有意识到这种副作用。
我找不到任何预构建的二进制文件来证明此提交“破坏”了反序列化,因此我决定在本地构建两个版本。一个包含中断提交 (e11aec59a2705854ded7eaf2103d9cde57d81652)
(A),另一个来自该提交之前的版本
(a1e2230a4045afb7930f95dca6a351339403f41d)
(B)。 重现器在构建 A 时抛出 NPE,但在构建 B 时工作正常。在研究过程中,我的队友发现这个问题已经在 JBS 中至少报告过两次,但有一段时间没有被提及。这些问题是:
基于 JDK-8199664 中提到的解决方法,如果我使用以下方法扩展
BaseCodeObject
类,则反序列化工作正常。
private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
}
这样我原来的问题就得到了解答。