我正在使用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);
}
}
FindBugs问题:
方法可能无法清除此行PreparedStatement ps = connection.prepareStatement(insertQuery, new String[] { "id" });
有人可以告诉我这究竟是什么?我们如何解决这个问题?
帮助将不胜感激:)
答案 0 :(得分:5)
FindBugs对异常情况下的潜在泄漏是正确的,因为setInt和setString被声明为抛出'SQLException'。如果这些行中的任何一行抛出SQLException,则PreparedStatement会泄露,因为没有可以关闭它的范围块。
为了更好地理解这个问题,让我们通过删除spring类型来细分代码错觉,并以调用返回资源的方法的方式内联方法来描述callstack作用域的工作方式。
public void leakyMethod(Connection con) throws SQLException {
PreparedStatement notAssignedOnThrow = null;
try {
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
为空则导致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;
}
因此,在此示例中,只有在出现问题时才会关闭语句。否则返回一个打开的声明供调用者清理。在catch块上使用finally块,因为错误的驱动程序实现可以抛出的不仅仅是SQLException对象。由于inspecting type of a throwable can fail在极少数情况下不使用Catch block和rethrow。
在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;
}
关于Spring,假设你的user.getUserId()
方法可能抛出IllegalStateException或者给定的用户是null
。在契约方面,Spring没有说明如果来自PreparedStatementCreator的java.lang.RuntimeException or java.lang.Error is thrown会发生什么。根据文档:
实现不需要关心可能从它们尝试的操作抛出的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)
正如您所看到的那样,每个分配都是平衡的,并且接近释放资源。
答案 1 :(得分:2)
是的,这看起来像是FindBugs团队希望听到的误报,因此他们可以调整此警告。他们在其他测试中为第三方方法添加了特定的例外,我希望这将以相同的方式处理。您可以file a bug report或email 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)
不幸的是,我无法定义忽略特定警告的注释。
答案 2 :(得分:0)
PreparedStatement
是Closeable
资源。但是,看起来JDBC模板负责关闭它 - 所以FindBugs可能偶然发现了误报。
答案 3 :(得分:0)
Spring会关闭你的PreparedStatement,那部分不是问题。 Spring提供了一种传递创建PreparedStatement的回调的方法,Spring知道在完成后关闭它。具体来说,this answer承诺jdbcTemplate将关闭它:
JdbcTemplate将关闭创建的语句。
Spring也将处理SQLExceptions,同样的javadoc说:
不需要捕获可能在此方法的实现中抛出的SQLExceptions。 JdbcTemplate类将处理它们。
即使JdbcTemplate类将处理SQLExceptions,如果PreparedStatement在设置参数时抛出SQLException,则预准备语句不会被jdbcTemplate代码关闭。但是在这种情况下,你的问题比未公开的PreparedStatement更糟糕,你有一个 参数不匹配。
如果检查源代码,update方法将调用此execute方法:
@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,要么(如果必须在此处记录),请在记录后重新抛出它。