Java ObjectInputStream 详解:从基础用法到反序列化安全
2026/9/6 10:03:58 网站建设 项目流程

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)throwsIOException

2.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的反序列化过程远比想象中复杂,其核心流程如下:

普通对象

数组/枚举/字符串

读取字节流

解析流头 magic & version

读取对象描述信息

判断对象类型

创建实例(不调用构造器)

特殊处理

恢复字段值

回调 readObject / readResolve

返回对象

4.1 关键步骤说明

  1. 读取流头ObjectInputStream会先读取两个字节的魔数0xACED和版本号0x0005,用于校验流格式。
  2. 读取类描述:读取类的全限定名、serialVersionUID以及字段描述符。
  3. 创建实例:通过sun.reflect.ReflectionFactory.newConstructorForSerialization()创建对象,不会调用构造函数
  4. 恢复字段:根据类描述中的字段信息,逐个恢复字段值。
  5. 回调钩子:如果类中定义了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 最佳实践

  1. 永远不要对不可信数据直接调用readObject()
  2. 使用ObjectInputFilter设置类白名单。
  3. 优先使用 JSON 等文本格式替代 Java 原生序列化。
  4. 对敏感数据加密传输。

7. 常见异常与排查

异常原因解决方案
InvalidClassExceptionserialVersionUID不匹配显式声明serialVersionUID
ClassNotFoundException类不存在或类加载器问题检查类路径
StreamCorruptedException流头校验失败确认数据来源正确
OptionalDataException读取顺序与写入不一致按写入顺序读取
EOFException提前到达流末尾检查数据完整性

8. 总结

ObjectInputStream是 Java 反序列化的核心入口,掌握它需要理解三个层面:

  1. 用法层面:与ObjectOutputStream配对使用,实现对象的持久化与传输。
  2. 原理层面:理解流头校验、类描述读取、实例创建(不调用构造器)等底层机制。
  3. 安全层面:警惕反序列化漏洞,使用ObjectInputFilter白名单机制防护。

在实际开发中,建议优先考虑 JSON 等跨语言格式;若必须使用 Java 原生序列化,务必做好安全防护。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询