当前位置: 首页 > 知识库问答 >
问题:

方法可能无法在检查异常时清理流或资源——FindBugs

归浩博
2023-03-14

我正在使用Spring JDBCTemboard访问数据库中的数据,它工作正常。但是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);
        }
    }

FindBugs问题:

方法可能无法清理流或资源的检查异常在这一行准备语句ps=connection.prepare语句(插入查询, new String[]{"id" });

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

我们将不胜感激:)

共有3个答案

唐伟
2023-03-14

PreparedStatement是一个可关闭的资源。然而,看起来JDBC模板负责关闭它——因此FindBugs可能偶然发现了一个假阳性。

弓宏茂
2023-03-14

是的,这看起来像是一个假阳性,FindBugs团队希望听到这个假阳性,这样他们可以调整这个警告。他们在其他测试中为第三方方法添加了特定的异常,我希望这将以同样的方式处理。您可以提交错误报告或向团队发送电子邮件。

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

@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)

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

禄奇希
2023-03-14

FindBugs关于异常情况下潜在泄漏的说法是正确的,因为setInt和setString被声明为抛出“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,由于没有用户给定或其他一些自定义异常(比如UserNotLoggedInExceptiongetUserId()的深层抛出),导致NullPointerException也是如此。

下面是一个解决此问题的丑陋修复示例:

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

因此,在本例中,它仅在出现问题时关闭语句。否则返回一个打开的语句供调用者清理。最后一个块用于捕获块,因为错误驱动程序实现可以抛出不仅仅是SQLException对象。不使用捕获块和重新抛出,因为在极少数情况下检查可抛出的类型可能会失败。

在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,请告诉你的用户。getUserId()方法可能会引发IllegalStateException,或者给定的用户是null。根据合同规定,Spring不指定如果java。lang.RuntimeException或java。lang.错误从PreparedStatementCreator抛出。根据文件:

实现不需要担心它们尝试的操作可能引发的SQLException。JdbcTemplate类将捕获并适当处理SQLException。

这种措辞暗示Spring依赖connection.close()来完成这项工作。

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

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)

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

 类似资料:
  • 它显示了这个特定声明的问题。你能帮帮我吗?

  • 我代码中的以下语句被SonarQube标记为一个关键问题。stmt=conn.createStatement(); 确切的问题是,“方法可能无法清理java.sql.检查异常的语句”。 下面是代码片段: SonarQube给出的规则如下: 方法可能无法在检查异常时清理流或资源此方法可能无法清理(关闭、处置)需要显式清理操作的流、数据库对象或其他资源。一般情况下,如果一个方法打开一个流或其他资源,该

  • 我正在使用FileWrite类写入文件。而且效果很好。但是FindBugs在我的代码片段中指出了一个小问题。 代码片段: Findbug报告: OBL_未满足_义务:方法可能无法清理流,或者资源WriteDataFile(字符串)可能无法清理java。伊奥。已检查异常的写入程序 我在哪一行遇到这个错误? 有人能告诉我这到底是什么吗?我们如何解决这个问题?

  • 本文向大家介绍如何快速清理 docker 资源的方法,包括了如何快速清理 docker 资源的方法的使用技巧和注意事项,需要的朋友参考一下 如果经常使用 docker,你会发现 docker 占用的资源膨胀很快,其中最明显也最容易被察觉的应该是对磁盘空间的占用。本文将介绍如何快速的清理 docker 占用的系统资源,具体点说就是删除那些无用的 镜像、容器、网络和数据卷。 查看 docker 占用的

  • 问题内容: 在Java线程中,“运行”方法不能引发“检查的异常”。我在Core Java(第1卷)书中遇到了这个问题。有人可以解释其背后的原因吗? 问题答案: 有人可以解释其背后的原因吗? 是的,因为您抛出的任何异常都会被JVM仔细忽略。因此,将其抛出可能是一个错误(除非您对该线程具有特定的异常处理程序,请参阅有关该文档的文档)。没有理由煽动潜在的错误行为。 或者举个例子。 编辑 为什么父线程不能

  • 我正试图修改PySpark dataframe中的列值,如下所示: 这将生成以下异常: 调用O435时出错。跟踪:py4j.py4jException:Method或([class java.lang.string])在py4j.reflection.reflectionEngine.getMethod(reflectionEngine.java:318)在py4j.reflection.refl