首页 > 解决方案 > SpringBoot应用程序销毁ApplicationContext后如何调用方法

问题描述

我有一个 SpringBoot 应用程序,它使用 Hikari jdbc 连接池来访问基于文件的 H2 数据库。

应用程序.yml

spring:
  datasource:
    hikari:
      driver-class-name: org.h2.Driver
      jdbc-url: jdbc:h2:${media.db.file};DB_CLOSE_ON_EXIT=FALSE
      username:
      password:
      maximum-pool-size: 5
      connection-test-query: "SELECT 1"
      pool-name: media-pool

现在,我有一个要求,即在应用程序关闭后,我需要创建 h2 数据库文件的本地备份。

显然,这只会在数据库连接关闭后起作用,并且由于我有各种注入数据源的 bean,我假设数据库连接只会在这些 bean 被销毁后关闭(显然创建副本的类不能一颗春豆)。

所以我的问题是如何检测所有 bean 何时已被销毁并且应用程序上下文已被销毁。

显然,使用 @PreDestroy 注释方法将不起作用,因为在销毁 bean之前会调用此类方法。

我找到了各种注册应用程序生命周期事件的方法,但是,如果我BackupUtility.backupDatabase()从其中任何一个调用我总是得到相同的异常:

java.io.IOException: The process cannot access the file because another process has locked a portion of the file
    at java.base/java.io.FileInputStream.readBytes(Native Method)
    at java.base/java.io.FileInputStream.read(FileInputStream.java:257)
    at ch.bee.mediathek.business.utility.BackupUtility.backupDatabase(BackupUtility.java:63)

我认为这意味着数据库连接尚未关闭,因此我试图过早地创建备份。

我尝试了以下方法来通知我的应用程序关闭:

  public static void main(String[] args) {
    ConfigurableApplicationContext context = SpringApplication.run(MediaSpringApplication.class, args);
    context.registerShutdownHook();
    log.info("Application started...");
    context.addApplicationListener(MediaSpringApplication::onApplicationEvent);
  }

  private static void onApplicationEvent(final ApplicationEvent event) {
    if (event instanceof ContextClosedEvent) {
      log.info("===== ContextClosed (1) =====");
      BackupUtility.backupDatabase();
    }
  }

  @Bean
  ServletListenerRegistrationBean<ServletContextListener> myServletListener() {
    ServletListenerRegistrationBean<ServletContextListener> srb = new ServletListenerRegistrationBean<>();
    srb.setListener(new MyServletContextListener());
    return srb;
  }

  public class MyServletContextListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent sce) {
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
      log.info("===== ContextDestroyed (2) =====");
      BackupUtility.backupDatabase();
    }
  }

  @EventListener({ContextClosedEvent.class})
  public void contextClosed() {
    log.info("===== ContextClosed (3) =====");
    BackupUtility.backupDatabase();
  }

查看日志输出,我看到以下内容:

[2021-01-06 19:38:12.023] [Thread-8] [INFO ] c.b.m.a.MediaSpringApplication::contextClosed(144) -    ===== ContextClosed (3) =====
[2021-01-06 19:38:12.030] [Thread-8] [ERROR] c.b.m.b.u.BackupUtility::backupDatabase(67) -    Error while copying file contents
java.io.IOException: Der Prozess kann nicht auf die Datei zugreifen, da ein anderer Prozess einen Teil der Datei gesperrt hat
    at java.base/java.io.FileInputStream.readBytes(Native Method)
    at java.base/java.io.FileInputStream.read(FileInputStream.java:257)
    at ch.bee.mediathek.business.utility.BackupUtility.backupDatabase(BackupUtility.java:63)
    at ch.bee.mediathek.app.MediaSpringApplication.contextClosed(MediaSpringApplication.java:145)
[2021-01-06 19:38:12.030] [Thread-8] [INFO ] c.b.m.a.MediaSpringApplication::onApplicationEvent(56) -    ===== ContextClosed (1) =====
[2021-01-06 19:38:12.031] [Thread-8] [ERROR] c.b.m.b.u.BackupUtility::backupDatabase(67) -    Error while copying file contents
java.io.IOException: Der Prozess kann nicht auf die Datei zugreifen, da ein anderer Prozess einen Teil der Datei gesperrt hat
    at java.base/java.io.FileInputStream.readBytes(Native Method)
    at java.base/java.io.FileInputStream.read(FileInputStream.java:257)
    at ch.bee.mediathek.business.utility.BackupUtility.backupDatabase(BackupUtility.java:63)
    at ch.bee.mediathek.app.MediaSpringApplication.onApplicationEvent(MediaSpringApplication.java:57)
[2021-01-06 19:38:12.138] [Thread-8] [INFO ] c.b.m.a.MediaSpringApplication::contextDestroyed(137) -    ===== ContextDestroyed (2) =====
[2021-01-06 19:38:12.140] [Thread-8] [ERROR] c.b.m.b.u.BackupUtility::backupDatabase(67) -    Error while copying file contents
java.io.IOException: Der Prozess kann nicht auf die Datei zugreifen, da ein anderer Prozess einen Teil der Datei gesperrt hat
    at java.base/java.io.FileInputStream.readBytes(Native Method)
    at java.base/java.io.FileInputStream.read(FileInputStream.java:257)
    at ch.bee.mediathek.business.utility.BackupUtility.backupDatabase(BackupUtility.java:63)
    at ch.bee.mediathek.app.MediaSpringApplication$MyServletContextListener.contextDestroyed(MediaSpringApplication.java:138)
[2021-01-06 19:38:12.393] [Thread-8] [WARN ] o.s.b.f.s.DisposableBeanAdapter::destroy(267) -    Invocation of destroy method failed on bean with name 'inMemoryDatabaseShutdownExecutor': org.h2.jdbc.JdbcSQLNonTransientConnectionException: Database is already closed (to disable automatic closing at VM shutdown, add ";DB_CLOSE_ON_EXIT=FALSE" to the db URL) [90121-200]
[2021-01-06 19:38:12.395] [Thread-8] [INFO ] c.z.h.HikariDataSource::close(350) -    media-pool - Shutdown initiated...
[2021-01-06 19:38:12.398] [Thread-8] [INFO ] c.z.h.HikariDataSource::close(352) -    media-pool - Shutdown completed.

查看日志的最后四行,我发现当我在最后一次回调(由 ServletContextListener 触发的回调)之后尝试备份数据库文件时,该文件显然仍处于打开状态。但是,在下一行我看到 DisposableBeanAdapter 尝试做一些失败的事情,因为数据库已经关闭。接下来的两行显示 HikariDataSource 类甚至更晚才关闭。

所以,似乎我需要在 HikariDataSource 关闭媒体池后找到一种方法来启动我的备份。我本来希望当这些 bean 被销毁时会发生这种情况,并且我的上下文销毁回调只会在发生之后调用,但它似乎不是那样工作的。

我试图从spring.datasource.hikari.jdbc-url我的application.yml文件中删除 DB_CLOSE_ON_EXIT=FALSE 但仍然无法创建备份,但此外还在多次关闭结束时添加了以下异常(可能每个数据池成员一次):

[2021-01-06 20:04:30.466] [Thread-9] [WARN ] c.z.h.p.PoolBase::isConnectionAlive(184) -    media-pool - Failed to validate connection conn1: url=jdbc:h2:/workspaces/media-spring/database/Media-local user= (Database is already closed (to disable automatic closing at VM shutdown, add ";DB_CLOSE_ON_EXIT=FALSE" to the db URL) [90121-200]). Possibly consider using a shorter maxLifetime value.

难道我做错了什么?还是有其他方法可以在以后通知我还没有找到?

标签: springspring-booth2hikaricp

解决方案


推荐阅读