Java反序列化是Java开发中一个看似简单实则暗藏玄机的操作。我见过太多团队在这个环节栽跟头,特别是当处理空对象时,那些看似无害的null值往往成为系统崩溃的导火索。简单来说,序列化是把对象转换为字节流的过程,而反序列化则是将这些字节流重新构造成对象。这个过程在远程方法调用(RMI)、分布式缓存、会话持久化等场景中极为常见。
但问题在于,Java的序列化机制在设计上存在一些"历史包袱"。ObjectInputStream在反序列化时,会完全信任流中的数据,这意味着攻击者可以构造恶意字节流,在反序列化时执行任意代码。2015年爆发的Apache Commons Collections反序列化漏洞就是一个典型案例,影响了WebLogic、JBoss、WebSphere等主流中间件。
空对象的处理之所以特殊,是因为null在Java中既是对象缺失的表示,又可能是有意设计的业务状态。当反序列化遇到null时,不同版本的JDK、不同的序列化框架可能会有不同的行为表现。更棘手的是,某些框架在遇到null对象时,会静默失败而不是抛出异常,这种"沉默的杀手"在分布式系统中尤为危险。
考虑这样一个场景:一个User对象的address字段在序列化时为null,反序列化后应该保持null还是被初始化为空字符串?这个问题看似简单,但在实际业务中可能导致严重的不一致。我曾遇到一个电商系统,订单的收货地址反序列化后null变成了空字符串,导致地址校验逻辑失效。
java复制public class Order implements Serializable {
private String shippingAddress; // 可能为null
// 反序列化后 shippingAddress 的状态取决于序列化实现
}
集合类的反序列化对空对象的处理更加复杂。ArrayList允许包含null元素,但某些自定义集合可能不允许。当反序列化一个包含null的集合时,可能会遇到:
java复制List<String> list = Arrays.asList("item1", null, "item2");
// 序列化/反序列化后 list.get(1) 的行为可能出人意料
当父类字段为null而子类有默认值时,反序列化的行为会变得难以预测。特别是在使用JDK序列化时,如果父类没有正确实现序列化方法,子类的默认值可能会覆盖父类的null值。
java复制class Parent {
protected String value = null;
}
class Child extends Parent implements Serializable {
{ value = "default"; } // 实例初始化块
// 反序列化后 value 可能是 null 或 "default",取决于实现细节
}
永远不要信任外部的序列化数据。在反序列化前应该:
Java 9引入了ObjectInputFilter可以有效地限制反序列化的类:
java复制ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.example.*;!*");
ObjectInputStream ois = new ObjectInputStream(inputStream);
ois.setObjectInputFilter(filter);
对于关键类,应该实现自定义的readObject方法,严格控制反序列化过程:
java复制private void readObject(ObjectInputStream ois)
throws IOException, ClassNotFoundException {
// 首先执行默认反序列化
ois.defaultReadObject();
// 然后验证对象状态
if (this.sensitiveField == null) {
throw new InvalidObjectException("sensitiveField cannot be null");
}
// 对集合字段进行防御性拷贝
this.internalList = Collections.unmodifiableList(
new ArrayList<>(this.internalList));
}
针对空对象,建议采用以下策略之一:
示例代码:
java复制public <T> T deserialize(byte[] data, Class<T> type, T defaultValue) {
if (data == null) {
if (defaultValue != null) {
return defaultValue;
}
throw new IllegalArgumentException("Data is null and no default provided");
}
try {
Object obj = new ObjectInputStream(new ByteArrayInputStream(data)).readObject();
return type.cast(obj != null ? obj : defaultValue);
} catch (Exception e) {
throw new RuntimeException("Deserialization failed", e);
}
}
JDK的ObjectOutputStream/ObjectInputStream对null的处理最为直接:
Jackson提供了丰富的null处理配置:
java复制ObjectMapper mapper = new ObjectMapper();
// 序列化时忽略null
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
// 反序列化时null转为默认值
mapper.setDefaultPropertyInclusion(Include.NON_NULL);
Gson的null处理较为宽松:
java复制Gson gson = new GsonBuilder()
.serializeNulls() // 明确要求序列化null
.create();
// 反序列化时,JSON中的null会转为Java中的null
Protocol Buffers有严格的null约束:
在Redis等缓存系统中,null值可能被表示为:
这会导致反序列化时的行为不一致。建议方案:
java复制public <T> T getFromCache(String key, Class<T> type) {
byte[] data = redis.get(key.getBytes());
if (data == null) {
return null; // 键不存在
}
if (data.length == 0 || "NULL".equals(new String(data))) {
throw new CacheException("Invalid null value in cache");
}
return deserialize(data, type);
}
跨服务的null传递需要特别注意:
频繁的null检查会影响性能,特别是在序列化大量对象时。优化技巧:
示例优化代码:
java复制public void writeExternal(ObjectOutput out) throws IOException {
// 使用bitmask标记null字段
byte nullMask = 0;
if (field1 == null) nullMask |= 0x01;
if (field2 == null) nullMask |= 0x02;
out.writeByte(nullMask);
if (field1 != null) out.writeObject(field1);
if (field2 != null) out.writeObject(field2);
}
完善的测试应该包含:
JUnit示例:
java复制@Test
public void testDeserializeWithNulls() {
MyObject original = new MyObject(null, "non-null");
byte[] serialized = serialize(original);
MyObject deserialized = deserialize(serialized);
assertNull(deserialized.getNullableField());
assertEquals("non-null", deserialized.getRequiredField());
// 测试null对象整体
assertThrows(InvalidObjectException.class,
() -> deserialize(serialize(null)));
}
将反序列化漏洞扫描纳入CI流程:
在以下情况考虑替代方案:
java复制public class SafeContainer implements Serializable {
private transient List<String> items;
// 自定义序列化保证null安全
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject();
oos.writeInt(items != null ? items.size() : -1);
if (items != null) {
for (String item : items) {
oos.writeObject(item != null ? item : "");
}
}
}
private void readObject(ObjectInputStream ois)
throws IOException, ClassNotFoundException {
ois.defaultReadObject();
int size = ois.readInt();
this.items = size >= 0 ? new ArrayList<>(size) : null;
for (int i = 0; i < size; i++) {
items.add((String) ois.readObject());
}
}
}
在生产环境中:
java复制public class SafeObjectInputStream extends ObjectInputStream {
@Override
protected Object readObjectOverride() throws IOException {
try {
long start = System.nanoTime();
Object obj = super.readObjectOverride();
if (obj == null) {
log.warn("Deserialized null object");
}
metrics.recordDeserialization(System.nanoTime() - start);
return obj;
} catch (Exception e) {
log.error("Deserialization failed", e);
throw e;
}
}
}
在十多年的Java开发经历中,我发现反序列化问题就像房间里的大象——每个人都知道存在风险,却常常选择忽视。特别是对空对象的处理,看似简单实则暗藏杀机。最深刻的教训来自一次生产事故:一个为null的配置对象在反序列化后被静默替换为默认对象,导致全站配置错误。从那时起,我在所有涉及序列化的代码中都坚持三条原则:显式处理null、记录序列化边界、单元测试覆盖null场景。记住,在分布式系统中,null不是不存在,而是一种需要明确处理的状态。