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

2024-02-22

我正在使用 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);
        }
    }

查找错误问题:

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

有人可以告诉我这到底是什么吗?我们该如何解决这个问题?

如有帮助,将不胜感激:)


FindBugs 的说法是正确的潜在的泄漏在例外情况下,因为setInt http://docs.oracle.com/javase/7/docs/api/java/sql/PreparedStatement.html#setInt%28int,%20int%29 and 设置字符串 http://docs.oracle.com/javase/7/docs/api/java/sql/PreparedStatement.html#setString%28int,%20java.lang.String%29被声明抛出“SQLException”。如果这些行中的任何一行抛出 SQLException,则PreparedStatement 就会泄漏,因为没有范围块可以访问它来关闭它。

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

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由于没有给出用户或其他一些自定义异常说UserNotLoggedInException被从内心深处抛出getUserId().

这是一个示例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 对象。不使用 catch 块并重新抛出,因为检查 throwable 的类型可能会失败 https://stackoverflow.com/questions/13883166/uncatchable-chucknorrisexception在超级罕见的情况下。

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

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

关于春天,说说你的user.getUserId()方法可能会抛出 IllegalStateException 或者给定的用户是null。根据合同,Spring 没有指定如果发生什么情况抛出 java.lang.RuntimeException 或 java.lang.Error http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/jdbc/core/PreparedStatementCreator.html#createPreparedStatement-java.sql.Connection-来自PreparedStatementCreator。根据文档:

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

这个措辞暗示 Spring 依赖于connection.close() 完成工作 https://stackoverflow.com/questions/4507440/must-jdbc-resultsets-and-statements-be-closed-separately-although-the-connection.

让我们进行概念验证来验证 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)

正如您所看到的,每个分配都是平衡的,并且关闭释放资源。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

方法可能无法清理已检查异常的流或资源 - FindBugs 的相关文章

随机推荐