Процедура завершения работы библиотеки, которая хорошо работает в «обычном» Java-приложении и в веб-приложении.

Я поддерживаю драйвер JDBC, который также имеет режим встроенного сервера базы данных, предоставляемый через собственную библиотеку (доступ к которой осуществляется через JNA). Завершение работы, выполняемое как часть выгрузки самой собственной библиотеки, вызывает проблемы в Windows из-за порядка выгрузки ее зависимостей. Чтобы избежать нарушений доступа или других проблем, мне нужно явно выключить встроенный движок, прежде чем эта библиотека будет выгружена.

Учитывая характер его использования, трудно определить подходящий момент для вызова выключения, и единственный правильный способ для обычного приложения Java, который я вижу прямо сейчас, - это зарегистрировать обработчик выключения, используя Runtime.getRuntime().addShutdownHook с подклассом Thread, который реализует выключение логика.

Это отлично работает для обычного Java-приложения, но для веб-приложений, которые включают мою библиотеку как часть приложения (в WEB-INF/lib WAR), это вызовет утечку памяти при отмене развертывания, поскольку ловушка выключения будет поддерживать сильную ссылку на мои завершение работы и загрузчик классов веб-приложения.

Что было бы подходящим и подходящим способом решения этой проблемы? Варианты, которые я сейчас рассматриваю:

  • Использование java.sql.DriverAction.deregister() для очистки.

    Не подходит, поскольку драйвер не будет снят с регистрации при обычном выходе из приложения.

  • Использование java.sql.DriverAction.deregister() для удаления ловушки выключения и выполнения самой логики выключения.

    Использование DriverAction немного проблематично, поскольку драйвер по-прежнему поддерживает Java 7, и этот класс был представлен в JDBC 4.2 (Java 8). Технически это не всегда является правильным использованием действия (драйвер JDBC также можно отменить регистрацию, пока существующие соединения остаются действительными и используются), и возможно, что драйвер используется (через javax.sql.DataSource), пока реализация JDBC java.sql.Driver не зарегистрирована. .

  • Включая реализацию javax.servlet.ServletContextListener, помеченную @WebListener, с драйвером, который удалит ловушку выключения и сам выполнит логику выключения.

    Этот вариант имеет сложности, если драйвер развертывается на сервере в целом, а не в конкретном веб-приложении (хотя эти сложности можно решить).

Есть ли в Java механизм выключения, который я упустил из виду, который подходит для моих нужд?


person Mark Rotteveel    schedule 04.08.2018    source источник
comment
Не могли бы вы объяснить, что вы имеете в виду под развертыванием драйвера на сервере в целом? Я ничего не нашел об этом, и я не знаю о такой функциональности (по крайней мере, не в Tomcat). В настоящее время я использую решение на основе @WebListener в Tomcat для отмены регистрации нескольких java.sql.Driver (с использованием DriverManager.deregisterDriver) в нескольких контейнерах (я всегда отменяю регистрацию именно того драйвера, который был зарегистрирован данным контейнером, сохраняя Class<? extends java.sql.Driver>), и мне интересно, нет ли Я что-то упустил.   -  person Tomasz Linkowski    schedule 14.08.2018
comment
@TomaszLinkowski Вы можете развернуть драйвер, чтобы он был доступен во всем мире (возможно, использовался в качестве общесерверного источника данных). Например, в Tomcat, если вы поместите его в <catalina-home>/lib и определите источник данных в server.xml. Подобные функции существуют и на других серверах приложений. Ручная регистрация / отмена регистрации драйвера не сработает для этих ситуаций (и это не будет работать для не веб-приложений), и я не думаю, что это решит мою проблему, если драйвер доступен глобально, даже если он зарегистрирован для WAR (java.sql.Driver реализации являются (или должны быть) довольно легкими).   -  person Mark Rotteveel    schedule 15.08.2018
comment
@TomaszLinkowski re: light-weight: сам драйвер на самом деле не очень `` удерживает '', поэтому остальная часть реализации может быть разделена, поэтому отмена регистрации моей собственной библиотеки в таких ситуациях, вероятно, будет плохой идеей (или потребует некоторого дополнительного отражения магия). Учитывая отсутствие ответов на данный момент, похоже, что нет механизма, который я мог бы применить во всех ситуациях, поэтому мне придется найти какой-то гибридный подход.   -  person Mark Rotteveel    schedule 15.08.2018
comment
Спасибо за объяснение :) Что ж, я понимаю, что вам нужно сделать больше, чем мне (я просто отменяю регистрацию драйверов, чтобы Tomcat не жаловался на повторное развертывание). Насколько я понимаю, вы хотите выключить встроенный движок только если драйверы больше не зарегистрированы, верно? Так, например, если глобального драйвера нет, и у вас есть два контейнера, и каждый зарегистрировал свой драйвер, тогда, если один контейнер уничтожен, вы не хотите выключать встроенный движок до тех пор, пока второй уничтожен, не так ли? И если есть есть глобальный движок, вы хотите отключать его только при завершении работы сервера приложений, не так ли?   -  person Tomasz Linkowski    schedule 15.08.2018
comment
@TomaszLinkowski Звучит примерно правильно, за исключением ситуации «без глобального драйвера», встроенный движок должен быть отключен при уничтожении контейнера, поскольку встроенный движок в этом случае будет загружен для каждого контейнера.   -  person Mark Rotteveel    schedule 15.08.2018


Ответы (1)


Я попытался разобраться в этом, так как это кажется очень интересным случаем. Я публикую здесь свои выводы, хотя мне кажется, что я все еще что-то неправильно понял или сделал слишком надуманные упрощения. На самом деле, также возможно, что я совершенно неправильно понял ваш случай, и этот ответ бесполезен (если так, прошу прощения).

То, что я собрал здесь, основано на двух идеях:

  • глобальное состояние сервера приложений (я использую System.props, но это может быть не лучший выбор - возможно, некоторые временные файлы подойдут лучше)
  • глобальное состояние, зависящее от контейнера (что означает, что все классы загружены зависящим от контейнера ClassLoader)

Предлагаю EmbeddedEngineHandler.loadEmbeddedEngineIfNeeded метод, который будет вызываться:

  • во время регистрации водителя
  • в вашем статическом инициализаторе реализации javax.sql.DataSource (если вся эта вещь, связанная с DataSource, работает таким образом - я мало об этом знаю)

Если я правильно понял, тебе вообще не нужно будет звонить Runtime.removeShutdownHook.

Главное, в чем я не уверен, это то, что если драйвер развернут глобально, будет ли он зарегистрирован до инициализации какого-либо сервлета? Если нет, то я ошибся, и это не сработает. Но, может быть, тогда может помочь проверка ClassLoader из EmbeddedEngineHandler?


Это EmbeddedEngineHandler:

final class EmbeddedEngineHandler {

    private static final String PREFIX = ""; // some ID for your library here
    private static final String IS_SERVLET_CONTEXT = PREFIX + "-is-servlet-context";
    private static final String GLOBAL_ENGINE_LOADED = PREFIX + "-global-engine-loaded";

    private static final String TRUE = "true";

    private static volatile boolean localEngineLoaded = false;

    // LOADING
    static void loadEmbeddedEngineIfNeeded() {
        if (isServletContext()) {
            // handles only engine per container case
            loadEmbeddedEngineInLocalContextIfNeeded();
        } else {
            // handles both normal Java application & global driver cases
            loadEmbeddedEngineInGlobalContextIfNeeded();
        }

    }

    private static void loadEmbeddedEngineInLocalContextIfNeeded() {
        if (!isGlobalEngineLoaded() && !isLocalEngineLoaded()) { // will not load if we have a global driver
            loadEmbeddedEngine();
            markLocalEngineAsLoaded();
        }
    }

    private static void loadEmbeddedEngineInGlobalContextIfNeeded() {
        if (!isGlobalEngineLoaded()) {
            loadEmbeddedEngine();
            markGlobalEngineAsLoaded();
            Runtime.getRuntime().addShutdownHook(new Thread(EmbeddedEngineHandler::unloadEmbeddedEngine));
        }
    }

    private static void loadEmbeddedEngine() {
    }

    static void unloadEmbeddedEngine() {
    }

    // SERVLET CONTEXT (state shared between containers)
    private static boolean isServletContext() {
        return TRUE.equals(System.getProperty(IS_SERVLET_CONTEXT));
    }

    static void markAsServletContext() {
        System.setProperty(IS_SERVLET_CONTEXT, TRUE);
    }

    // GLOBAL ENGINE (state shared between containers)
    private static boolean isGlobalEngineLoaded() {
        return TRUE.equals(System.getProperty(GLOBAL_ENGINE_LOADED));
    }

    private static void markGlobalEngineAsLoaded() {
        System.setProperty(GLOBAL_ENGINE_LOADED, TRUE);
    }

    // LOCAL ENGINE (container-specific state)
    static boolean isLocalEngineLoaded() {
        return localEngineLoaded;
    }

    private static void markLocalEngineAsLoaded() {
        localEngineLoaded = true;
    }
}

а это ServletContextListener:

@WebListener
final class YourServletContextListener implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent sce) {
        EmbeddedEngineHandler.markAsServletContext();
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        if (EmbeddedEngineHandler.isLocalEngineLoaded()) {
            EmbeddedEngineHandler.unloadEmbeddedEngine();
        }
    }
}
person Tomasz Linkowski    schedule 15.08.2018
comment
Похоже, это многообещающее решение, спасибо. Я вижу некоторые возможные болевые точки, чтобы реализовать это, но я думаю, что это может сработать. - person Mark Rotteveel; 16.08.2018
comment
Мне потребовалось время, чтобы добраться до этого, но я, наконец, реализовал это: github.com/Firebird jaybird / commit / Еще раз спасибо за вашу помощь. - person Mark Rotteveel; 20.04.2019
comment
@MarkRotteveel Я рад, что вам удалось решить проблему. Я вижу, что это немного отличается от того, что я предлагал (например, вы используете removeShutdownHook и сравниваете ClassLoaders, чтобы определить, был ли драйвер загружен в контексте сервлета или нет), но я надеюсь, что у вас были веские причины для такого подхода :) - person Tomasz Linkowski; 20.04.2019
comment
Да, я изменил ваш подход для решения проблем, когда драйвер находится в основном пути к классам, но только первый доступ из контекста сервлета. С вашим подходом это привело бы к проблемам, если бы драйвер использовался в нескольких контекстах, а один был остановлен или повторно развернут. Мое текущее решение работает в Tomcat, хотя мне все еще нужно протестировать его на других серверах приложений (проверка загрузчиков классов может быть слишком наивной). - person Mark Rotteveel; 20.04.2019
comment
Понимаю, это то, что я боялся, может быть случай, когда я писал: если драйвер развернут глобально, будет ли он зарегистрирован до инициализации какого-либо сервлета ?. Приятно, что вам удалось это обойти! - person Tomasz Linkowski; 20.04.2019
comment
Да, потребовались некоторые методы проб и ошибок, чтобы заставить его работать, но ваш ответ дал мне хорошую отправную точку. И на всякий случай, если я что-то напортачил (или не рассмотрел некоторые случаи), я также определил системное свойство, которое полностью отключает эту функцию очистки / выключения. - person Mark Rotteveel; 20.04.2019