将 java 对象可靠地存储在文件中的最少代码

Minimal code to reliably store java object in a file

我想在我的小型独立 Java 应用程序中存储信息。

我的要求:

因此,我想使用 jaxb 将所有信息存储在文件系统 中的一个简单 XML 文件中。我的示例应用程序如下所示(将所有代码复制到一个名为 Application.java 的文件中并编译,没有其他要求!):

@XmlRootElement
class DataStorage {
    String emailAddress;
    List<String> familyMembers;
    // List<Address> addresses;
}

public class Application {

    private static JAXBContext jc;
    private static File storageLocation = new File("data.xml");

    public static void main(String[] args) throws Exception {
        jc = JAXBContext.newInstance(DataStorage.class);

        DataStorage dataStorage = load();

        // the main application will be executed here

        // data manipulation like this:
        dataStorage.emailAddress = "me@example.com";
        dataStorage.familyMembers.add("Mike");

        save(dataStorage);
    }

    protected static DataStorage load() throws JAXBException {
        if (storageLocation.exists()) {
            StreamSource source = new StreamSource(storageLocation);
            return (DataStorage) jc.createUnmarshaller().unmarshal(source);
        }
        return new DataStorage();
    }

    protected static void save(DataStorage dataStorage) throws JAXBException {
        jc.createMarshaller().marshal(dataStorage, storageLocation);
    }
}

我该如何克服这些缺点?

  • 多次启动应用程序可能会导致不一致:多个用户可能运行网络驱动器上的应用程序并遇到并发 问题
  • 中止写入过程可能会导致损坏数据或丢失所有数据

正在查看您的要求:

  • 多次启动应用程序
  • 多个用户可以 运行 网络驱动器上的应用程序
  • 防止数据损坏

我认为基于 XML 的文件系统是不够的。 如果您认为合适的关系数据库有点矫枉过正,您仍然可以选择 H2 db 这是一个超轻量级的数据库,可以解决上述所有问题(即使不是完美,但肯定比手写 XML 数据库好得多),并且仍然非常容易设置和维护。

您可以将其配置为将您的更改保存到磁盘,可以配置为 运行 作为独立服务器并接受多个连接,或者可以 运行 作为嵌入式应用程序的一部分-模式也是。

关于 "How do you save the data" 部分:

如果您不想使用任何高级 ORM 库(如 Hibernate 或任何其他 JPA 实现),您仍然可以使用普通的旧 JDBC。或者至少有一些 Spring-JDBC,非常轻便且易于使用。

"What do you save"

H2 是一个关系型数据库。因此,无论您保存什么,它最终都会出现在列中。但!如果您真的不打算查询您的数据(既不对其应用迁移脚本),保存您已经 XML 序列化的对象 一个选项。您可以轻松定义一个带有 ID 的 table + 一个 "data" varchar 列,并将您的 xml 保存在那里。 H2DB中数据长度没有限制。

注意:在关系数据库中保存 XML 通常不是一个好主意。我只是建议您评估此选项,因为您似乎确信您只需要 SQL 实现可以提供的一组特定功能。

想了想,想这样实现:

  • 打开具有最新时间戳的 data.<timestamp>.xml 文件。
  • 只使用只读模式。
  • 进行更改。
  • 将文件另存为 data.<timestamp>.xml - 不要覆盖并检查是否不存在具有较新时间戳的文件。

不一致和并发的处理方式有两种:

  • 通过锁定
  • 通过版本控制

在应用程序级别无法很好地处理损坏的写入。文件系统应支持日志记录,它试图在某种程度上解决这个问题。您也可以通过

  • 制作您自己的日志文件(即一个短暂的单独文件,其中包含要提交给真实数据文件的更改)。

所有这些功能即使在最简单的关系数据库中也可用,例如H2、SQLite,甚至网页都可以使用 HTML5 中的此类功能。从头开始重新实现这些是相当大材小用的,数据存储层的正确实现实际上会使您的简单需求变得相当复杂。

但是,仅作记录:

带锁的并发处理

  • 在开始更改 xml 之前,使用文件锁获得对文件的独占访问权,另请参阅 How can I lock a file using java (if possible)
  • 更新完成并成功关闭文件后,释放锁

用锁处理一致性(原子性)

  • 其他应用程序实例可能仍在尝试读取文件,而其中一个应用程序正在写入文件。这可能会导致不一致(又名脏读)。确保在写入过程中,写入进程对文件具有独占锁。如果无法获得独占访问锁,编写者必须稍等片刻并重试。

  • 读取该文件的应用程序应读取它(如果它可以获得访问权限,没有其他实例执行独占锁),然后关闭该文件。如果无法读取(由于其他应用程序锁定),请等待并重试。

  • 仍然可以使用外部应用程序(例如记事本)更改 xml。您可能更喜欢在读取文件时使用独占读锁。

基本日记

这里的想法是,如果您可能需要进行大量写入,(或者如果您稍后可能想要回滚您的写入),您不想接触真实文件。相反:

  • 随着更改写入一个单独的日志文件,由您的应用程序实例创建并锁定

  • 您的应用程序实例不锁定主文件,它只锁定日志文件

  • 一旦所有写入都准备就绪,您的应用程序将使用独占写入锁打开真实文件,并提交日志文件中的所有更改,然后关闭文件。

如你所见,带锁的方案将文件作为一种共享资源,被锁保护,同一时间只有一个应用程序可以访问该文件。这解决了并发问题,但也使文件访问成为瓶颈。因此,Oracle 等现代数据库使用版本控制而不是锁定。版本控制意味着文件的旧版本和新版本同时可用。读者将得到旧的、最完整的文件的服务。新版本写入完成后,合并到旧版本,新数据即刻可用。这实现起来比较棘手,但由于它允许并行读取所有应用程序的所有时间,因此它的扩展性要好得多。

请注意,您的简单回答不会处理不同实例的并发写入。如果两个实例进行更改并保存,仅选择最新的实例将最终丢失另一个实例的更改。正如其他答案所提到的,您应该尝试为此使用文件锁定。

一个相对简单的解决方案:

  • 使用单独的锁定文件写入 "data.xml.lck"。写入文件时锁定
  • 如我的评论所述,首先写入临时文件 "data.xml.tmp",然后在写入完成后重命名为最终名称 "data.xml"。这将合理保证任何阅读该文件的人都将获得完整的文件。
  • 即使有文件锁定,您仍然必须处理 "merge" 问题(一个实例读取,另一个实例写入,然后第一个想要写入)。为了处理这个你应该在文件内容中有一个版本号。当一个实例想要写入时,它首先获取锁。然后它根据文件版本号检查其本地版本号。如果它已过时,则需要将文件中的内容与本地更改合并。然后它可以写一个新版本。

回答你提到的三个问题:

多次启动应用程序可能会导致不一致

为什么会导致不一致?如果您的意思是多个并发编辑会导致不一致,您只需在编辑前锁定文件即可。在文件旁边创建一个锁定文件的最简单方法。在开始编辑之前,只需检查是否存在锁定文件。

如果你想让它更容错,你也可以在文件上设置超时。例如锁定文件的有效期为 10 分钟。你可以在锁文件中写入一个随机生成的uuid,在保存之前,你可以检查uuid是否仍然匹配。

多个用户可能 运行 网络驱动器上的应用程序并遇到并发问题

我认为这与数字 1 相同。

中止写入过程可能会导致数据损坏或丢失所有数据

这可以通过使写入原子化或文件不可变来解决。为了使其成为原子,而不是直接编辑文件,只需复制文件,然后在副本上进行编辑。保存副本后,只需重命名文件即可。但是如果你想更安全,你总是可以做一些事情,比如在文件上附加时间戳,而不是编辑或删除文件。因此,每次进行编辑时,您都会创建一份副本,并在文件上附加一个较新的时间戳。对于阅读,您将始终阅读最新的。