如何恢复未正常关闭ObjectOutputStream的序列化文件?
我们的应用使用ObjectOutputStream写入文件时,曾存在流偶尔未正常关闭的Bug,导致后续通过ObjectInputStream读取该文件时会抛出EOFException。目前Bug已修复,但受影响的用户仍需重建文件,想知道是否存在无需重建即可恢复文件的方法?
我尝试创建新的输出流来关闭该受损文件,但读取时仍会出现另一种EOFException。通过以下步骤模拟问题:调用load()创建文件、save()插入数据、save_damage()执行未关闭的写入操作,此时读取抛出如下异常:
java.io.EOFException: Unexpected end of ZLIB input stream at java.util.zip.InflaterInputStream.fill(InflaterInputStream.java:240) at java.util.zip.InflaterInputStream.read(InflaterInputStream.java:158) at java.util.zip.GZIPInputStream.read(GZIPInputStream.java:117) at java.io.ObjectInputStream$PeekInputStream.read(ObjectInputStream.java:2779) at java.io.ObjectInputStream$PeekInputStream.readFully(ObjectInputStream.java:2795) at java.io.ObjectInputStream$BlockDataInputStream.readShort(ObjectInputStream.java:3272) at java.io.ObjectInputStream.readStreamHeader(ObjectInputStream.java:932) at java.io.ObjectInputStream.<init>(ObjectInputStream.java:394) at TestProgram.load(TestProgram.java:47) at TestProgram.main(TestProgram.java:24)
这模拟出了我们曾遇到的Bug。现在希望找到无需重建即可恢复配置文件的方法,尝试在启动时调用close_damaged_file()修复,但取消注释该代码后读取又抛出如下异常:
java.io.EOFException at java.io.ObjectInputStream$BlockDataInputStream.peekByte(ObjectInputStream.java:3076) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1616) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:501) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:459) at TestProgram.load(TestProgram.java:48) at TestProgram.main(TestProgram.java:19)
测试代码如下:
import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; public class TestProgram { private final static String FILE_PATH = "./config.ser"; private static String config; public static void main(String[] args) { try { // close_damaged_file(); load(); save("Test Data"); load(); System.out.println(config); save_damage("Damaged Data"); load(); } catch(Exception e) { e.printStackTrace(System.out); return; } System.out.println(config); } public static boolean load() throws IOException, ClassNotFoundException { File file = new File(FILE_PATH); if (!file.exists()) { file.createNewFile(); save(""); file.setReadable(true, false); //set authority to 666 file.setWritable(true, false); file.setExecutable(false, false); } InputStream inputStream = null; GZIPInputStream gZipStream = null; ObjectInputStream objInputStream = null; try { inputStream = new BufferedInputStream(new FileInputStream(file)); gZipStream = new GZIPInputStream(inputStream); objInputStream = new ObjectInputStream(gZipStream); config = (String) objInputStream.readObject(); } finally { if (inputStream != null) inputStream.close(); if (gZipStream != null) gZipStream.close(); if (objInputStream != null) objInputStream.close(); } return true; } public static boolean save(String data) throws IOException { FileOutputStream outputStream = new FileOutputStream(FILE_PATH); GZIPOutputStream gZipStream = new GZIPOutputStream(outputStream); ObjectOutputStream objOutputStream = new ObjectOutputStream(gZipStream); objOutputStream.writeObject(data); objOutputStream.flush(); objOutputStream.close(); return true; } public static boolean save_damage(String data) throws IOException { FileOutputStream outputStream = new FileOutputStream(FILE_PATH); GZIPOutputStream gZipStream = new GZIPOutputStream(outputStream); ObjectOutputStream objOutputStream = new ObjectOutputStream(gZipStream); objOutputStream.writeObject(data); return true; } public static boolean close_damaged_file() throws IOException { FileOutputStream fOut= null; GZIPOutputStream gZip = null; ObjectOutputStream outStream = null; try { fOut = new FileOutputStream(FILE_PATH); gZip = new GZIPOutputStream(fOut); outStream = new ObjectOutputStream(gZip); outStream.flush(); } finally { outStream.close(); } return true; } }
首先明确:未正常关闭的GZIPOutputStream会导致文件缺少GZIP结束标记,同时ObjectOutputStream也可能未写完序列化的完整元数据。直接重新打开流关闭的方式行不通,因为这会覆盖原有文件的有效数据。
以下是可行的恢复思路:
1. 提取GZIP流中的有效数据并重建文件
GZIP格式损坏时,仍有可能读取到部分有效压缩内容。可以通过捕获读取时的EOFException,提取已成功读取的数据,再重新生成完整的GZIP文件:
public static byte[] extractValidData(File damagedFile) throws IOException { ByteArrayOutputStream validData = new ByteArrayOutputStream(); try (FileInputStream fis = new FileInputStream(damagedFile); GZIPInputStream gzis = new GZIPInputStream(fis)) { byte[] buffer = new byte[1024]; int readLen; // 读取直到抛出EOF异常,此时已读取的内容就是有效数据 while ((readLen = gzis.read(buffer)) != -1) { validData.write(buffer, 0, readLen); } } catch (EOFException e) { // 正常捕获,说明流到末尾但不完整,已读取的内容可用 } return validData.toByteArray(); } // 用提取的有效数据重建完整文件 public static void restoreConfig(byte[] validContent) throws IOException { try (FileOutputStream fos = new FileOutputStream(FILE_PATH); GZIPOutputStream gzos = new GZIPOutputStream(fos); ObjectOutputStream oos = new ObjectOutputStream(gzos)) { // 这里根据实际序列化的对象类型调整,示例中是String oos.writeObject(new String(validContent)); } }
2. 绕开GZIP直接尝试读取序列化数据
如果GZIP的头部未损坏,仅尾部缺失,可以尝试跳过GZIP层,直接读取序列化数据:
- GZIP头部固定为10字节,先跳过这部分
- 用
ObjectInputStream读取剩余字节,捕获EOFException,尽可能还原可读取的对象
注意:这种方法成功率不高,若序列化数据本身已不完整则无效,但可作为备选方案。
3. 后续版本的预防措施
虽然Bug已修复,可在后续版本中加入文件完整性校验:
- 每次写入文件后,额外保存文件的哈希值(如MD5)到单独的校验文件
- 读取前先校验哈希值,若不匹配则自动触发恢复逻辑或提示用户
最后说明:受损文件的恢复成功率取决于损坏程度,如果序列化核心数据已丢失,只能重建。但上述方法可挽救大部分未完全损坏的文件。
内容的提问来源于stack exchange,提问作者Andrew

