方法可能无法清除已检查异常上的流或资源 - FindBugs

时间:2014-05-30 18:52:34

标签: java spring jdbctemplate findbugs

我正在使用Spring JDBCTemplate来访问数据库中的数据,并且它正常工作。但FindBugs在我的代码片段中指出了一个小问题。

CODE:

public String createUser(final User user) {
        try { 
            final String insertQuery = "insert into user (id, username, firstname, lastname) values (?, ?, ?, ?)";
            KeyHolder keyHolder = new GeneratedKeyHolder();
            jdbcTemplate.update(new PreparedStatementCreator() {
                public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
                    PreparedStatement ps = connection.prepareStatement(insertQuery, new String[] { "id" });
                    ps.setInt(1, user.getUserId());
                    ps.setString(2, user.getUserName());
                    ps.setString(3, user.getFirstName());
                    ps.setInt(4, user.getLastName());
                    return ps;
                }
            }, keyHolder);
            int userId = keyHolder.getKey().intValue();
            return "user created successfully with user id: " + userId;
        } catch (DataAccessException e) {
            log.error(e, e);
        }
    }

FindBugs问题:

方法可能无法清除此行PreparedStatement ps = connection.prepareStatement(insertQuery, new String[] { "id" });

中已检查异常的流或资源

有人可以告诉我这究竟是什么?我们如何解决这个问题?

帮助将不胜感激:)

4 个答案:

答案 0 :(得分:5)

FindBugs对异常情况下的潜在泄漏是正确的,因为setIntsetString被声明为抛出'SQLException'。如果这些行中的任何一行抛出SQLException,则PreparedStatement会泄露,因为没有可以关闭它的范围块。

为了更好地理解这个问题,让我们通过删除spring类型来细分代码错觉,并以调用返回资源的方法的方式内联方法来描述callstack作用域的工作方式。

public void leakyMethod(Connection con) throws SQLException {
    PreparedStatement notAssignedOnThrow = null;
    try {
        PreparedStatement inMethod = con.prepareStatement("select * from foo where key = ?");
        //If we made it here a resource was allocated.
        inMethod.setString(1, "foo"); //<--- This can throw which will skip next line.
        notAssignedOnThrow = inMethod; //return from createPreparedStatement method call.
    } finally {
        if (notAssignedOnThrow != null) { //No way to close because it never 
            notAssignedOnThrow.close();   //made it out of the try block statement.
        }
    }
}

回到原始问题,如果user为空则导致NullPointerException由于没有给定用户或其他一些自定义异常而导致UserNotLoggedInException从深处抛出,则情况也是如此在getUserId()内。

以下是此问题的丑陋修复示例:

    public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
        boolean fail = true;
        PreparedStatement ps = connection.prepareStatement(insertQuery, new String[] { "id" });
        try {
            ps.setInt(1, user.getUserId());
            ps.setString(2, user.getUserName());
            ps.setString(3, user.getFirstName());
            ps.setInt(4, user.getLastName());
            fail = false;
        } finally {
            if (fail) {
                try {
                   ps.close();
                } catch(SQLException warn) {
                }
            }
        }
        return ps;
    }

因此,在此示例中,只有在出现问题时才会关闭语句。否则返回一个打开的声明供调用者清理。在catch块上使用finally块,因为错误的驱动程序实现可以抛出的不仅仅是SQLException对象。由于inspecting type of a throwable can fail在极少数情况下不使用Catch block和rethrow。

在JDK 7及更高版本中,您可以编写如下补丁:

public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
        PreparedStatement ps = connection.prepareStatement(insertQuery, new String[] { "id" });
        try {
            ps.setInt(1, user.getUserId());
            ps.setString(2, user.getUserName());
            ps.setString(3, user.getFirstName());
            ps.setInt(4, user.getLastName());
        } catch (Throwable t) {    
            try {
               ps.close();
            } catch (SQLException warn) {
                if (t != warn) {
                    t.addSuppressed(warn);
                }
            }
            throw t;
        }
        return ps;
    }

关于Spring,假设你的user.getUserId()方法可能抛出IllegalStateException或者给定的用户是null。在契约方面,Spring没有说明如果来自PreparedStatementCreator的java.lang.RuntimeException or java.lang.Error is thrown会发生什么。根据文档:

  

实现不需要关心可能从它们尝试的操作抛出的SQLExceptions。 JdbcTemplate类将适当地捕获和处理SQLExceptions。

这个措辞意味着Spring依赖于connection.close() doing the work

让我们进行概念验证,以验证Spring文档所承诺的内容。

public class LeakByStackPop {
    public static void main(String[] args) throws Exception {
        Connection con = new Connection();
        try {
            PreparedStatement ps = createPreparedStatement(con);
            try {

            } finally {
                ps.close();
            }
        } finally {
            con.close();
        }
    }

    static PreparedStatement createPreparedStatement(Connection connection) throws Exception {
        PreparedStatement ps = connection.prepareStatement();
        ps.setXXX(1, ""); //<---- Leak.
        return ps;
    }

    private static class Connection {

        private final PreparedStatement hidden = new PreparedStatement();

        Connection() {
        }

        public PreparedStatement prepareStatement() {
            return hidden;
        }

        public void close() throws Exception {
            hidden.closeFromConnection();
        }
    }

    private static class PreparedStatement {


        public void setXXX(int i, String value) throws Exception {
            throw new Exception();
        }

        public void close() {
            System.out.println("Closed the statement.");
        }

        public void closeFromConnection() {
            System.out.println("Connection closed the statement.");
        }
    }
}

结果输出为:

Connection closed the statement.
Exception in thread "main" java.lang.Exception
    at LeakByStackPop$PreparedStatement.setXXX(LeakByStackPop.java:52)
    at LeakByStackPop.createPreparedStatement(LeakByStackPop.java:28)
    at LeakByStackPop.main(LeakByStackPop.java:15)

正如您所看到的,连接是对预准备语句的唯一引用。

让我们通过修补伪造的'PreparedStatementCreator'方法更新示例以修复内存泄漏。

public class LeakByStackPop {
    public static void main(String[] args) throws Exception {
        Connection con = new Connection();
        try {
            PreparedStatement ps = createPreparedStatement(con);
            try {

            } finally {
                ps.close();
            }
        } finally {
            con.close();
        }
    }

    static PreparedStatement createPreparedStatement(Connection connection) throws Exception {
        PreparedStatement ps = connection.prepareStatement();
        try {
            //If user.getUserId() could throw IllegalStateException
            //when the user is not logged in then the same leak can occur.
            ps.setXXX(1, "");
        } catch (Throwable t) {
            try {
                ps.close();
            } catch (Exception suppressed) {
                if (suppressed != t) {
                   t.addSuppressed(suppressed);
                }
            }
            throw t;
        }
        return ps;
    }

    private static class Connection {

        private final PreparedStatement hidden = new PreparedStatement();

        Connection() {
        }

        public PreparedStatement prepareStatement() {
            return hidden;
        }

        public void close() throws Exception {
            hidden.closeFromConnection();
        }
    }

    private static class PreparedStatement {


        public void setXXX(int i, String value) throws Exception {
            throw new Exception();
        }

        public void close() {
            System.out.println("Closed the statement.");
        }

        public void closeFromConnection() {
            System.out.println("Connection closed the statement.");
        }
    }
}

结果输出为:

Closed the statement.
Exception in thread "main" java.lang.Exception
Connection closed the statement.
    at LeakByStackPop$PreparedStatement.setXXX(LeakByStackPop.java:63)
    at LeakByStackPop.createPreparedStatement(LeakByStackPop.java:29)
    at LeakByStackPop.main(LeakByStackPop.java:15)

正如您所看到的那样,每个分配都是平衡的,并且接近释放资源。

答案 1 :(得分:2)

是的,这看起来像是FindBugs团队希望听到的误报,因此他们可以调整此警告。他们在其他测试中为第三方方法添加了特定的例外,我希望这将以相同的方式处理。您可以file a bug reportemail the team

但是,现在,您可以使用SuppressFBWarnings注释在这种情况下忽略此警告:

@SuppressFBWarnings("OBL_UNSATISFIED_OBLIGATION_EXCEPTION_EDGE")
public PreparedStatement createPreparedStatement...

为了提高可读性并允许重用警告,我发现在辅助类中定义常量很有帮助:

public final class FindBugs {
    final String UNCLOSED_RESOURCE = "OBL_UNSATISFIED_OBLIGATION_EXCEPTION_EDGE";

    private FindBugs() {
        // static only
    }
}

...

@SuppressFBWarnings(FindBugs.UNCLOSED_RESOURCE)

不幸的是,我无法定义忽略特定警告的注释。

答案 2 :(得分:0)

PreparedStatementCloseable资源。但是,看起来JDBC模板负责关闭它 - 所以FindBugs可能偶然发现了误报。

答案 3 :(得分:0)

Spring会关闭你的PreparedStatement,那部分不是问题。 Spring提供了一种传递创建PreparedStatement的回调的方法,Spring知道在完成后关闭它。具体来说,this answer承诺jdbcTemplate将关闭它:

  

JdbcTemplate将关闭创建的语句。

Spring也将处理SQLExceptions,同样的javadoc说:

  

不需要捕获可能在此方法的实现中抛出的SQLExceptions。 JdbcTemplate类将处理它们。

即使JdbcTemplate类将处理SQLExceptions,如果PreparedStatement在设置参数时抛出SQLException,则预准备语句不会被jdbcTemplate代码关闭。但是在这种情况下,你的问题比未公开的PreparedStatement更糟糕,你有一个 参数不匹配。

如果检查源代码,update方法将调用此execute方法:

@Override
public <T> T  [More ...] execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action)
        throws DataAccessException {

    Assert.notNull(psc, "PreparedStatementCreator must not be null");

    Assert.notNull(action, "Callback object must not be null");
    if (logger.isDebugEnabled()) {
        String sql = getSql(psc);
        logger.debug("Executing prepared SQL statement" + (sql != null ? " [" + sql + "]" : ""));
    }
    Connection con = DataSourceUtils.getConnection(getDataSource());
    PreparedStatement ps = null;
    try {
        Connection conToUse = con;
        if (this.nativeJdbcExtractor != null &&             this.nativeJdbcExtractor.isNativeConnectionNecessaryForNativePreparedStatements()) {
            conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
        }
        ps = psc.createPreparedStatement(conToUse);
        applyStatementSettings(ps);
        PreparedStatement psToUse = ps;
        if (this.nativeJdbcExtractor != null) {
            psToUse =     this.nativeJdbcExtractor.getNativePreparedStatement(ps);
        }
        T result = action.doInPreparedStatement(psToUse);
        handleWarnings(ps);
        return result;
    }
    catch (SQLException ex) {
        // Release Connection early, to avoid potential connection pool deadlock
        // in the case when the exception translator hasn't been initialized yet.
        if (psc instanceof ParameterDisposer) {
            ((ParameterDisposer) psc).cleanupParameters();
        }
        String sql = getSql(psc);
        psc = null;
        JdbcUtils.closeStatement(ps);
        ps = null;
        DataSourceUtils.releaseConnection(con, getDataSource());
        con = null;
        throw getExceptionTranslator().translate("PreparedStatementCallback", sql, ex);
    }
    finally {
        if (psc instanceof ParameterDisposer) {
             ((ParameterDisposer) psc).cleanupParameters();
        }
        JdbcUtils.closeStatement(ps);
        DataSourceUtils.releaseConnection(con, getDataSource());
    }
}

期望静态代码分析工具足够聪明以确保一切正常是不现实的,只有他们能够做到这一点。

对我来说,此代码的真正问题在于捕获并记录异常。不抛出异常会阻止Spring在发生错误时回滚事务。要么删除try-catch并抛出DataAccessException,要么(如果必须在此处记录),请在记录后重新抛出它。