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

Unk*_*own 7 java spring findbugs jdbctemplate

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

码:

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);
        }
    }
Run Code Online (Sandbox Code Playgroud)

FindBugs问题:

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

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

帮助将不胜感激:)

jme*_*ens 5

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

为了更好地理解这个问题,让我们通过去掉spring类型并内联方法来分解代码错觉,这种方法是调用返回资源的方法时callstack作用域的工作方式的近似值.

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.
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

回到最初的问题,如果user是null导致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;
    }
Run Code Online (Sandbox Code Playgroud)

所以在这个例子中,只有在出现问题时才会关闭语句.否则返回一个打开的声明供调用者清理.在catch块上使用finally块,因为错误的驱动程序实现可以抛出的不仅仅是SQLException对象.不使用捕获块和重新抛出,因为在极少数情况下检查throwable的类型可能会失败.

在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;
    }
Run Code Online (Sandbox Code Playgroud)

关于Spring,假设您的user.getUserId()方法可能抛出IllegalStateException或给定的用户null.在契约方面,Spring没有指定如果从PreparedStatementCreator 抛出java.lang.RuntimeException或java.lang.Error会发生什么.根据文档:

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

这个措辞意味着Spring依赖于connection.close()来完成这项工作.

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

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;
    }
Run Code Online (Sandbox Code Playgroud)

结果输出是:

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.");
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

如您所见,连接是对预准备语句的唯一引用.

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

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)
Run Code Online (Sandbox Code Playgroud)

结果输出是:

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.");
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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


Tom*_*m G 0

PreparedStatement是一种Closeable资源。然而,看起来 JDBC 模板负责关闭它——因此 FindBugs 很可能偶然发现了误报。