【问题标题】:Try / Try-with-resources and Connection, Statement and ResultSet closingTry / Try-with-resources 和 Connection、Statement 和 ResultSet 关闭
【发布时间】:2014-05-05 11:41:03
【问题描述】:

我最近和我的教授讨论了如何处理基本的 jdbc 连接方案。假设我们要执行两个查询,这就是他的建议

public void doQueries() throws MyException{
    Connection con = null;
    try {
        con = DriverManager.getConnection(dataSource);
        PreparedStatement s1 = con.prepareStatement(updateSqlQuery);
        PreparedStatement s2 = con.prepareStatement(selectSqlQuery);

        // Set the parameters of the PreparedStatements and maybe do other things

        s1.executeUpdate();
        ResultSet rs = s2.executeQuery();

        rs.close();
        s2.close();
        s1.close();
    } catch (SQLException e) {
        throw new MyException(e);
    } finally {
        try {
            if (con != null) {
                con.close();
            }
        } catch (SQLException e2) {
            // Can't really do anything
        }
    }
}

我不喜欢这种方法,对此我有两个问题:

1.A) 我认为,如果在我们做“其他事情”的地方或在rs.close()s2.close() 行中抛出任何异常,那么当方法结束时s1 不会被关闭。我说的对吗?

1.B) 教授一直要求我明确关闭 ResultSet(即使声明文档明确表示它将关闭 ResultSet)她说 Sun 推荐它。有什么理由这样做吗?

现在我认为这是同一件事的正确代码:

public void doQueries() throws MyException{
    Connection con = null;
    PreparedStatement s1 = null;
    PreparedStatement s2 = null;
    try {
        con = DriverManager.getConnection(dataSource);
        s1 = con.prepareStatement(updateSqlQuery);
        s2 = con.prepareStatement(selectSqlQuery);

        // Set the parameters of the PreparedStatements and maybe do other things

        s1.executeUpdate();
        ResultSet rs = s2.executeQuery();

    } catch (SQLException e) {
        throw new MyException(e);
    } finally {
        try {
            if (s2 != null) {
                s2.close();
            }
        } catch (SQLException e3) {
            // Can't do nothing
        }
        try {
            if (s1 != null) {
                s1.close();
            }
        } catch (SQLException e3) {
            // Can't do nothing
        }
        try {
            if (con != null) {
                con.close();
            }
        } catch (SQLException e2) {
            // Can't do nothing
        }
    }
}

2.A) 这段代码正确吗? (是否保证方法结束时全部关闭?)

2.B) 这非常大而且冗长(如果有更多的语句,情况会变得更糟)有没有更短或更优雅的方法来做到这一点而不使用 try-with-resources?

最后这是我最喜欢的代码

public void doQueries() throws MyException{
    try (Connection con = DriverManager.getConnection(dataSource);
         PreparedStatement s1 = con.prepareStatement(updateSqlQuery);
         PreparedStatement s2 = con.prepareStatement(selectSqlQuery))
    {

        // Set the parameters of the PreparedStatements and maybe do other things

        s1.executeUpdate();
        ResultSet rs = s2.executeQuery();

    } catch (SQLException e) {
        throw new MyException(e);
    }
}

3) 这段代码正确吗?我认为我的教授不喜欢这种方式,因为没有明确关闭 ResultSet,但她告诉我,只要在文档中清楚地表明所有内容都已关闭,她就可以接受。能否给出官方文档的链接,有类似的例子,或者根据文档显示这段代码没有问题?

【问题讨论】:

  • A ResultSet object is automatically closed when the Statement object that generated it is closed阅读here
  • 2.A) 这段代码正确吗? 不,close 可能会抛出 RuntimeException。在 try-with-resources 之前,我使用了嵌套的 try/finally 块,因此每个资源都有自己的 finally。
  • 我认为你的教授说自己关闭 ResultSet 是对的。依赖其他代码来做是危险的。你怎么知道在未来的版本中他们不会改变它,所以 ResultSets 不会自动关闭?另外,我会捕获 Throwable 而不是 SqlException。它将涵盖所有可能的场景。
  • @Trollkemada RuntimeException 可以总是被抛出。您不希望有错误的驱动程序抛出 NullPointerException 来阻止资源被释放。
  • 已知一些 JDBC 驱动程序在关闭连接时无法正确关闭 ResultSet 的问题。因此,虽然理论上您对文档的解释是正确的,说您不需要明确关闭 ResultSet,但实际上您应该这样做。

标签: java correctness try-with-resources


【解决方案1】:

这确实是 try-with-resources 的主要动机。请参阅 Java tutorials 作为参考。你的教授已经过时了。如果您想处理结果集问题,您始终可以将其包含在另一个 try-with-resources 语句中。

【讨论】:

    【解决方案2】:

    关于 JDBC 代码的有趣之处在于,您正在按照规范进行编码,但并不总是清楚您的实现是否符合规范。有很多不同的数据库和驱动程序,有些驱动程序比其他驱动程序表现更好。这往往使人们在谨慎方面犯错,建议明确关闭所有内容。您可以只关闭此处的连接。为了安全起见而关闭结果集是很难争论的。您没有在此处说明您使用的是什么数据库或驱动程序,我不想硬编码对某些实现可能无效的驱动程序的假设。

    按顺序关闭事物确实会让您面临可能引发异常并导致跳过某些关闭的问题。您对此感到担忧是对的。

    请注意,这是一个玩具示例。大多数实际代码使用连接池,其中调用 close 方法实际上并没有关闭连接,而是将连接返回到池中。因此,一旦您使用池,资源可能不会关闭。如果您想更改此代码以使用连接池,那么您至少必须返回并关闭语句。

    另外,如果您反对这种冗长,那么答案就是将代码隐藏在使用策略、resultSet 映射器、准备好的语句设置器等的可重用实用程序中。当然,这一切都已经完成了;您将走上重塑 Spring JDBC 的道路。

    说到这一点:Spring JDBC 显式地关闭了所有内容(可能是因为它需要与尽可能多的驱动程序一起工作,并且不希望由于某些驱动程序的表现不佳而导致问题)。

    【讨论】:

    • 很好,清晰,简洁的解释。喜欢,先生。
    【解决方案3】:

    您可以创建一个实用程序类来处理这些资源的关闭。 IE 仅供参考,我只是在尝试关闭 util 类中的资源时忽略了 SQLExceptions,但您也可以在完成关闭集合中的资源后根据您的需要记录或收集并抛出它们

    public class DBUtil {
    public static void closeConnections(Connection ...connections){
        if(connections != null ){
            for(Connection conn : connections){
                if(conn != null){
                    try {
                        conn.close();
                    } catch (SQLException ignored) {
                        //ignored
                    }
                }
            }
        }
    }
    
    public static void closeResultSets(ResultSet ...resultSets){
        if(resultSets != null ){
            for(ResultSet rs: resultSets){
                if(rs != null){
                    try {
                        rs.close();
                    } catch (SQLException ignored) {
                        //ignored
                    }
                }
            }
        }
    }
    
    public static void closeStatements(Statement ...statements){
        if(statements != null){
            for(Statement statement : statements){
                if(statement != null){
                    try {
                        statement.close();
                    } catch (SQLException ignored) {
                        //ignored
                    }
                }
            }
        }
    }
    

    }

    然后从你的方法中调用它:

        public void doQueries() throws MyException {
        Connection con = null;
        try {
            con = DriverManager.getConnection(dataSource);
            PreparedStatement s1 = null;
            PreparedStatement s2 = null;
            try {
                s1 = con.prepareStatement(updateSqlQuery);
                s2 = con.prepareStatement(selectSqlQuery);
    
                // Set the parameters of the PreparedStatements and maybe do other things
                s1.executeUpdate();
                ResultSet rs = null;
                try {
                    rs = s2.executeQuery();
                } finally {
                    DBUtil.closeResultSets(rs);
                }
            } finally {
                DBUtil.closeStatements(s2, s1);
            }
    
        } catch (SQLException e) {
            throw new MyException(e);
        } finally {
            DBUtil.closeConnections(con);
        }
    }
    

    【讨论】:

      【解决方案4】:

      我发现这是处理 JDBC 等资源的最佳解决方案。此方法提供了一个不可变的功能,通过利用最终变量,并且仅在需要时声明和分配这些变量,它的 CPU 效率非常高,并保证在所有情况下,无论状态如何,分配和打开的所有资源都是关闭的的例外。如果不仔细实施以解决所有场景,您正在使用的技术会留下可能导致资源泄漏的空白。如果始终遵循该模式,此技术不允许资源泄漏:
      1) 分配资源
      2) 试试
      3) 使用资源
      4) 最后关闭资源

      public void doQueries() throws MyException {
         try {
            final Connection con = DriverManager.getConnection(dataSource);
            try {
               final PreparedStatement s1 = con.prepareStatement(updateSqlQuery);
               try {
      
                  // Set the parameters of the PreparedStatements and maybe do other things
      
                  s1.executeUpdate();
      
               } finally {
                  try { s1.close(); } catch (SQLException e) {}
               }
      
               final PreparedStatement s2 = con.prepareStatement(selectSqlQuery);
               try {
      
                  // Set the parameters of the PreparedStatements and maybe do other things
      
                  final ResultSet rs = s2.executeQuery();
                  try {
      
                     // Do something with rs
      
                  } finally {
                     try { rs.close(); } catch (SQLException e) {}
                  }
               } finally {
                  try { s2.close(); } catch (SQLException e) {}
               }
            } finally {
               try { con.close(); } catch (SQLException e) {}
            }
         } catch (SQLException e) {
            throw new MyException(e);
         }
      }
      

      在 Java 7 中,您可以利用新的 try -with-resources 来进一步简化此过程:新的 try -with-resources 遵循上述逻辑流程,因为它将保证所有资源都包含在 with resources 块中被分配关闭。在 with resources 块中抛出的任何异常都将被抛出,但那些分配的资源仍将被关闭。这段代码非常简化,如下所示:

      public void doQueries() throws MyException {
         try (
            final Connection con = DriverManager.getConnection(dataSource);
            final PreparedStatement s1 = con.prepareStatement(updateSqlQuery);
            final PreparedStatement s2 = con.prepareStatement(selectSqlQuery);
            final ResultSet rs = s2.executeQuery()) {
      
            s1.executeUpdate();
      
               // Do something with rs
      
         } catch (SQLException e) {
            throw new MyException(e);
         }
      }
      

      [编辑]:将 rs 分配移到资源块中以显示最简单的实现。在实践中,这个简单的解决方案并没有真正起作用,因为它效率不高。连接应该被重用,因为建立连接是一项非常昂贵的操作。此外,这个简单的示例没有将查询参数分配给准备好的语句。应注意处理这些情况,因为资源块应该只包含赋值语句。为了描述这一点,我还添加了另一个示例

         public void doQueries() throws MyException {
      
            final String updateSqlQuery = "select @@servername";
            final String selecSqlQuery  = "select * from mytable where col1 = ? and col2 > ?";
            final Object[] queryParams  = {"somevalue", 1};
      
            try (final Connection con = DriverManager.getConnection(dataSource);
               final PreparedStatement s1 = newPreparedStatement(con, updateSqlQuery);
               final PreparedStatement s2 = newPreparedStatement(con, selectSqlQuery, queryParams);
               final ResultSet rs = s2.executeQuery()) {
      
               s1.executeUpdate();
      
               while (!rs.next()) {
                  // do something with the db record.
               }
            } catch (SQLException e) {
               throw new MyException(e);
            }
         }
      
         private static PreparedStatement newPreparedStatement(Connection con, String sql, Object... args) throws SQLException
         {
            final PreparedStatement stmt = con.prepareStatement(sql);
            for (int i = 0; i < args.length; i++)
               stmt.setObject(i, args[i]);
            return stmt;
         }
      

      【讨论】:

      • 你能证明为什么最后一个 try-with-resources 是必要的吗? (ResultSet 的那个)
      • 再看一遍我没有看到我的答案。因为我展示了两种方式的示例,因为有两个语句。您可以在资源块中完成所有操作。关键是您不应该从资源块中分配语句参数,而是应该使用函数来构造语句,因此块中的唯一代码是可关闭资源的分配。在这种文字情况下,您只需要使用资源进行一次尝试。
      【解决方案5】:

      tl;博士

      • 理论上关闭语句会关闭结果集。
      • 在实践中,众所周知,一些错误的 JDBC 驱动程序实现未能做到这一点。因此,她从硬敲门学校学到的教练的建议。除非您熟悉可能为您的应用部署的每个 JDBC driver 的每个实现,否则请使用 try-with-resources 自动关闭您的 JDBC 工作的每个级别,例如语句和结果集。

      使用 try-with-resources 语法

      您的所有代码都完全使用try-with-resources。在 try-with-resources 语法中,在大括号前的括号中声明和实例化 ConnectionPreparedStatementResultSet。见Tutorial by Oracle

      虽然您的 ResultSet 在上一个代码示例中没有被显式关闭,但它应该在其语句关闭时间接关闭。但正如下面所讨论的,它可能没有因为错误的 JDBC 驱动程序而被关闭。

      AutoCloseable

      任何实现AutoCloseable 的对象都将自动调用其close 方法。所以不需要那些finally 子句。

      对于阅读本文的人文学科专业的学生,​​是的,Java 团队拼错了“可关闭”。

      您如何知道哪些对象是可自动关闭的,哪些不是?查看他们的类文档,看看它是否将AutoCloseable 声明为超级接口。相反,请参阅the JavaDoc page for AutoCloseable 以获取所有捆绑的子接口和实现类的列表(实际上有几十个)。

      例如,对于 SQL 工作,我们看到 ConnectionStatementPreparedStatementResultSetRowSet 都是可自动关闭的,但 DataSource 不是。这是有道理的,因为DataSource 存储有关潜在资源(数据库连接)的数据,但它本身并不是资源。 DataSource 永远不会“打开”,因此无需关闭。

      请参阅 Oracle 教程,The try-with-resources Statement

      代码示例

      您的最后一个代码示例已经接近完美,但应该将 ResultSet 包装在 try-with-resources 语句中以自动关闭。

      引用ResultSetJavaDoc:

      当生成它的 Statement 对象关闭、重新执行或用于从多个结果序列中检索下一个结果时,ResultSet 对象会自动关闭。

      正如您的老师所建议的那样,一些 JDBC 驱动程序存在严重缺陷,它们未能兑现 JDBC 规范的承诺,即在 StatementPreparedStatement 关闭时关闭 ResultSet。如此多的程序员养成了明确关闭每个ResultSet 对象的习惯。

      现在使用 try-with-resources 语法可以更轻松地完成这项额外任务。在实际工作中,您可能会对所有AutoCloseable 对象(例如ResultSet)进行尝试。所以我自己的看法是:为什么不把它做成一个try-with-resources + else?不会造成伤害,让您的代码更能自我记录您的意图,如果您的代码遇到错误的 JDBC 驱动程序之一,它可能会有所帮助。 唯一的成本是一对括号,假设无论如何你都有一个 try-catch-else。

      Oracle Tutorial 中所述,一起声明的多个AutoCloseable 对象将按相反顺序关闭,正如我们所希望的那样。

      提示:try-with-resources 语法允许在最后声明的资源项上使用可选的分号。我把分号作为一种习惯包括在内,因为它读起来很好,是一致的,并且便于剪切和粘贴编辑。我将它包含在您的 PreparedStatement s2 行中。

      public void doQueries() throws MyException{
          // First try-with-resources.
          try ( Connection con = DriverManager.getConnection( dataSource ) ;
                PreparedStatement s1 = con.prepareStatement( updateSqlQuery ) ;
                PreparedStatement s2 = con.prepareStatement( selectSqlQuery ) ;
          ) {
      
              … Set parameters of PreparedStatements, etc.
      
              s1.executeUpdate() ;
      
              // Second try-with-resources, nested within first.
              try (
                  ResultSet rs = s2.executeQuery() ;
              ) {
                  … process ResultSet
              } catch ( SQLException e2 ) {  
                  … handle exception related to ResultSet.
              }
      
          } catch ( SQLException e ) {  
              … handle exception related to Connection or PreparedStatements.
          }
      }
      

      我想这种工作可能会在未来的编程语言中发明一种更优雅的语法。但是现在,我们有 try-with-resources,我很乐意使用它。虽然 try-with-resources 并不完美,但它比旧语法有了很大的改进。

      顺便说一句,Oracle 建议使用DataSource 实现来获取连接,而不是在您的代码中看到的DriverManager 方法。在整个代码中使用DataSource 可以更轻松地切换驱动程序或切换到连接池。查看您的 JDBC 驱动程序是否提供了DataSource 的实现。

      更新:Java 9

      现在在 Java 9 中,您可以在 try-with-resources 之前初始化资源。见this article。这种灵活性在某些情况下可能很有用。

      【讨论】:

      • 我仍然觉得很烦人,因为人们(数据库实现)不遵循文档,我不得不让我的 ResultSet 代码更混乱......无论如何,谢谢你的好回答
      • 在 2018 年遇到了这个答案,这是写得最漂亮的答案之一。我遇到了类似的问题,多亏了这个答案,我的概念很清楚。
      • 非常好的解释,谢谢。
      • 为什么 s1.executeUpdate() 没有用 try-catch 包装?
      • @fire_water 声明 s1.executeUpdate() is 确实包含在 try-catch 中,外部 try-with-resources 定义了三个资源(ConnectionPreparedStatement 对象)。如果那个executeUpdate调用抛出异常,异常会被底部的catch子句捕获,三个资源(cons1s2)会被关闭。
      【解决方案6】:

      我更喜欢让 Java 自动关闭。所以当我必须为 ResultSet 设置值时,我会这样做。

      try (Connection conn = DB.getConn();
           PreparedStatement ps = conn.prepareStatement("SELECT * FROM x WHERE y = ?")
      ) {
          ps.setString(1, "yValue");
         
          try(ResultSet rs = ps.executeQuery()) {
              while(rs.next()) {
                  ...
              }
          }
      } catch (SQLException e) {
          e.printStackTrace(e);
          ...
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2019-03-12
        • 1970-01-01
        • 2017-12-04
        • 2020-12-02
        • 2020-01-01
        • 2016-09-29
        • 2014-12-18
        相关资源
        最近更新 更多