【问题标题】:How to Correctly Close Resources如何正确关闭资源
【发布时间】:2011-08-12 13:31:48
【问题描述】:

当我清理一些代码时,FindBugs 向我指出了一些使用 Connection、CallableStatement 和 ResultSet 对象的 JDBC 代码。这是该代码中的一个 sn-p:

CallableStatement cStmt = getConnection().prepareCall("...");
...
ResultSet rs = cStmt.executeQuery();

while ( rs.next() )
{
    ...
}

cStmt.close();
rs.close();
con.close();

FindBugs 指出这些应该在 finally 块中。我开始重构我的代码来做到这一点,我开始想知道如何处理 finally 块中的代码。

创建 Connection 对象的 CallableStatement 可能会引发异常,使我的 ResultSet 对象为空。当我尝试关闭 ResultSet 时,我会收到 NullPointerException,而我的 Connection 将永远不会关闭。事实上,this thread 提出了相同的概念,并表明将您的 close() 调用包装在一个空检查中是一个好主意。

但是其他可能的例外情况呢?根据 Java API 规范,Statement.close() 可以“如果发生数据库错误”抛出 SQLException。因此,即使我的 CallableStatement 不为 null 并且我可以成功地对其调用 close(),我仍然可能会遇到异常并且没有机会关闭我的其他资源。

我能想到的唯一“故障安全”解决方案是将每个 close() 调用包装在其自己的 try/catch 块中,如下所示:

finally {

    try {
        cStmt.close();
    } catch (Exception e) { /* Intentionally Swallow  Exception */ }

    try {
        rs.close();
    } catch (Exception e) { /* Intentionally Swallow  Exception */ }

    try {
        con.close();
    } catch (Exception e) { /* Intentionally Swallow  Exception */ }

}

男孩,如果这看起来不可怕的话。有没有更好的方法来解决这个问题?

【问题讨论】:

标签: java jdbc


【解决方案1】:

我认为最好的答案已经被提及,但我认为提及您可以考虑可自动关闭资源的新 JDK 7 功能可能会很有趣。

try{
    try(Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/hrdb", "obiwan", "kenobi"); 
        Statement stm = conn.createStatement(); 
        ResultSet rs = stm.executeQuery("select name from department")) {

        while(rs.next()){
            System.out.println(rs.getString("name"));
        }

    } 
}catch(SQLException e){
    //you might wanna check e.getSuppressed() as well
    //log, wrap, rethrow as desired.
}

现在并非所有人都可以迁移到 JDK 7,但对于那些可以开始使用开发者预览版的人来说,这提供了一种有趣的做事方式,并且在不久的将来肯定会弃用其他方法。

【讨论】:

    【解决方案2】:

    如果可以,请使用Lombok's cleanup:

    @Cleanup
    Connection c = ...
    @Cleanup
    statement = c.prepareStatement(...);
    @Cleanup
    rs = statement.execute(...);
    

    这可以转换为三个嵌套的 try-finally 块,并且可以正常工作,但有异常。 永远不要在没有充分理由的情况下吞下异常!

    另一种选择:

    像这样编写一个自己的实用程序方法:

    public static void close(ResultSet rs, Statement stmt, Connection con) throws SQLException {
        try {
            try {
                if (rs!=null) rs.close();
            } finally {
                if (stmt!=null) stmt.close();
            }
        } finally {
            if (con!=null) con.close();
        }
    }
    

    并在

    中使用它
    try {
        Connection con = ...
        Statement stmt = ...
        ResultSet rs = ...
    } finally {
        close(rs, stmt, con);
    }
    

    让异常冒泡或随心所欲地处理它。

    【讨论】:

    • 您的替代方案与 MJB 的想法并没有太大的不同,我认为从在一个资源上调用 close() 到在方法定义中添加三个的进展非常明显 - 为此,您拒绝了他的回答?
    • 我对他的回答投了反对票,只是因为吞下了异常——在公开展示的示例中,这是任何人都不应该做的事情。因为在示例中看到它,初学者会一遍又一遍地做......你一遍又一遍地解释它。他的答案无法修改,因此它不会吞下异常 - 这是主要区别。
    • @maaartinus 我不得不说,在 MJB 代码摘录中,异常并没有被吞下,而是被记录下来,这是完全不同的并且在战略上是有效的,尤其是对于在 close 方法中发生的异常,恕我直言,你很可能不想做任何事情来恢复。
    • @edalorzo 是的,日志记录是有时处理异常的方式。 如果它总是适用于关闭方法,那么也不例外,只是在 JDBC 中记录语句。有时日志记录就像什么都不做一样糟糕。
    • 我的意思是,我看不出您的方法比 MIJB 更好。您提供了一个静态关闭方法,您仍然需要处理有问题的异常(如果发生),但您还没有展示如何处理关闭资源的问题。在 MIJ 的方法中,他只是简单地记录异常并假设关闭资源无关紧要。在您的方法中,您只需将异常转发到调用堆栈。据你说,你的方法更好,因为你可以做点什么。在我们能够证明我们可以对此有所作为之前,这可以说是正确的。
    【解决方案3】:

    嗯,基本上这就是你所做的,除了你首先不一定吞下异常(你可以空检查并至少记录异常)。其次,您可以设置一个不错的实用程序类,例如

    public static void close(ResultSet rs) {
       try { if (rs != null) rs.close();
       } catch (SQLException (e) {
          log.error("",e);
       } 
    
    }
    

    然后你只需静态导入该类。

    你终于变成了这样的东西

    finally {
         close(resultset);
         close(statement);
         close(connection);
    }
    

    这真的没有那么可怕。

    【讨论】:

    • -1 你基本上是在吞下异常。在这个地方你没有机会处理它们,因为你不知道它在哪里被使用。记录 != 处理(虽然有时是合适的)。
    • 我考虑过这一点,但问题是我需要为我想要关闭的每种类型的对象都有一个实用方法。在我的情况下,这将是三个相同的方法,除了参数类型。为了解决这个问题,我可以使用反射找到一个名为“close”的方法并调用它,但我不明白这如何让事情变得更好。
    • 这看起来仍然相当可怕。您还可以创建一个父 close 方法,该方法将采用 ResultSet、Statement 和 Connection,然后对它们中的每一个调用 close。从 3 条接近线变为 1 条。
    • @kfox 这就是我在回答中所做的。 @McGlone 在 JDK7 中有一个接口 Autocloseable 声明抛出异常并且可能是出于这个原因而设计的。
    • cglone,kfox - 判断电话在这里。我的理论是你的程序中可能有不止一个 jdbc 用法。我想你可以想出语句/preparedstatement/resultset/connection 的每一个排列,我认为这样更清楚。 Maatinus - 如果你已经记录了你的异常,那通常是你可以用 SQL 做的所有事情。您提供的银行更新示例无效,因为通常您会执行 setAutoCommit(false),然后执行 COMMIT,如果出现异常则执行 ROLLBACK。您永远不会依赖语句/结果集/连接的 CLOSE 来提交更改。 (如果你这样做,-40)
    【解决方案4】:

    您只需关闭连接。

    try
    {
        cStmt.close();
    }
    catch(Exception e)
    {
        /* Intentionally Swallow Exception */
    } 
    

    来自 docs.oracle.com:

    Statement 对象在被垃圾回收时会自动关闭。当 Statement 对象关闭时,其当前的 ResultSet 对象(如果存在)也将关闭。

    在 Connection 上调用 close() 会释放其数据库和 JDBC 资源。

    【讨论】:

      【解决方案5】:

      我知道隐藏所有难看的 try-catch 样板代码的唯一方法是使用 Spring's JBDC Template 之类的东西。

      【讨论】:

        【解决方案6】:

        您可以将一个块包装到另一个块中:

        try{
          Connection c = ...
            try{
              statement = c.prepareStatement(...);
              try{
                rs = statement.execute(...);
              }finally{
                rs.close();
              }
            }finally{
              statement.close()
            }
          }finally{
            c.close();
          }
        }catch(SQLException e){}
        

        对可能出现的所有内容都使用最低的捕获块

        【讨论】:

        • @maaartinus - 在资源关闭时重新抛出异常几乎没有实际价值。
        • 此外,结构错误或缺少空检查。对于 'rs` 和 statement,您可能会收到 NullPointerException。
        • @matt b:我同意,您对此无能为力,但您不能忽视它。如果事务中有任何 UPDATE,您必须处理数据未写入的可能性。想象一下这是一笔银行交易……
        猜你喜欢
        • 1970-01-01
        • 2015-03-15
        • 2017-06-21
        • 1970-01-01
        • 1970-01-01
        • 2013-04-02
        • 1970-01-01
        • 1970-01-01
        • 2016-02-03
        相关资源
        最近更新 更多