【问题标题】:Method may fail to clean up stream or resource on checked exception -- FindBugs方法可能无法在检查异常时清理流或资源 - FindBugs
【发布时间】:2014-07-20 14:58:48
【问题描述】:

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

代码:

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

查找错误问题:

该行PreparedStatement ps = connection.prepareStatement(insertQuery, new String[] { "id" });中的检查异常可能无法清理流或资源

有人可以简要介绍一下这到底是什么吗?我们该如何解决这个问题?

我们将不胜感激:)

【问题讨论】:

  • 如果您尝试关闭 catch 块中的 ps 会发生什么?

标签: java spring jdbctemplate findbugs


【解决方案1】:

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

【讨论】:

  • 是的,兄弟。甚至我也这么想。作为 Spring 和 FindBugs 的新手,他们很想知道如何解决它。如果可以的话:)
【解决方案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)

很遗憾,我无法定义忽略特定警告的注释。

【讨论】:

  • 成功了,谢谢。我为此使用了以下依赖项。 com.google.code.findbugsannotations2.0.1
【解决方案3】:

FindBugs 关于异常情况下的潜在泄漏 是正确的,因为setIntsetString 被声明为抛出“SQLException”。如果这些行中的任何一行要抛出 SQLException,那么 PreparedStatement 就会泄漏,因为没有范围块可以访问它来关闭它。

为了更好地理解这个问题,让我们通过摆脱弹簧类型来打破代码错觉,并以一种近似于调用返回资源的方法时调用堆栈范围的工作方式的方式内联方法。

public void leakyMethod(Connection con) throws SQLException {
    PreparedStatement notAssignedOnThrow = null; //Simulate calling method storing the returned value.
    try { //Start of what would be createPreparedStatement method
        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 为 null 导致 NullPointerException 由于没有给定用户或其他一些自定义异常(例如 UserNotLoggedInExceptiongetUserId() 的深处抛出),情况也是如此。

以下是针对此问题的 ugly 修复示例:

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

所以在这个例子中,它只在出现问题时关闭语句。否则返回一个 open 语句供调用者清理。 finally 块在 catch 块上使用,因为有缺陷的驱动程序实现可以抛出的不仅仅是 SQLException 对象。不使用捕获块并重新抛出,因为在极少数情况下inspecting type of a throwable can fail

JDK 7 和 JDK 8 中,您可以这样编写补丁:

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

JDK 9 及更高版本中,您可以这样编写补丁:

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) { // closes statement on error
               throw t;
            }
        }
        return ps;
    }

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

实现不需要关心可能从他们尝试的操作中抛出的 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)

如您所见,每次分配都与关闭以释放资源平衡。

【讨论】:

  • 啊,是的,我想他们可以抛出异常“如果发生数据库访问错误”。不需要 fail 布尔值,因为在这种情况下您将始终输入 finally 块。
  • @DavidHarkness 错误的索引也会触发泄漏和另一个 FB 警告。需要fail boolen,因为我们想在正常情况下返回一个open语句。
  • 您返回了一个不好的封闭语句。去掉fail,从try块中返回语句,从catch块中抛出异常,简化逻辑。或者如果语句未打开,模板是否会抛出一个。
  • 该示例要么返回一个打开语句,要么关闭该语句,并允许从 setXXX 抛出的未决异常转义该方法。您可以简化此逻辑,但这些版本具有仍然允许发生泄漏的边缘情况。
  • 哦抱歉,错过了这是一个 finally 块。
【解决方案4】:

Spring 将关闭您的 PreparedStatement,这部分不是问题。 Spring 为您提供了一种方法来传递创建 PreparedStatement 的回调,Spring 知道一旦完成就关闭它。具体来说,api doc for the PreparedStatementCreator 承诺 jdbcTemplate 将关闭它:

JdbcTemplate 将关闭创建的语句。

Spring 也会处理 SQLExceptions,同一个 javadoc 说:

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

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

如果查看源代码,更新方法会调用这个执行方法:

@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 被抛出,或者(如果你必须在此处记录它)在记录后重新抛出它。

【讨论】:

    猜你喜欢
    • 2017-08-22
    • 1970-01-01
    • 2018-12-21
    • 2011-11-16
    • 1970-01-01
    • 1970-01-01
    • 2012-09-04
    • 2021-07-15
    • 1970-01-01
    相关资源
    最近更新 更多