Подтвердить что ты не робот

Случайное исключение NullPointerException в ResultSetImpl.checkColumnBounds или ResultSetImpl.getStringInternal

Прежде всего, не закрывайте это как дубликат What's 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-коннектора и нашел это (взято из здесь):

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.

Самое близкое, что я мог найти, это этот вопрос, на который нет ответа.

Я подозреваю, что проблема не в запросе, который я опубликовал. Возможно, что-то пошло не так с экземпляром Connection из-за некоторых других операторов, которые мы запускаем в одном и том же соединении. Все, что я могу сказать, это то, что мы закрываем каждое утверждение сразу после его выполнения и считываем данные из ResultSet.

EDIT (1/19/2017):

Я не смог воссоздать ошибку в моей среде разработки. Я думал, что это может быть некоторая ошибка mysql-connect, вызванная, когда одно и то же соединение используется в течение длительного времени. Я ограничил вышеуказанный цикл загрузкой не более 6 элементов за раз. Кроме того, мы обновили версию mysql-коннектора до 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 здесь и соответствующий код:

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

что означает, что this.connection равно null. this.connection - это переменная экземпляра типа MySQLConnection, которая инициализируется в конструкторах ResultSetImpl и устанавливается только в null при вызове public void realClose(boolean calledExplicitly) (она вызывается public void close()), которая, согласно документации в источнике, вызывается при вызове ResultSet.close()). Мы, конечно, не закрываем ResultSet до чтения всех данных из него.

Любые идеи о том, как действовать?

4b9b3361

Ответ 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();

Проблема заключается в том, что "делать некоторую обработку, связанную с загрузкой других записей из базы данных с использованием той же связи", которая, к сожалению, не включалась в мой первоначальный вопрос, так как я не думал, что проблема была там.

Масштабирование в этом разделе:

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 в наших классах, что затрудняет поиск ошибки.

Ответ 2

Единственный способ, которым это может происходить, насколько я вижу, - это кто-то/что-то вызывает ResultSet.close() сразу после Вызывается ResultSet.getString(), но до его возвращения.

Это может быть ваше приложение (оно многопоточное или оно имеет сложный путь обработки), или вы можете потерять соединение когда-нибудь за это время, что заставляет водителя закрыть соединение неявно, которое закрывает все открытые операторы, которые закрываются все открытые результирующие наборы.

Вы не должны получать NPE, но мы должны повысить уровень SQLException.

Вы можете проверить журнал ошибок mysqld, чтобы узнать, что происходит что это означает, что соединение теряется?

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

Ответ 3

Было бы полезно, если бы вы опубликовали полную трассировку стека.

Это может быть проблема concurrency, если rs - это переменная класса, в которой два потока делят ваш набор результатов и выполняют его одновременно для разных запросов.

Один из способов продолжить это - использовать отладчик с точкой останова до возникновения исключения и выполнить шаг, чтобы заметить, где это происходит. Поместите точку останова в код соединителя sql-кода, также как и connection.createStatement(...) или внутри api, если это многопоточность, то отладчик проблемы перейдет к нему, где вы сможете проанализировать дальнейший, посмотрев на стек вызовов.

Ответ 4

Я не могу доказать, что это правильное объяснение, но даже если оно не указывает на хорошее направление.

Все сводится к тому, как com.mysql.jdbc.ResultSetImpl fields Защищенная переменная-член могла бы принять null значение.

Если я правильно следил за трассировкой стека, ResultSetImpl создается, вызывая статический ResultSetImpl.getInstance(), который вызывается com.mysql.jdbc.MysqlIO.buildResultSetWithRows(), который вызывается MysqlIO. getResultSet() Внутри 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 не будет установлено значение non null. Тогда предположим, что

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

is 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 получит значение null в своем fields и совместимо с ним после обновления с версии до 5.0.5 до 5.1.36.

Плохая новость заключается в том, что я не вижу, как isBinaryEncoded принимает значение true, и это еще одно необходимое условие для создания экземпляра ResultSetImpl с полями null.

Если эта гипотеза верна, ошибка будет отсутствовать (metadataFromCache == null) ? fields : metadataFromCache at MysqlIO.getResultSet(), а изменение типа ResultSet от TYPE_FORWARD_ONLY до TYPE_SCROLL_INSENSITIVE может помешать NullPointerException.

Ответ 5

Теперь для избежания множественной итерации и продолжения соединения Connection. вы можете использовать запрос пользователя 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 идентификаторов, поэтому, если он больше, чем тогда, вам нужно разделить в равном количестве списка. Я думаю, вы можете попробовать. Он должен решить.

Ответ 6

Как указано в других ответах, основной причиной NullPointerException является то, что экземпляр ResultSet закрывается. Ниже могут быть возможные причины:

  • Явное закрытие результирующего набора путем вызова rs.close(). (здесь это не кажется применимым, как в приведенном выше коде rs.close() не вызывается).
  • Неявное закрытие набора результатов. Набор результатов может быть закрыт неявно драйвером JDBC в некоторых сценариях, таких как:
    • Если тот же объект PreparedStatement используется совместно несколькими потоками одновременно. Повторное выполнение запроса косвенно закрыло ранее открытый ResultSet.

[Предполагая, что ваш запрос выполняется одновременно). Кажется, connection.preapresStatement возвращает один и тот же объект-оператор (некорректно) при вызове одновременно.

Что я могу предложить вам попробовать отключить кеш инструкции подготовки (через свойство cachePrepStmts connection) и посмотреть, сохраняется ли проблема.

Ответ 7

Не могли бы вы переписать свой код, чтобы использовать один блок try-catch? Тогда у вас будет соединение внутри метода, и никто не сможет закрыть соединение.

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