一、问题描述
对象输出流 (ObjectOutputStream) 以追加方式向文件序列化保存对象时,可以使用文件输出流 (FileOutputStream) 作为构造器参数,通过 writeObject() 方法向文件中写入对象信息,如下示例代码所示:
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("people.txt", true));
oos.writeObject(new People("Tom", 'M', 21));
第一次通过对象输出流序列化保存对象,而后反序列化读取数据时正常读取不会报错。当第二次追加写入对象信息,再次通过对象输入流(ObjectInputStream)反序列化读取第二次追加写入的对象信息时,就会抛出 java.io.StreamCorruptedException: invalid type code: AC 的异常。详细问题如下示例代码所示:
- People.java
import java.io.Serializable;
/**
* People.java
*
* @author Spring-_-Bear
* @version 2021-10-30 22:45
*/
public class People implements Serializable {
private static final long serialVersionUID = -2047975638001087437L;
private final String name;
private final Character sex;
private final Integer age;
public People(String name, Character sex, Integer age) {
this.name = name;
this.sex = sex;
this.age = age;
}
@Override
public String toString() {
return "People{" +
"name='" + name + '\'' +
", sex=" + sex +
", age=" + age +
'}';
}
}
- AppendSaveObject.java
import java.io.*;
/**
* AppendSaveObject.java
*
* @author Spring-_-Bear
* @version 2021-10-30 22:45
*/
public class AppendSaveObject {
private static final String FILE_PATH = "people.txt";
public static void main(String[] args) throws IOException, ClassNotFoundException {
File file = new File(FILE_PATH);
if (file.exists() && file.isFile()) {
if (file.delete()) {
System.out.println("------------------ " + FILE_PATH + " 文件删除成功 ------------------\n");
}
}
// 初次序列化保存 2 个对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH, true));
oos.writeObject(new People("Tom", 'M', 21));
oos.writeObject(new People("Smith", 'M', 22));
oos.close();
// 初次反序列化读取已保存的对象信息,正常读取
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
System.out.println("第一次读取到的的对象信息:" + ois.readObject());
System.out.println("第一次读取到的的对象信息:" + ois.readObject());
ois.close();
// 第二次追加写入 2 个对象信息
oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH, true));
oos.writeObject(new People("Mary", 'M', 21));
oos.writeObject(new People("Jack", 'M', 20));
oos.close();
// 再次创建对象输入流读取对象信息
ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
// 以下两行代码读取第一次添加的对象信息,正常输出
System.out.println("第二次读取到的的对象信息:" + ois.readObject());
System.out.println("第二次读取到的的对象信息:" + ois.readObject());
// 当试图读取第二次添加的两个新对象信息时,会抛异常 java.io.StreamCorruptedException: invalid type code: AC
System.out.println("第二次读取到的的对象信息:" + ois.readObject());
System.out.println("第二次读取到的的对象信息:" + ois.readObject());
ois.close();
}
}
示例代码运行情况如下图所示:
二、原因分析
调试程序跟踪源码,找出异常抛出位置为 ObjectInputStream 类的 readObject0(boolean unshared) 方法的 1721 行代码,如下图:
查看 ObjectOutputStream 的构造器源码可知,每次序列化创建 ObjectOutputStream 类的对象时都会在构造器中调用 writeStreamHeader() 方法写入流头的信息。
而反序列化创建 ObjectInputStream 类的对象的时候会在构造器中读取流头的信息。
invalid type code: AC 问题出现在哪呢?分析问题描述中给出的源码执行流程可以发现:当第一次创建输出流写入对象信息前,写入了一次流头信息。再次创建输出流写入对象前,又写入了一次流头信息,一共写入了两个流头信息。而创建对象输入流时只能读取到第一次写入的流头信息,因而在尝试读取第二次追加保存的对象信息时,由于未读取文件中第二次写入的流头信息而直接读取对象,从而抛出了异常。
三、解决方案
如此看来似乎找到了问题关键所在,即每创建一个对象输出流对象都会写入一次流头信息,而创建对象输入流读取文件中的对象信息时只能读取到第一次添加的流头信息,当尝试读取后续追加的对象而读取到流头信息时就会抛出异常。对象读取流程描述如下所示:
StreamHeader(对象输入流只能读到这个 StreamHeader)
Tom 对象信息
Smith 对象信息
StreamHeader(当对象输入流读到这个 StreamHeader 时就会抛出异常)
Mary 对象信息
Jack 对象信息
······
从上面的分析自然想到了问题的解决方案:
- 不写入流信息
- 只写入一次流信息
本文采用后者作为问题解决方案。通过自定义类 AppendObjectOutputStream 继承 ObjectOutputStream 类,并重写父类的 writeStreamHeader() 方法,当创建 AppendObjectOutputStream 类的对象时进行流程控制,以达到文件中只存在一个 StreamHeader 的目的。AppendObjectOutputStream 类的源码如下:
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
/**
* AppendObjectOutputStream.java: 解决 java.io.StreamCorruptedException: invalid type code: AC 异常
*
* @author Spring-_-Bear
* @version 2021-10-30 21:48
*/
public class AppendObjectOutputStream extends ObjectOutputStream {
private static File file;
public static void initFile(File file) {
AppendObjectOutputStream.file = file;
}
public AppendObjectOutputStream(File file) throws IOException {
super(new FileOutputStream(file, true));
}
@Override
public void writeStreamHeader() throws IOException {
// 如果文件为空直接写入 StreamHeader
if (file == null || file.length() == 0) {
super.writeStreamHeader();
} else {
// 文件中存在内容,则说明文件中已经存在了一个 StreamHeader,调用父类的 reset() 方法保证文件中只存在一个 StreamHeader
this.reset();
}
}
}
修改问题描述中 AppendSaveObject.java 文件代码,将原来自己创建对象输出流的代码修改为创建 AppendObjectOutputStream 对象,从而保证文件中只存在一个 StreamHeader,进而解决 invalid type code: AC 异常问题。
需要注意的问题是在创建 AppendObjectOutputStream 类的实例之前需要调用其 initFile() 静态方法初始化 file 对象,避免创建其实例动态绑定到 writeStreamHeader() 方法时 file 对象始终为 null。
修改后的代码如下:
import java.io.*;
/**
* AppendSaveObject.java
*
* @author Spring-_-Bear
* @version 2021-10-30 22:45
*/
public class AppendSaveObject {
private static final String FILE_PATH = "people.txt";
public static void main(String[] args) throws IOException, ClassNotFoundException {
File file = new File(FILE_PATH);
if (file.exists() && file.isFile()) {
if (file.delete()) {
System.out.println("------------------ " + FILE_PATH + " 文件删除成功 ------------------\n");
}
}
// Important: 在创建 ObjectOutputStream 类的实例前先初始化该类的文件信息
AppendObjectOutputStream.initFile(file);
// new ObjectOutputStream(new FileOutputStream(FILE_PATH, true)) => new AppendObjectOutputStream(file)
ObjectOutputStream oos = new AppendObjectOutputStream(file);
oos.writeObject(new People("Tom", 'M', 21));
oos.writeObject(new People("Smith", 'M', 22));
oos.close();
// 初次反序列化读取已保存的对象信息,正常读取
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
System.out.println("第一次读取到的的对象信息:" + ois.readObject());
System.out.println("第一次读取到的的对象信息:" + ois.readObject());
ois.close();
// new ObjectOutputStream(new FileOutputStream(FILE_PATH, true)) => new AppendObjectOutputStream(file)
oos = new AppendObjectOutputStream(file);
oos.writeObject(new People("Mary", 'M', 21));
oos.writeObject(new People("Jack", 'M', 20));
oos.close();
// 再次创建对象输入流读取对象信息
ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
// 以下两行代码读取第一次添加的对象信息,正常输出
System.out.println("第二次读取到的的对象信息:" + ois.readObject());
System.out.println("第二次读取到的的对象信息:" + ois.readObject());
// 当试图读取第二次添加的两个新对象信息时,会抛异常 java.io.StreamCorruptedException: invalid type code: AC
System.out.println("第二次读取到的的对象信息:" + ois.readObject());
System.out.println("第二次读取到的的对象信息:" + ois.readObject());
ois.close();
}
}
修改后的代码运行结果如下图所示,可见异常问题得到了完美的解决。
综上所述,通过自定义的类 AppendOutputStream 继承自 ObjectOutputStream, 并且重写了父类的 writeStreamHeader() 方法以保证文件中只存在一个 StreamHeader,从而 java.io.StreamCorruptedException: invalid type code: AC 异常抛出问题得到解决。