如何正确关闭资源

21

我在清理一些代码时,FindBugs指向了一个使用Connection、CallableStatement和ResultSet对象的JDBC代码。这里是来自那段代码的片段:

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

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

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

FindBugs指出这些代码应该在finally块中,我开始重构我的代码来实现这一点,但我开始思考如何处理finally块内的代码。

可能CallableStatement或Connection对象的创建会引发异常,导致我的ResultSet对象为空。当我尝试关闭ResultSet时,我会得到NullPointerException,我的Connection也因此永远无法关闭。实际上,这个线程提出了同样的概念,并显示将close()调用包装在null检查中是一个好主意。

但其他可能的异常呢?根据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 */ }

}

哎呀,这看起来真是糟糕透顶。有没有更好的方法?


请看:https://dev59.com/DHRC5IYBdhLWcg3wXPwC - worpet
2
吞噬异常可能是你所能做的最糟糕的事情,就像我的回答一样。 - maaartinus
6个回答

11

我认为最好的答案已经提到了,但我想提一下你可以考虑使用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,但对于那些可以开始尝试开发人员预览版的人来说,这提供了一种有趣的方法,并且在不久的将来可能会淘汰其他方法。


5

如果可以的话,使用Lombok的cleanup功能

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

这段代码涉及到IT技术,它的作用是将三个嵌套的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()到在方法定义中添加三个资源的进展是非常明显的 - 你因此而对他的回答投了反对票? - matt b
我之所以对他的回答进行了负评,仅仅是因为他吞掉了异常 - 这是在公开示例中展示的东西,没有人应该这样做。由于在示例中看到它,初学者一遍又一遍地重复这种错误......而你不得不一遍又一遍地解释它。他的答案无法修改,因此无法避免吞掉异常 - 这就是主要区别。 - maaartinus
2
@maaartinus 我必须说,在 MJB 代码片段中,异常并没有被吞掉,而是被记录下来了,这是完全不同的策略上的有效做法,尤其是在 close 方法中发生异常时,你很可能不想采取任何恢复措施,以我个人的看法。 - Edwin Dalorzo
@edalorzo 是的,记录日志有时是处理异常的方法。如果它总是适用于close方法,那么就不会有异常,只会有JDBC中的日志语句。有时记录日志和什么都不做一样糟糕。 - maaartinus
1
我的观点是,我看不出你的方法比MIJB的更好。你提供了一个静态的close方法,但你仍然需要处理相关的异常(如果发生),而你还没有展示如何解决关闭资源的问题。在MIJ的方法中,他只是记录异常并假设没有关于关闭资源的操作。在你的方法中,你只是将异常转发到调用堆栈上。根据你的说法,你的方法更好,因为你可以对此做些什么。这可能是正确的,直到我们能够证明我们可以对此做些什么。 - Edwin Dalorzo
@edalorzo 同意,我没有展示如何处理它,因为这个代码片段可能不是处理它的工作。在99%的情况下,声明 throws SQLException 是正确的方法,让问题保持开放状态。在我看来,不做(可能错误的)我没有被要求做的事情是正确的,但我理解你的观点。 - maaartinus

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);
}

这其实并不那么丑陋。


2
你基本上是在吞噬异常。在这个地方,你没有机会处理它们,因为你不知道它被用在哪里。记录日志并不等同于处理异常(尽管有时是适当的)。 - maaartinus
我考虑过这个问题,但是问题在于我需要为每种我想要关闭的对象类型编写一个实用方法。在我的情况下,这将是三种除了参数类型外完全相同的方法。为了解决这个问题,我可以使用反射来查找名为“close”的方法并调用它,但我不认为这会使事情变得更好。 - McGlone
这看起来仍然相当丑陋。您还可以创建一个父级关闭方法,该方法将接受ResultSet、Statement和Connection,然后对它们中的每一个调用close方法。从三个关闭行到1个。 - kfox
@kfox 这就是我在答案中所做的。@McGlone 在JDK7中有一个名为Autocloseable的接口,声明抛出异常并可能为此设计。 - maaartinus
cglone,kfox - 这里需要做出判断。我的理论是你的程序中可能有多个jdbc使用。我猜你可以列出每个语句/预处理语句/结果集/连接的排列组合,但我认为这样更清晰。Maatinus - 如果你记录了异常,那通常就是你在SQL方面所能做的全部了。你提到的银行更新示例是无效的,因为通常你会执行setAutoCommit(false),然后执行COMMIT,如果有异常则执行ROLLBACK。你永远不会依赖于语句/结果集/连接的CLOSE来提交更改。(如果你这样做,扣40分) - MJB
只要想想还有很多程序员在使用JDK6进行开发。这种方法虽然可以清理资源,但可能会丢失真正的异常信息。不管怎样,我认为与ResultSet相比,你应该使用更通用的类型(Closeable)。 - Genaut

3
你只需要关闭连接即可。
try
{
    cStmt.close();
}
catch(Exception e)
{
    /* Intentionally Swallow Exception */
} 

来自docs.oracle.com:

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

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


2

我知道的唯一隐藏所有丑陋的try-catch样板代码的方法是利用类似于Spring的JBDC模板这样的东西。


-2

你可以将一个代码块包裹在另一个代码块中:

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

使用最下面的catch块来处理可能出现的所有问题


3
捕获资源关闭时抛出的异常再次抛出实际上几乎没有任何实用价值。 - matt b
1
此外,结构不正确或缺少空值检查。您可能会在“rs”和“statement”两者上获得NullPointerException。 - maaartinus
1
@matt b:我同意,这方面你无能为力,但不能忽视它。如果交易中有任何更新,你必须考虑数据未被写入的可能性。想象一下这是一笔银行交易... - maaartinus

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