2016-10-14 1 views
12

Quand je reçois un NPE, je vais obtenir une trace de la pile avec le numéro de ligne. Ce est utile, mais si la ligne est très dense et/ou contient l'expression imbriquées, il est encore impossible de savoir quelle référence était nulle.Est-il possible de retrouver l'expression qui a provoqué une NPE?

Certes, cette information a dû être disponible quelque part. Est-il possible de comprendre cela? (Si ce n'est pas l'expression java, alors au moins l'instruction bytecode qui a causé NPE serait utile aussi)

Édition 1: J'ai vu quelques commentaires suggérant de briser la ligne, etc., ce qui, sans vouloir offenser, est vraiment non-constructif et non pertinent. Si je pouvais le faire, j'aurais! Disons simplement que modifier la source est hors de question. Edit # 2: apangin a posté une excellente réponse ci-dessous, que j'ai acceptée. Mais c'est SOOO COOL que j'ai dû inclure la sortie ici pour tous ceux qui ne veulent pas l'essayer eux-mêmes! ;)

suppose donc que j'ai ce programme pilote TestNPE.java

1 public class TestNPE { 
2  public static void main(String[] args) { 
3   int n = 0; 
4   String st = null; 
5 
6   System.out.println("about to throw NPE"); 
7   if (n >= 0 && st.isEmpty()){ 
8    System.out.println("empty"); 
9   } 
10   else { 
11    System.out.println("othereise"); 
12   } 
13  } 
14  
15 } 

Le bytecode ressemble à ceci (montrant que la principale() méthode et en omettant d'autres parties non pertinentes)

Code: 
    stack=2, locals=3, args_size=1 
    0: iconst_0 
    1: istore_1 
    2: aconst_null 
    3: astore_2 
    4: getstatic  #2     // Field java/lang/System.out:Ljava/io/PrintStream;            
    7: ldc   #3     // String about to throw NPE                  
    9: invokevirtual #4     // Method java/io/PrintStream.println:(Ljava/lang/String;)V          
    12: iload_1 
    13: iflt   34 
    16: aload_2 
    17: invokevirtual #5     // Method java/lang/String.isEmpty:()Z               
    20: ifeq   34 
    23: getstatic  #2     // Field java/lang/System.out:Ljava/io/PrintStream;            
    26: ldc   #6     // String empty                     
    28: invokevirtual #4     // Method java/io/PrintStream.println:(Ljava/lang/String;)V          
    31: goto   42 
    34: getstatic  #2     // Field java/lang/System.out:Ljava/io/PrintStream;            
    37: ldc   #7     // String othereise                    
    39: invokevirtual #4     // Method java/io/PrintStream.println:(Ljava/lang/String;)V          
    42: return 

maintenant Lorsque vous exécutez le pilote TestNPE avec l'agent, vous obtenez ce

$ java -agentpath:libRichNPE.o TestNPE 
about to throw NPE 
Exception in thread "main" java.lang.NullPointerException: location=17 
    at TestNPE.main(TestNPE.java:7) 

l'invokevirtual # 5 à l'offset 17! Juste COMMENT COOL EST-CE QUE C'EST?

+2

Bonne question, mais aussi garder à l'esprit que si la ligne est dense la la qualité du code est probablement moins qu'optimale. – Kayaman

+0

Je sais que vous ne pouvez pas obtenir le numéro de colonne ... mais si vous pouvez utiliser les retours pour interrompre les appels sur cette ligne vers une série de lignes 'Java' affichera le numéro de ligne concerné tout en le traitant comme une ligne. –

+0

Cela dépend de l'implémentation de la machine virtuelle Java. Par exemple, SAP JVM imprime le nom de champ null dans le message d'exception. –

Répondre

7

Lorsqu'une exception se produit, machine virtuelle Java connaît le bytecode original qui a provoqué l'exception. Cependant, StackTraceElement ne suit pas les indices de bytecode.

La solution consiste à capturer l'index bytecode en utilisant JVMTI chaque fois qu'une exception se produit. L'exemple d'agent JVMTI suivant intercepte toutes les exceptions et si le type d'exception est NullPointerException, l'agent remplacera detailMessage par les informations d'emplacement du bytecode.

#include <jvmti.h> 
#include <stdio.h> 

static jclass NullPointerException; 
static jfieldID detailMessage; 

void JNICALL VMInit(jvmtiEnv* jvmti, JNIEnv* env, jthread thread) { 
    jclass localNPE = env->FindClass("java/lang/NullPointerException"); 
    NullPointerException = (jclass) env->NewGlobalRef(localNPE); 

    jclass Throwable = env->FindClass("java/lang/Throwable"); 
    detailMessage = env->GetFieldID(Throwable, "detailMessage", "Ljava/lang/String;"); 
} 

void JNICALL ExceptionCallback(jvmtiEnv* jvmti, JNIEnv* env, jthread thread, 
           jmethodID method, jlocation location, jobject exception, 
           jmethodID catch_method, jlocation catch_location) { 
    if (env->IsInstanceOf(exception, NullPointerException)) { 
     char buf[32]; 
     sprintf(buf, "location=%ld", (long)location); 
     env->SetObjectField(exception, detailMessage, env->NewStringUTF(buf)); 
    } 
} 

JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM* vm, char* options, void* reserved) { 
    jvmtiEnv* jvmti; 
    vm->GetEnv((void**)&jvmti, JVMTI_VERSION_1_0); 

    jvmtiCapabilities capabilities = {0}; 
    capabilities.can_generate_exception_events = 1; 
    jvmti->AddCapabilities(&capabilities); 

    jvmtiEventCallbacks callbacks = {0}; 
    callbacks.VMInit = VMInit; 
    callbacks.Exception = ExceptionCallback; 
    jvmti->SetEventCallbacks(&callbacks, sizeof(callbacks)); 
    jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_VM_INIT, NULL); 
    jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_EXCEPTION, NULL); 

    return 0; 
} 

Compile ceci dans une bibliothèque partagée et exécutez java avec l'option -agentpath:

java -agentpath:/pato/to/libRichNPE.so Main 
+0

Oh, c'est intéressant! Merci. Je vais essayer. –

+0

@apngin: Désolé, comment puis-je compiler ceci dans une bibliothèque partagée? –

+0

J'ai l'erreur suivante en essayant de le compiler http://paste.ubuntu.com/23339517/ –

1

L'exception elle-même n'a pas suffisamment d'informations pour fournir plus que des numéros de ligne.

Une option que je vois est d'utiliser un débogueur bytecode comme visualiseur bytecode pour localiser plus près l'instruction bytecode qui provoque le npe. Avancez jusqu'à ce que l'exception se produise, ou ajoutez un point d'arrêt pour npe.

0

Le mécanisme de trace de pile repose sur les métadonnées de débogage éventuellement compilées dans chaque classe (à savoir les attributs SourceFile et LineNumberTable). Pour autant que je sache, les décalages bytecode ne sont conservés nulle part. Cependant, ceux-ci ne seraient pas utiles pour un programme Java typique, puisque vous savez toujours à quel code correspond chaque instruction bytecode. Cependant, il existe une solution de contournement évidente: il suffit de diviser le code en question en plusieurs lignes et de le recompiler! Vous pouvez insérer des espaces presque n'importe où dans Java.

0

Vous pouvez briser la ligne complexe en plusieurs plus petits que vous pouvez suivre, ou vous utilisez votre débogueur pour voir quelle valeur était null lorsque l'exception s'est produite.

Alors que vous pourriez essayer de regarder le code de l'octet où cela est arrivé, ce ne sera que le début d'un voyage complexe. Je suggère de rendre votre code plus simple à comprendre et vous pourriez deviner quelles valeurs pourraient être null (Note: il pourrait être null sauf si vous savez qu'il est impossible)