1. 引言
在 Java 的 I/O 体系中,ObjectInputStream是一个非常重要的类。它负责将之前通过ObjectOutputStream序列化写入的字节流,重新还原为 Java 对象。这个过程被称为反序列化(Deserialization)。
无论是本地缓存、网络传输,还是分布式系统中的 RPC 调用,ObjectInputStream都在背后默默承担着数据还原的重任。本文将带你从零开始,系统掌握ObjectInputStream的核心用法、底层原理以及必须警惕的安全陷阱。
2. 什么是 ObjectInputStream
ObjectInputStream位于java.io包下,它继承自InputStream,是一个对象输入流。它的核心职责是:读取字节流,并将其反序列化为 Java 对象。
publicclassObjectInputStreamextendsInputStreamimplementsObjectInput,ObjectStreamConstants它与ObjectOutputStream是成对出现的:
ObjectOutputStream:把 Java 对象序列化为字节流(写出去)。ObjectInputStream:把字节流反序列化为 Java 对象(读进来)。
2.1 核心构造方法
// 从输入流中创建 ObjectInputStreampublicObjectInputStream(InputStreamin)throwsIOException2.2 核心方法
| 方法 | 说明 |
|---|---|
readObject() | 读取并反序列化一个对象 |
readInt()/readUTF()等 | 读取基本数据类型 |
available() | 返回可读取的字节数 |
close() | 关闭流并释放资源 |
3. 基础用法:序列化与反序列化
3.1 准备一个可序列化的类
要让对象能够被序列化,必须实现Serializable接口:
importjava.io.Serializable;publicclassUserimplementsSerializable{privatestaticfinallongserialVersionUID=1L;privateStringname;privateintage;publicUser(Stringname,intage){this.name=name;this.age=age;}@OverridepublicStringtoString(){return"User{name='"+name+"', age="+age+"}";}}3.2 使用 ObjectOutputStream 写入
importjava.io.FileOutputStream;importjava.io.IOException;importjava.io.ObjectOutputStream;publicclassSerializeDemo{publicstaticvoidmain(String[]args){Useruser=newUser("张三",25);try(ObjectOutputStreamoos=newObjectOutputStream(newFileOutputStream("user.dat"))){oos.writeObject(user);System.out.println("对象已序列化到 user.dat");}catch(IOExceptione){e.printStackTrace();}}}3.3 使用 ObjectInputStream 读取
importjava.io.FileInputStream;importjava.io.IOException;importjava.io.ObjectInputStream;publicclassDeserializeDemo{publicstaticvoidmain(String[]args){try(ObjectInputStreamois=newObjectInputStream(newFileInputStream("user.dat"))){Useruser=(User)ois.readObject();System.out.println("反序列化成功: "+user);}catch(IOException|ClassNotFoundExceptione){e.printStackTrace();}}}运行结果:
反序列化成功: User{name='张三', age=25}4. 底层原理:反序列化过程
ObjectInputStream的反序列化过程远比想象中复杂,其核心流程如下:
4.1 关键步骤说明
- 读取流头:
ObjectInputStream会先读取两个字节的魔数0xACED和版本号0x0005,用于校验流格式。 - 读取类描述:读取类的全限定名、
serialVersionUID以及字段描述符。 - 创建实例:通过
sun.reflect.ReflectionFactory.newConstructorForSerialization()创建对象,不会调用构造函数。 - 恢复字段:根据类描述中的字段信息,逐个恢复字段值。
- 回调钩子:如果类中定义了
readObject()或readResolve()方法,会在此阶段被调用。
4.2 serialVersionUID 的作用
serialVersionUID是序列化版本号,用于验证反序列化时的类与序列化时的类是否兼容:
privatestaticfinallongserialVersionUID=1L;- 如果序列化和反序列化时的
serialVersionUID不一致,会抛出InvalidClassException。 - 建议显式声明
serialVersionUID,避免因类结构微调导致反序列化失败。
5. 高级特性
5.1 transient 关键字
被transient修饰的字段不会被序列化:
publicclassUserimplementsSerializable{privatestaticfinallongserialVersionUID=1L;privateStringname;privatetransientStringpassword;// 不会被序列化}5.2 自定义 readObject
可以在类中自定义readObject()方法,控制反序列化过程:
privatevoidreadObject(ObjectInputStreamois)throwsIOException,ClassNotFoundException{// 先调用默认反序列化ois.defaultReadObject();// 自定义校验逻辑if(age<0||age>150){thrownewInvalidObjectException("年龄不合法: "+age);}}5.3 readResolve 单例保护
对于单例类,反序列化会破坏单例特性。通过readResolve()可以解决:
publicclassSingletonimplementsSerializable{privatestaticfinalSingletonINSTANCE=newSingleton();privateSingleton(){}publicstaticSingletongetInstance(){returnINSTANCE;}// 反序列化时返回同一个实例privateObjectreadResolve(){returnINSTANCE;}}6. 安全陷阱:反序列化漏洞
ObjectInputStream最大的风险在于反序列化漏洞。攻击者可以构造恶意字节流,在readObject()时触发任意代码执行。
6.1 漏洞原理
当程序对不可信的字节流调用readObject()时,攻击者可以利用 Java 原生库中的Gadget链(如CommonsCollections)构造恶意对象,在反序列化过程中执行系统命令。
6.2 防护措施
方案一:使用安全的对象过滤(JEP 290)
JDK 9+ 提供了ObjectInputFilter,可以限制允许反序列化的类:
ObjectInputFilterfilter=ObjectInputFilter.Config.createFilter("com.example.*;java.lang.*;!*");try(ObjectInputStreamois=newObjectInputStream(newFileInputStream("data.bin"))){ois.setObjectInputFilter(filter);Objectobj=ois.readObject();}方案二:使用白名单校验
privatestaticfinalSet<String>WHITE_LIST=Set.of("com.example.User","java.lang.String");privatevoidvalidateClass(Class<?>clazz)throwsInvalidClassException{if(!WHITE_LIST.contains(clazz.getName())){thrownewInvalidClassException("禁止反序列化的类: "+clazz.getName());}}方案三:使用安全的序列化框架
- Fastjson2:内置
autoType安全校验。 - Jackson:通过
activateDefaultTyping配合白名单。 - Kryo:默认不信任任意类。
6.3 最佳实践
- 永远不要对不可信数据直接调用
readObject()。 - 使用
ObjectInputFilter设置类白名单。 - 优先使用 JSON 等文本格式替代 Java 原生序列化。
- 对敏感数据加密传输。
7. 常见异常与排查
| 异常 | 原因 | 解决方案 |
|---|---|---|
InvalidClassException | serialVersionUID不匹配 | 显式声明serialVersionUID |
ClassNotFoundException | 类不存在或类加载器问题 | 检查类路径 |
StreamCorruptedException | 流头校验失败 | 确认数据来源正确 |
OptionalDataException | 读取顺序与写入不一致 | 按写入顺序读取 |
EOFException | 提前到达流末尾 | 检查数据完整性 |
8. 总结
ObjectInputStream是 Java 反序列化的核心入口,掌握它需要理解三个层面:
- 用法层面:与
ObjectOutputStream配对使用,实现对象的持久化与传输。 - 原理层面:理解流头校验、类描述读取、实例创建(不调用构造器)等底层机制。
- 安全层面:警惕反序列化漏洞,使用
ObjectInputFilter白名单机制防护。
在实际开发中,建议优先考虑 JSON 等跨语言格式;若必须使用 Java 原生序列化,务必做好安全防护。