ResultSetImpl.checkColumnBounds或ResultSetImpl.getStringInternal

时间:2017-01-12 15:09:58

标签: java mysql jdbc

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

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

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

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版本的源代码,发现了这个(取自here):

756 protected final void checkColumnBounds(int columnIndex) throws SQLException {
757     synchronized (checkClosed().getConnectionMutex()) {    
758         if ((columnIndex < 1)) {    
759             throw SQLError.createSQLException(    
760                   Messages.getString("ResultSet.Column_Index_out_of_range_low",    
761                   new Object[] { Integer.valueOf(columnIndex), Integer.valueOf(this.fields.length) }), SQLError.SQL_STATE_ILLEGAL_ARGUMENT,
762                   getExceptionInterceptor());   
763         } else 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 }

如您所见,此行发生异常:

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

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

我能找到的最接近的是this question,它没有答案。

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

编辑(1/19/2017):

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

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

堆栈跟踪是:

  

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)

这意味着这次由于我们res.getString()个调用之一而抛出了异常(我不知道哪一个)。

我找到 mysql-connector-java-5.1.40-bin.jar here的来源,相关代码是:

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

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

任何想法如何进行?

7 个答案:

答案 0 :(得分:2)

  

据我所知,唯一可能发生这种情况的方法是   某人/某事正在紧接着调用ResultSet.close()   ResultSet.getString()已被调用,但在它返回之前。

     

这可能是你的应用程序(它是多线程的,还是它的   一个棘手的处理路径),或者你可能正在失去连接   在此期间的某个时间,这会导致司机关闭   隐式连接,关闭所有打开的语句   所有打开的结果集。

     

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

     

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

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

答案 1 :(得分:1)

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

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

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

答案 2 :(得分:1)

自从我发布此问题以来已经很长时间了,我想发布一个答案来描述导致这个棘手NullPointerException的确切情况。

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

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

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

回到我发布的代码:

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();

问题出在&#34;做一些涉及使用相同连接从数据库加载其他记录的处理&#34;但遗憾的是,我没有在原来的问题中加入,因为我没有想到问题就在那里。

进入该部分,我们有:

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的调用,该调用接受打开的连接作为参数。 someMethod将连接传递给SomeOtherClass的本地实例。 SomeOtherClass有一个finalize方法可以关闭连接。

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

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

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

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

我们不会经常覆盖我们课程中的finalize方法,因此很难找到错误。

答案 3 :(得分:0)

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

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

如果我已正确跟踪堆栈跟踪,则通过调用由ResultSetImpl.getInstance()调用的com.mysql.jdbc.MysqlIO.buildResultSetWithRows()来调用静态MysqlIO.getResultSet()来实例化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

trueusingCursor也是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在其null中获得fields值,并且在您从5.0.5之前的版本升级到5.1.36后,它与之相符

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

如果此推测是正确的,则错误(metadataFromCache == null) ? fields : metadataFromCache MysqlIO.getResultSet() ResultSetTYPE_FORWARD_ONLY类型从TYPE_SCROLL_INSENSITIVE更改为NullPointerException可能会阻止{ {1}}。

答案 4 :(得分:0)

至于现在要避免多次迭代并保持连接更长时间打开。你可以在迭代中使用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,所以如果它超过那么你需要分割相同数量的列表。我想你可以尝试一下。它应该解决。

答案 5 :(得分:0)

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

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

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

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

答案 6 :(得分:-1)

您是否可以重写代码以使用一个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
    }
}