适用于“普通”Java应用程序和Web应用程序的库关闭例程。

12

我维护一个JDBC驱动程序,该驱动程序还通过本地库提供了嵌入式数据库服务器模式,该本地库通过JNA访问。关闭过程作为卸载本地库本身的一部分运行时,在Windows上由于卸载其依赖项的顺序而遇到问题。为避免访问冲突或其他问题,我需要在卸载此库之前显式关闭嵌入式引擎。

由于其使用方式的特殊性,很难确定调用关闭的适当时机。对于普通Java应用程序,唯一正确的方法是使用Runtime.getRuntime().addShutdownHook注册一个关机挂钩,并使用实现关闭逻辑的Thread子类。

这对于普通的Java应用程序来说很好用,但对于将我的库作为应用程序的一部分(在WAR的WEB-INF/lib中)的Web应用程序来说,这将导致内存泄漏,因为关闭挂钩将保持对我的关闭实现和Web应用程序的类加载器的强引用。

有什么适当的方法来解决这个问题?我正在研究的选项包括:

  • 使用 java.sql.DriverAction.deregister() 进行清理。

    不适合在正常应用程序退出时注销驱动程序。

  • 使用 java.sql.DriverAction.deregister() 移除关闭挂钩并执行关闭逻辑本身。

    使用 DriverAction 有些问题,因为该驱动程序仍支持 Java 7,并且此类是在 JDBC 4.2(Java 8)中引入的。这在技术上并不总是正确的操作方式(JDBC 驱动程序也可以在现有连接仍然有效和正在使用的情况下注销),而且可能会在 JDBC java.sql.Driver 实现未注册时使用该驱动程序(通过 javax.sql.DataSource)。

  • 包含一个使用 @WebListener 注释的 javax.servlet.ServletContextListener 实现,其中包含将删除关闭挂钩并执行关闭逻辑本身的驱动程序。

    如果将驱动程序部署到整个服务器而不是特定的 Web 应用程序,则此选项会产生复杂性(尽管可以解决这些复杂性)。

在Java中,我是否忽略了适合我的需求的关闭机制?


1
请问您所说的驱动程序作为整体部署到服务器是什么意思?我找不到相关信息,也不知道是否有这样的功能(至少在Tomcat中没有)。我目前在Tomcat中使用基于@WebListener的解决方案,在一些容器中注销了一些java.sql.Driver(使用DriverManager.deregisterDriver),我总是通过存储Class<? extends java.sql.Driver>来注销给定容器注册的确切驱动程序,并且我想知道是否有什么遗漏。 - Tomasz Linkowski
@TomaszLinkowski 您可以部署驱动程序,以便全局可用(可能用作服务器范围的数据源)。例如,在Tomcat中,如果将其放置在 <catalina-home>/lib 中并在 server.xml 中定义数据源,则可以实现此目的。其他应用服务器中也存在类似的功能。手动注册/注销驱动程序对于这些情况不起作用(对于非Web应用程序也是如此),即使每个WAR都注册了驱动程序,如果驱动程序全局可用,我认为它也无法解决我的问题(java.sql.Driver 实现应该是轻量级的)。 - Mark Rotteveel
@TomaszLinkowski 关于轻量级:驱动程序本身实际上并没有“持有”太多东西,因此其余的实现可以共享,因此在这种情况下注销我的本地库可能是一个坏主意(或需要一些额外的反射魔法)。鉴于迄今为止缺乏响应,看起来没有机制可以在所有情况下应用,因此我必须找到一些混合方法。 - Mark Rotteveel
谢谢您的解释 :) 嗯,我明白您需要做的比我多(我只是注销驱动程序,以便Tomcat在重新部署时不会出现问题)。据我所知,您想要关闭嵌入式引擎仅当没有注册驱动程序时,对吗?例如,如果没有全局驱动程序,并且您有两个容器,每个容器都注册了其驱动程序,则如果一个容器被销毁,您就不想关闭嵌入式引擎,直到第二个容器被销毁,对吗?如果全局引擎,您只想在应用服务器关闭时关闭,是这样吗? - Tomasz Linkowski
@TomaszLinkowski 听起来差不多,除了“没有全局驱动程序”的情况外,在容器销毁时需要关闭嵌入式引擎,因为在这种情况下,每个容器将加载嵌入式引擎。 - Mark Rotteveel
1个回答

2
我尝试研究这个问题,因为这似乎是一个有趣的案例。我把我的发现发布在这里,尽管我觉得我可能还是误解了一些东西,或者过于简化了一些内容。实际上,也有可能我完全误解了你的情况,这个答案是毫无用处的(如果是这样,我很抱歉)。
我所组织的内容基于两个概念:
- 应用程序服务器全局状态(我使用 System.props,但这可能不是最好的选择 - 也许一些临时文件会更好) - 容器特定的全局状态(这意味着由容器特定 ClassLoader 加载的所有类)
我建议一个 EmbeddedEngineHandler.loadEmbeddedEngineIfNeeded 方法,在以下情况下调用它:
- 在您的驱动程序注册期间 - 在您的 javax.sql.DataSource 实现静态初始化程序中(如果整个 DataSource 相关的事情都是这样进行的 - 我对此知之甚少)
如果我理解正确,您将不需要调用 Runtime.removeShutdownHook。
我在这里最不确定的主要事情是 - 如果驱动程序是全局部署的,那么它是否会在任何 Servlet 初始化之前进行注册?如果不是,那么我就错了,这不起作用。但也许检查 EmbeddedEngineHandler 的 ClassLoader 可以帮助解决这个问题?
这是 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();
        }
    }
}

1
这看起来是一个很有前途的解决方案,谢谢。我可以看到实施这个方案可能会有一些痛点,但我认为这可能会奏效。 - Mark Rotteveel
1
我花了一些时间才完成这个,但是我终于实现了:https://github.com/FirebirdSQL/jaybird/commit/7e65ee051cbfb8d179777095c4a55f214c32a298 再次感谢您的帮助。 - Mark Rotteveel
1
@MarkRotteveel,我很高兴你成功解决了这个问题。我看到你的方法有些不同于我的建议(例如,你使用了removeShutdownHook,并且比较ClassLoader来确定驱动程序是否在servlet上下文中加载),但我相信你有充分的理由采用这种方法 :) - Tomasz Linkowski
1
是的,我修改了你的方法来解决当驱动程序在主类路径上,但只有在Servlet上下文中首次访问时会出现问题的情况。使用你的方法,如果驱动程序在多个上下文中使用,并且其中一个停止或重新部署,那么就会出现问题。我的目前的解决方案可以在Tomcat中工作,尽管我仍然需要在其他应用服务器中测试它(检查类加载器可能太幼稚)。 - Mark Rotteveel
我明白了,这正是我写下“如果驱动程序是全局部署的,它是否会在任何servlet初始化之前注册?”时担心的情况。很高兴你设法解决了它! - Tomasz Linkowski
1
是的,需要一些尝试和错误才能使其正常工作,但您的答案给了我一个很好的起点。以防万一我搞砸了(或者没有考虑到某些情况),我还定义了一个系统属性来完全禁用此清理/关闭功能。 - Mark Rotteveel

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