如何向现有的java.io.ObjectStream追加数据?

10

现在,当我尝试追加一个对象时,我会遇到java.io.StreamCorruptedException的问题。我已经在互联网上搜索了解决方法。目前找到的答案是无法解决该问题。解决这个问题的一个方法是将对象写入列表中,然后再将列表写入文件。

但是,每次添加新对象时,我必须覆盖该文件。在长时间内,这似乎不是最优解。

是否有一种方法可以将对象追加到现有的对象流中?


你能否使用数据库进行持久化? - Asaph
@danben:是的,问题是对象是否可以追加到文件中。 @Asaph:我可以这样做,但我想知道是否适用于对象追加。如果我的问题没有表述清楚,请原谅。 - starcorn
那是 ObjectOutputStream 吗? - OscarRyz
@Oscar:是的,ObjectInputStream 和 ObjectOutputStream。 - starcorn
你是如何添加这个对象的? - OscarRyz
@Oscar:使用ObjectOutputStream中的writeObject()方法。 - starcorn
4个回答

5

实际上这很容易做到。当您向现有流添加内容时,需要使用ObjectOutStream的子类来覆盖writeStreamHeader方法,以便在文件中间不会写入第二个标头。例如:

class NoHeaderObjectOutputStream extends ObjectOutputStream {
  public NoHeaderObjectOutputStream(OutputStream os) {
    super(os);
  }
  protected void writeStreamHeader() {}
}

然后只需使用标准的ObjectInputStream读取整个文件即可。

非常好,void write(byte[] b, int off, int len, boolean copy) 不幸使用了私有方法writeBlockHeader。 - stacker
+1 我在文档中读到了这个,但是(像我经常遇到的那样)我没有完全理解,直到现在。 - OscarRyz
@stacker 我不明白这会引起什么问题。该标头是序列化流格式的一部分,用于指示将下一个n个字节读取为单个字节数组,并在稍后消耗流时正确读取。 - Geoff Reedy
非常好的提示,Geoff。再一次,SO让我意识到了一种新技术。 - Kevin Day

5
我找到的关于这个主题的最好文章是:http://codify.flansite.com/2009/11/java-serialization-appending-objects-to-an-existing-file/ 覆盖ObjectOutputStream的“解决方案”是错误的。我刚刚完成了一项调查,发现一个由此引起的错误(浪费了两天宝贵的时间)。不仅有时会损坏序列化文件,甚至还能读取而不抛出异常,并在最后提供垃圾数据(混合字段)。对于那些不相信的人,我附上了一些代码以揭示问题:
import java.io.*;
import java.util.HashMap;
import java.util.Map;

public class Main {

    public static void main(String[] args) throws Exception {

        File storageFile = new File("test");
        storageFile.delete();

        write(storageFile, getO1());
        write(storageFile, getO2());
        write(storageFile, getO2());

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(storageFile));
        read(ois, getO1());
        read(ois, getO2());
        read(ois, getO2());
    }

    private static void write(File storageFile, Map<String, String> o) throws IOException {
        ObjectOutputStream oos = getOOS(storageFile);
        oos.writeObject(o);
        oos.close();
    }

    private static void read(ObjectInputStream ois, Map<String, String> expected) throws ClassNotFoundException, IOException {
        Object actual = ois.readObject();
        assertEquals(expected, actual);
    }

    private static void assertEquals(Object o1, Object o2) {
        if (!o1.equals(o2)) {
            throw new AssertionError("\n expected: " + o1 + "\n actual:   " + o2);
        }
    }

    private static Map<String, String> getO1() {
        Map<String, String> nvps = new HashMap<String, String>();
        nvps.put("timestamp", "1326382770000");
        nvps.put("length", "246");
        return nvps;
    }

    private static Map<String, String> getO2() {
        Map<String, String> nvps = new HashMap<String, String>();
        nvps.put("timestamp", "0");
        nvps.put("length", "0");
        return nvps;
    }

    private static ObjectOutputStream getOOS(File storageFile) throws IOException {
        if (storageFile.exists()) {
            // this is a workaround so that we can append objects to an existing file
            return new AppendableObjectOutputStream(new FileOutputStream(storageFile, true));
        } else {
            return new ObjectOutputStream(new FileOutputStream(storageFile));
        }
    }

    private static class AppendableObjectOutputStream extends ObjectOutputStream {

        public AppendableObjectOutputStream(OutputStream out) throws IOException {
            super(out);
        }

        @Override
        protected void writeStreamHeader() throws IOException {
            // do not write a header
        }
    }
}

正如文章所述,您可以使用以下解决方案之一:

Solution #1: Fake Multiple file in a Single Stream

...

Write your “transaction” to a ByteArrayOutputStream, then write the length and contents of this ByteArrayOutputStream to a file via the DataOutputStream.

Solution #2: Reopen and Skip

Another solution involves saving the file position using:

long pos = fis.getChannel().position();

closing the file, reopening the file, and skipping to this position before reading the next transaction.


这里有一对函数(用Scala编写),实现了解决方案#1。 - F. P. Freely

3
感谢 George Hategan 提供的问题暴露代码。我也仔细研究了一段时间。然后,我意识到一个问题。如果您正在使用一个子类化的 ObjectOutputStream,并覆盖 writeStreamHeader() 方法来写入数据,那么您必须使用相应的子类化 ObjectInputStream 并覆盖 readStreamHeader() 方法来读取数据。当然,我们可以在写入和读取对象时在不同的实现之间切换,但只要在写入/读取过程中使用相应的子类对 - 我们就会(希望)没问题。Tom。
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.io.OutputStream;
import java.util.HashMap;
import java.util.Map;

public class SerializationDemo {

    public static void main(String[] args) throws Exception {
        File storageFile = new File("test.ser");
        storageFile.delete();
        write(storageFile, getO1());
        write(storageFile, getO2());
        write(storageFile, getO2());
        FileInputStream fis = new FileInputStream(storageFile);
        read(fis, getO1());
        read(fis, getO2());
        read(fis, getO2());
        fis.close();
    }

    private static void write(File storageFile, Map<String, String> o)
                    throws IOException {
        ObjectOutputStream oos = getOOS(storageFile);
        oos.writeObject(o);
        oos.flush();
        oos.close();
    }

    private static void read(FileInputStream fis, Map<String, String> expected)
                    throws ClassNotFoundException, IOException {
        Object actual = getOIS(fis).readObject();
        assertEquals(expected, actual);
        System.out.println("read serialized " + actual);
    }

    private static void assertEquals(Object o1, Object o2) {
        if (!o1.equals(o2)) {
            throw new AssertionError("\n expected: " + o1 + "\n actual:   " + o2);
        }
    }

    private static Map<String, String> getO1() {
        Map<String, String> nvps = new HashMap<String, String>();
        nvps.put("timestamp", "1326382770000");
        nvps.put("length", "246");
        return nvps;
    }

    private static Map<String, String> getO2() {
        Map<String, String> nvps = new HashMap<String, String>();
        nvps.put("timestamp", "0");
        nvps.put("length", "0");
        return nvps;
    }

    private static ObjectOutputStream getOOS(File storageFile)
                    throws IOException {
        if (storageFile.exists()) {
            // this is a workaround so that we can append objects to an existing file
            return new AppendableObjectOutputStream(new FileOutputStream(storageFile, true));
        } else {
            return new ObjectOutputStream(new FileOutputStream(storageFile));
        }
    }

    private static ObjectInputStream getOIS(FileInputStream fis)
                    throws IOException {
        long pos = fis.getChannel().position();
        return pos == 0 ? new ObjectInputStream(fis) : 
            new AppendableObjectInputStream(fis);
    }

    private static class AppendableObjectOutputStream extends
                    ObjectOutputStream {

        public AppendableObjectOutputStream(OutputStream out)
                        throws IOException {
            super(out);
        }

        @Override
        protected void writeStreamHeader() throws IOException {
            // do not write a header
        }
    }

    private static class AppendableObjectInputStream extends ObjectInputStream {

        public AppendableObjectInputStream(InputStream in) throws IOException {
            super(in);
        }

        @Override
        protected void readStreamHeader() throws IOException {
            // do not read a header
        }
    }
}

0
你需要创建一个新的 ObjectInputStream 来匹配每个 ObjectOutputStream。我不知道有没有一种方法可以将完整的 ObjectInputStream 的状态传输到 ObjectOutputStream 中(在纯 Java 中进行完整重新实现有点棘手)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接