Já faz muito tempo desde que postei essa pergunta e quero postar uma resposta que descreva o cenário exato que levou a essa
NullPointerException
complicada . Eu acho que isso pode ajudar futuros leitores que encontrarem uma exceção tão confusa a pensar fora da caixa, já que eu tinha quase todos os motivos para suspeitar que isso era um bug do conector mysql, mesmo que não fosse afinal.
Ao investigar essa exceção, tive certeza de que meu aplicativo não pode fechar a conexão de banco de dados ao tentar ler dados dela, pois minhas conexões de banco de dados não são compartilhadas entre threads e se o mesmo thread fechasse a conexão e tentasse acessar isso, uma exceção diferente deveria ter sido lançada (algumas
SQLException
). Essa foi a principal razão pela qual suspeitei de um bug no conector mysql. Descobriu-se que havia dois encadeamentos acessando a mesma conexão afinal. A razão pela qual isso foi difícil de descobrir foi que um desses threads era um thread do coletor de lixo .
Voltando ao código que postei:
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();
O problema está na seção "fazer algum processamento que envolva carregar outros registros do banco de dados usando a mesma conexão", que, infelizmente, não incluí na minha pergunta original, pois não achei que o problema estivesse lá.
Ampliando essa seção, temos:
if (sc != null) {
...
someMethod (conn);
...
}
E
someMethod
se parece com isso:public void someMethod (Connection conn)
{
...
SomeOtherClass instance = new SomeOtherClass (conn);
...
}
SomeOtherClass
se parece com isso (é claro que estou simplificando aqui):public class SomeOtherClass
{
Connection conn;
public SomeOtherClass (Connection conn)
{
this.conn = conn;
}
protected void finalize() throws Throwable
{
if (this.conn != null)
conn.close();
}
}
SomeOtherClass
pode criar sua própria conexão de banco de dados em alguns cenários, mas pode aceitar uma conexão existente em outros cenários, como o que temos aqui. Como você pode ver, essa seção contém uma chamada para
someMethod
que aceita a conexão aberta como argumento. someMethod
passa a conexão para uma instância local de SomeOtherClass
. SomeOtherClass
teve um finalize
método que fecha a conexão. Agora, depois de
someMethod
retorna, instance
torna-se elegível para a coleta de lixo. Quando é coletado como lixo, seu finalize
é chamado pelo thread do coletor de lixo, que fecha a conexão. Agora voltamos ao loop for, que continua a executar instruções SELECT usando a mesma conexão que pode ser fechada a qualquer momento pela thread do coletor de lixo.
Se o thread do coletor de lixo fechar a conexão enquanto o thread do aplicativo estiver no meio de algum método de conector mysql que depende da conexão ser aberta, um
NullPointerException
pode ocorrer. Removendo o
finalize
método resolveu o problema. Não costumamos substituir o
finalize
em nossas classes, o que dificultou muito a localização do bug.