【问题标题】:Database connection leak数据库连接泄漏
【发布时间】:2017-03-28 13:30:33
【问题描述】:

最近,我不得不测量我们软件的一些 SQL 请求所花费的时间。为此,我决定采用天真的方法,并通过调用 System.nanoTime() 来围绕查询。 在这样做的同时,我发现一个类(SqlSuggest)在 4 个非常相似的方法中包含 4 个非常相似的查询。我认为重构和重新组合公共部分是个好主意。

这造成了连接泄漏,连接不再关闭。我回滚了重构,但我仍然想了解我做错了什么。

SqlSuggest 类的第一个版本有 4 个方法 getSuggestListByItems、getDisplayValueByItems、getSuggestListByListID 和 getDisplayValueByListeID。这些方法中的每一个都通过另一个类打开和关闭连接、语句和结果集(在 finally 块中):DBAccess。

该类的第二个版本具有相同的 4 个方法,除了打开和关闭 Connection 之外,它们调用 2 个方法中的 1 个,具体取决于它们需要一个结果还是一个列表。

这 2 个方法(executeQueryGetString 和 executeQueryGetListOfStringArray)分别声明了 Connection、Statement 和 ResultSet,然后调用一个方法:executeQuery,其中 Connection 打开,创建 Statement 并返回 ResultSet。

然后第二层方法从 ResultSet 中提取数据并关闭所有内容。

我猜我错误地认为在一个方法中声明 Connection、Statement 和 ResultSet 可以让我从同一个方法中关闭它们。

这是旧的类(简化):

public class SqlSuggest {

    private final static Logger LOGGER = LogManager.getLogger();

    public List<String[]> getSuggestListByItems(String codeSite) {
        List<String[]> suggestList = new ArrayList<>();
        String query = "SELECT iDcode, designation FROM Mytable WHERE codeSite = " + codeSite; // Simplified query building
        DBAccess accesBD = new DBAccess();
        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;
        try {
            conn = accesBD.getConnection();
            stmt = conn.createStatement();
            rs = stmt.executeQuery(query);
            while (rs.next()) {
                suggestList.add(new String[]{rs.getString(1), rs.getString(2)});
            }
        } catch (NamingException | SQLException ex) {
            LOGGER.error("", ex);
        } finally {
            accesBD.closeResultSet(rs);
            accesBD.closeStatement(stmt);
            accesBD.closeConnection(conn);
        }
        return suggestList;
    }

    public String getDisplayValueByItems(String table, String codeSite) {
        String displayValue = null;
        String query = "SELECT iDcode, designation FROM " + table + " WHERE codeSite = " + codeSite; // Different query building
        DBAccess accesBD = new DBAccess();
        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;

        try {
            conn = accesBD.getConnection();
            stmt = conn.createStatement();
            rs = stmt.executeQuery(query);
            if (rs.next()) {
                displayValue = rs.getString(1);
            }
        } catch (NamingException | SQLException ex) {
            LOGGER.error("", ex);

        } finally {
            accesBD.closeResultSet(rs);
            accesBD.closeStatement(stmt);
            accesBD.closeConnection(conn);
        }

        return displayValue;
    }

    public List<String[]> getSuggestListByListID(String listeID, String table, String codeSite) {
        List<String[]> suggestList = new ArrayList<>();
        String query = "SELECT iDcode, designation FROM " + table + " WHERE codeSite = " + codeSite; // Different query building involving listID
        DBAccess accesBD = new DBAccess();
        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;
        try {
            conn = accesBD.getConnection();

            stmt = conn.createStatement();
            rs = stmt.executeQuery(query);
            while (rs.next()) {
                suggestList.add(new String[]{rs.getString(1), rs.getString(2)});
            }
        } catch (NamingException | SQLException ex) {
            LOGGER.error("", ex);
        } finally {
            accesBD.closeResultSet(rs);
            accesBD.closeStatement(stmt);
            accesBD.closeConnection(conn);
        }
        return suggestList;
    }

    public String getDisplayValueByListeID(String listeID, String table, String codeSite) {
        String displayValue = null;        
        String query = "SELECT iDcode, designation FROM " + table + " WHERE codeSite = " + codeSite; // Different query building involving listID
        DBAccess accesBD = new DBAccess();
        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;
        try {
            conn = accesBD.getConnection();
            stmt = conn.createStatement();
            rs = stmt.executeQuery(query);
            if (rs.next()) {
                displayValue = rs.getString(2);
            }
        } catch (NamingException | SQLException ex) {
            LOGGER.error("", ex);
        } finally {
            accesBD.closeResultSet(rs);
            accesBD.closeStatement(stmt);
            accesBD.closeConnection(conn);
        }


        return displayValue;
    }
}

这是新的类(有泄漏的类):

public class SqlSuggest {

    private static final Logger LOGGER = LogManager.getLogger();

    public List<String[]> getSuggestListByItems(String codeSite) {
        List<String[]> suggestList;
        String query = "SELECT iDcode, designation FROM Mytable WHERE codeSite = " + codeSite; // Simplified query building
        suggestList = executeQueryGetListOfStringArray(query);
        return suggestList;
    }

    public String getDisplayValueByItems(String table, String codeSite) {
        String displayValue = null;
        String query = "SELECT iDcode, designation FROM " + table + " WHERE codeSite = " + codeSite; // Different query building
        int columnToFetch = 1;
        displayValue = executeQueryGetString(query, columnToFetch);
        return displayValue;
    }

    public List<String[]> getSuggestListByListID(String listeID, String table, String codeSite) {
        List<String[]> suggestList = new ArrayList<>();
        String query = "SELECT iDcode, designation FROM " + table + " WHERE codeSite = " + codeSite; // Different query building involving listID
        suggestList = executeQueryGetListOfStringArray(query);
        return suggestList;
    }

    public String getDisplayValueByListeID(String listeID, String table, String codeSite) {
        String displayValue = null;
        String query = "SELECT iDcode, designation FROM " + table + " WHERE codeSite = " + codeSite; // Different query building involving listID
        int columnToFetch = 2;
        displayValue = executeQueryGetString(query, columnToFetch);
        return displayValue;
    }

    private String executeQueryGetString(String query, int columnToFetch){
        String result = null;
        Statement stmt = null;
        Connection conn = null;
        ResultSet rs = executeQuery(query, stmt, conn);
        try{
            if (rs.next()) {
                result = rs.getString(columnToFetch);
            }
        } catch (SQLException ex) {
            LOGGER.error("", ex);
        } finally {
            DBAccess accesBD = new DBAccess();
            accesBD.closeResultSet(rs);
            accesBD.closeStatement(stmt);
            accesBD.closeConnection(conn);
        }
        return result;
    }

    private List<String[]> executeQueryGetListOfStringArray(String query){
        List<String[]> result = new ArrayList<>();
        Statement stmt = null;
        Connection conn = null;
        ResultSet rs = executeQuery(query, stmt, conn);
        try{
            while (rs.next()) {
                result.add(new String[]{rs.getString(1), rs.getString(2)});
            }
        } catch (SQLException ex) {
            LOGGER.error("", ex);
        } finally {
            DBAccess accesBD = new DBAccess();
            accesBD.closeResultSet(rs);
            accesBD.closeStatement(stmt);
            accesBD.closeConnection(conn);
        }
        return result;
    }

    private ResultSet executeQuery(String query, Statement stmt, Connection conn){
        DBAccess accesBD = new DBAccess();
        ResultSet rs = null;
        try {
            conn = accesBD.getConnection();
            stmt = conn.createStatement();
            rs = stmt.executeQuery(query);   
        } catch (NamingException | SQLException ex) {
            LOGGER.error("", ex);
        }
        return rs;
    }
}

这里是实际打开和关闭连接的类(没有以任何方式改变):

    public class DBAccess {

    private final static Logger LOGGER = LogManager.getLogger();

    public void closeResultSet(ResultSet rs) {
        if (rs != null) {
            try {
                rs.close();
            } catch (Exception e) {
                LOGGER.error("", e);
            }
        }
    }

    public void closeStatement(Statement stmt) {
        if (stmt != null) {
            try {
                stmt.close();
            } catch (Exception e) {
                LOGGER.error("", e);
            }
        }
    }

    public void closeConnection(Connection conn) {
        if (conn != null) {
            try {
                conn.close();
            } catch (Exception e) {
                LOGGER.error("", e);
            }
        }
    }

    public Connection getConnection() throws SQLException, NamingException {
        Connection cnx = null;
        PropertiesManager manager = PropertiesDelegate.getPropertiesManager();
        String jndi = manager.getProperty("datasource.name", "QuartisWeb-PU");
        Context ctx = null;
        DataSource dataSource = null;
        try {
            ctx = new InitialContext();
            dataSource = (DataSource) ctx.lookup(jndi);
        } catch (NamingException ex) {
            try {
                dataSource = (DataSource) ctx.lookup("java:comp/env/" + jndi);
            } catch (NamingException ex1) {
                LOGGER.error("", ex1);
            }
        }
        if (dataSource != null) {
            cnx = dataSource.getConnection();
        }
        return cnx;
    }

}

请忽略查询中未正确设置这些参数的事实,我知道应该这样做。

【问题讨论】:

  • 我强烈推荐使用现有的框架(也许 Apache Commons Db 有像 'closeQuietly' 这样的方法以正确的顺序关闭你的语句、连接和结果集),或者 SpringJdbc,它封装了很多以上
  • 是的,这就是我对下一个项目的想法。但是这个已经 10 年了,它正在生产中,我们有比修复工作更紧迫的事情。总有一天……
  • @BrianAgnew 或者使用try-with-resources,效果和`closeQuietly不完全一样,但更简洁。

标签: java jdbc


【解决方案1】:

当您从executeQueryGetString() 调用executeQuery() 时,connstmt 的值将在executeQuery() 内部发生变化,但在返回时将保持nullexecuteQueryGetString() 内部。因此,在executeQueryGetString()finally 处,对accesBD.closeConnection(conn) 的调用实际上是在执行accesBD.closeConnection(null)

【讨论】: