FindBugs 关于异常情况下的潜在泄漏 是正确的,因为setInt 和setString 被声明为抛出“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 由于没有给定用户或其他一些自定义异常(例如 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 对象。不使用捕获块并重新抛出,因为在极少数情况下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)
如您所见,每次分配都与关闭以释放资源平衡。