ResultSetImpl.checkColumnBounds或ResultSetImpl.getStringInternal中偶尔出现NullPointerException

首先,请不要将它作为NullPointerException副本以及如何修复它来关闭它。 我知道什么是NullPointerException ,我知道如何在我自己的代码中解决它,但不是当它被mysql-connector-java-5.1.36-bin.jar抛出时,我无法控制它。

我们在mySQL数据库上运行公共数据库查询时遇到exception,该查询在大多数情况下都有效。 我们在部署新版本后开始看到此exception,但发生这种exception的查询在很长一段时间内都没有发生变化。

以下是查询的外观(通过一些必要的简化)。 我用之前和之后执行的一些逻辑包围了它。 实际的代码不是全部都在一个方法中,但我把它放在一个块中,以便更容易理解。

 Connection conn = ... // the connection is open ... for (String someID : someIDs) { SomeClass sc = null; PreparedStatement stmt = conn.prepareStatement ("SELECT A, B, C, D, E, F, G, H FROM T WHERE A = ?"); stmt.setString (1, "someID"); ResultSet res = stmt.executeQuery (); if (res.next ()) { sc = new SomeClass (); sc.setA (res.getString (1)); sc.setB (res.getString (2)); sc.setC (res.getString (3)); sc.setD (res.getString (4)); sc.setE (res.getString (5)); sc.setF (res.getInt (6)); sc.setG (res.getString (7)); sc.setH (res.getByte (8)); // the exception is thrown here } stmt.close (); conn.commit (); if (sc != null) { // do some processing that involves loading other records from the // DB using the same connection } } conn.close(); 

res.getByte(8)导致带有以下调用堆栈的NullPointerException

com.mysql.jdbc.ResultSetImpl.checkColumnBounds(ResultSetImpl.java:763)com.mysql.jdbc.ResultSetImpl.getStringInternal(ResultSetImpl.java:5251)com.mysql.jdbc.ResultSetImpl.getString(ResultSetImpl.java:5173)com。 mysql.jdbc.ResultSetImpl.getByte(ResultSetImpl.java:1650)org.apache.tomcat.dbcp.dbcp2.DelegatingResultSet.getByte(DelegatingResultSet.java:206)org.apache.tomcat.dbcp.dbcp2.DelegatingResultSet.getByte(DelegatingResultSet。 Java的:206)

我搜索了相关mysql-connector版本的源代码,发现了这个(取自这里 ):

 756 protected final void checkColumnBounds(int columnIndex) throws SQLException { 757 synchronized (checkClosed().getConnectionMutex()) { 758 if ((columnIndex  this.fields.length)) { 764 throw SQLError.createSQLException( 765 Messages.getString("ResultSet.Column_Index_out_of_range_high", 766 new Object[] { Integer.valueOf(columnIndex), Integer.valueOf(this.fields.length) }), SQLError.SQL_STATE_ILLEGAL_ARGUMENT, 767 getExceptionInterceptor()); 768 } 769 770 if (this.profileSql || this.useUsageAdvisor) { 771 this.columnUsed[columnIndex - 1] = true; 772 } 773 } 774 } 

如您所见,此行发生exception:

 } else if ((columnIndex > this.fields.length)) { 

这意味着this.fields以某种方式变为null

我能找到的最接近的是这个问题 ,没有答案。

我怀疑问题不在我发布的查询中。 由于我们在同一连接上运行的其他一些语句, Connection实例可能出现了问题。 我只能说我们在执行它之后立即关闭每个语句并从ResultSet读取数据。

编辑(1/19/2017):

我无法在开发环境中重新创建错误。 我认为这可能是长时间使用相同连接时触发的一些mysql-connector错误。 我限制上面的循环一次加载最多6个元素。 另外,我们将mysql-connector版本升级到5.1.40。

我们仍然在ResultSetImpl看到NullPointerException ,但这次是在不同的位置。

堆栈跟踪是:

com.mysql.jdbc.ResultSetImpl.getStringInternal(ResultSetImpl.java:5294)com.mysql.jdbc.ResultSetImpl.getString(ResultSetImpl.java:5151)org.apache.tomcat.dbcp.dbcp2.DelegatingResultSet.getString(DelegatingResultSet.java: 198)org.apache.tomcat.dbcp.dbcp2.DelegatingResultSet.getString(DelegatingResultSet.java:198)

这意味着这次exception是由于我们的一个res.getString()调用而抛出的(我不知道哪一个)。

我在这里找到了mysql-connector-java-5.1.40-bin.jar的源代码,相关代码是:

 5292 // Handles timezone conversion and zero-date behavior 5293 5294 if (checkDateTypes && !this.connection.getNoDatetimeStringSync()) { 5295 switch (metadata.getSQLType()) { 

这意味着this.connection为null。 this.connectionMySQLConnection类型的实例变量,它在ResultSetImpl的构造函数中初始化,并且仅在调用public void realClose(boolean calledExplicitly)时设置为null(由public void close()调用,根据在调用ResultSet.close()时调用源中的文档。 在从中读取所有数据之前,我们绝对不会关闭ResultSet

任何想法如何进行?

据我所知,唯一可能发生这种情况的方法是在调用ResultSet.getString()之后但在返回之前调用ResultSet.close()。

这可能是您的应用程序(它是multithreading的,还是它有一个棘手的处理路径),或者您可能在此期间的某个时间丢失连接,这会导致驱动程序隐式关闭连接,这将关闭所有打开的语句,它会关闭所有打开的结果集。

你不应该得到一个NPE,我们应该提出一个更好的SQLException。

您可以检查mysqld错误日志,看看是否有任何事情发生意味着连接丢失?

http://bugs.mysql.com/bug.php?id=41484

如果您已经发布了完整的堆栈跟踪,那将会很有帮助。

如果rs是一个类变量,它可能是一个并发问题,其中两个线程共享您的Resultset并同时为不同的查询执行它。

进行此操作的一种方法是在发生exception之前使用具有断点方式的调试器,并逐步注意这种情况发生的位置。 在一个必须使用的sql连接器代码中设置一个断点,就像connection.createStatement(…)或者在api里面一样,如果它是一个multithreading问题,调试器会跳到它,你可以通过查看调用堆栈进一步分析它。

我无法certificate这是正确的解释,但即使不是,它也可能指向一个好的方向。

这一切都归结为com.mysql.jdbc.ResultSetImpl fields受保护的成员变量如何取null值。

如果我正确地跟踪了堆栈跟踪,则通过调用由MysqlIO.getResultSet()调用的com.mysql.jdbc.MysqlIO.buildResultSetWithRows()调用的静态ResultSetImpl.getInstance()来实例化ResultSetImpl ,我们发现getResultSet()内部第一个fields初始化为null

 Field[] fields = null; 

它确实如此

 if (metadataFromCache == null) { fields = new Field[(int) columnCount]; ... } else { for (int i = 0; i < columnCount; i++) { skipPacket(); } } 

假设metadataFromCache != null因此fields不会设置为非null值。 然后让我们假设

 this.connection.versionMeetsMinimum(5, 0, 2) && this.connection.getUseCursorFetch() && isBinaryEncoded && callingStatement != null && callingStatement.getFetchSize() != 0 && callingStatement.getResultSetType() == ResultSet.TYPE_FORWARD_ONLY 

true并且usingCursor也是true ,也就是说versionMeetsMinimum(5, 0, 5) && (this.serverStatus & SERVER_STATUS_CURSOR_EXISTS) != 0也是true然后调用

 ResultSetImpl rs = buildResultSetWithRows(callingStatement, catalog, fields, rows, resultSetType, resultSetConcurrency, isBinaryEncoded) 

字段将为null

这可能导致ResultSetImpl在其fields获取null值,并且与从5.0.5之前的版本升级到5.1.36后发生的情况兼容。

坏消息是我无法看到isBinaryEncoded如何使值为true,这是使用null字段创建ResultSetImpl实例的另一个必要条件。

如果这个猜想是正确的那么bug就是缺失的(metadataFromCache == null) ? fields : metadataFromCache (metadataFromCache == null) ? fields : metadataFromCache MysqlIO.getResultSet() (metadataFromCache == null) ? fields : metadataFromCache并将ResultSet类型从TYPE_FORWARD_ONLY更改为TYPE_SCROLL_INSENSITIVE可能会阻止NullPointerException

至于现在要避免多次迭代并保持连接更长时间打开。 您可以在迭代中使用IN查询而不是单个id。

 Connection conn = ... // the connection is open ... SomeClass sc = null; PreparedStatement stmt = conn.prepareStatement ("SELECT A, B, C, D, E, F, G, H FROM T WHERE A in (?)"); //here you need to concate String to form like ('a','b') stmt.setString (1, someID); ResultSet res = stmt.executeQuery (); if (res.next ()) { sc = new SomeClass (); sc.setA (res.getString (1)); sc.setB (res.getString (2)); sc.setC (res.getString (3)); sc.setD (res.getString (4)); sc.setE (res.getString (5)); sc.setF (res.getInt (6)); sc.setG (res.getString (7)); sc.setH (res.getByte (8)); } stmt.close (); conn.commit (); if (sc != null) { // do some processing that involves loading other records from the // DB using the same connection } conn.close(); 

IN查询也允许你只有1000个ID,所以如果它超过那么你需要分成相同数量的列表。 我想你可以尝试一下。 它应该解决。

正如其他答案所指出的, NullPointerException的根本原因是ResultSet实例正在关闭。 以下可能是原因:

  • 通过调用rs.close()显式关闭结果集。 (它似乎不适用于此,如上面的代码rs.close()未调用)。
  • 隐式关闭结果集。 在某些情况下,JDBC驱动程序可以隐式关闭结果集,例如:
    • 如果多个线程同时共享同一个PreparedStatement对象。 重新执行查询将隐式关闭先前打开的ResultSet

[假设您的查询正在并发执行],似乎connection.preapresStatement在并发调用时返回相同的语句对象( 错误 )。

我可以建议您尝试禁用prepare语句缓存(通过cachePrepStmts连接属性)并查看问题是否仍然存在。

我发布这个问题已经有很长一段时间了,我想发布一个答案来描述导致这个棘手的NullPointerException的确切场景。

我认为这可能会帮助那些遇到这种令人困惑的exception的未来读者在盒子外思考,因为我几乎有理由怀疑这是一个mysql连接器错误,尽管它毕竟不是。

在调查此exception时,我确信我的应用程序在尝试从中读取数据时不可能关闭数据库连接,因为我的数据库连接不是跨线程共享的,并且如果相同的线程关闭连接然后尝试访问它应该抛出一个不同的exception(一些SQLException )。 这是我怀疑mysql连接器错误的主要原因。

事实certificate,毕竟有两个线程访问同一个连接 。 难以弄清楚的原因是这些线程中的一个是垃圾收集器线程

回到我发布的代码:

 Connection conn = ... // the connection is open ... for (String someID : someIDs) { SomeClass sc = null; PreparedStatement stmt = conn.prepareStatement ("SELECT A, B, C, D, E, F, G, H FROM T WHERE A = ?"); stmt.setString (1, "someID"); ResultSet res = stmt.executeQuery (); if (res.next ()) { sc = new SomeClass (); sc.setA (res.getString (1)); sc.setB (res.getString (2)); sc.setC (res.getString (3)); sc.setD (res.getString (4)); sc.setE (res.getString (5)); sc.setF (res.getInt (6)); sc.setG (res.getString (7)); sc.setH (res.getByte (8)); // the exception is thrown here } stmt.close (); conn.commit (); if (sc != null) { // do some processing that involves loading other records from the // DB using the same connection } } conn.close(); 

问题在于“执行一些涉及使用相同连接从DB加载其他记录的处理”部分,遗憾的是,我没有在原始问题中包含,因为我认为问题不在那里。

放大该部分,我们有:

 if (sc != null) { ... someMethod (conn); ... } 

someMethod看起来像这样:

 public void someMethod (Connection conn) { ... SomeOtherClass instance = new SomeOtherClass (conn); ... } 

SomeOtherClass看起来像这样(当然我在这里简化):

 public class SomeOtherClass { Connection conn; public SomeOtherClass (Connection conn) { this.conn = conn; } protected void finalize() throws Throwable { if (this.conn != null) conn.close(); } } 

SomeOtherClass可能会在某些情况下创建自己的数据库连接,但可以在其他方案中接受现有连接,例如我们在此处提供的连接。

如您所见,该部分包含对someMethod的调用,该调用接受open连接作为参数。 someMethod将连接传递给SomeOtherClass的本地实例。 SomeOtherClass有一个finalize方法可以关闭连接。

现在,在someMethod返回后, instance有资格进行垃圾回收。 当它被垃圾收集时,它的finalize方法由垃圾收集器线程调用,它关闭连接。

现在我们回到for循环,它继续使用相同的连接执行SELECT语句,可以随时由垃圾收集器线程关闭。

如果垃圾收集器线程碰巧关闭连接,而应用程序线程处于依赖于打开连接的某些mysql连接器方法的中间,则可能发生NullPointerException

删除finalize方法解决了这个问题。

我们通常不会在类中覆盖finalize方法,这使得很难找到bug。

您可以重写代码以使用一个try-catch-block吗? 然后你将在方法内部建立连接,而没有其他人可以关闭连接。

 import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class StackProblem1 { void dbQuery(final String[] someIDs) { try (final Connection conn = getConnection(); final PreparedStatement stmt = conn.prepareStatement( "SELECT A, B, C, D, E, F, G, H FROM T WHERE A = ?")) { for (final String someID : someIDs) { stmt.setString(1, "someID"); final ResultSet res = stmt.executeQuery(); if (res.next()) { final SomeClass sc = new SomeClass(); sc.set(res.getString(1)); sc.set(res.getString(2)); sc.set(res.getString(3)); sc.set(res.getString(4)); sc.set(res.getString(5)); sc.set(res.getInt(6)); sc.set(res.getString(7)); sc.set(res.getByte(8)); // the exception is thrown here // do some processing that involves loading other records from the // DB using the same connection } } } catch (final SQLException sqlEx) { sqlEx.printStackTrace(); } } /** * @return the connection is open */ private Connection getConnection() { return null;//FIXME return connection } }