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

Можно ли отслеживать, какое выражение вызвало NPE?

Когда я получаю NPE, я получаю трассировку стека с номером строки. Это полезно, но если строка очень плотная и/или содержит вложенное выражение, все равно невозможно определить, какая ссылка была нулевой.

Конечно, эта информация должна быть доступна где-то. Есть ли способ понять это? (Если не java-выражение, то, по крайней мере, команда байт-кода, которая вызвала NPE, также была бы полезна)

Редактировать # 1: Я видел несколько комментариев, предлагающих разбить линию и т.д., которые, без обид, действительно неконструктивны и неактуальны. Если бы я мог это сделать, я бы это сделал! Позвольте просто сказать, что это изменение источника не может быть и речи.

Редактировать # 2: apangin опубликовал отличный ответ ниже, который я принял. Но это СООО ХОЛЛ, что я должен был включить вывод здесь для тех, кто не хочет опробовать себя!;)

Итак, предположим, что у меня есть эта программа-драйвер 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  }

Байт-код выглядит так (показывая только метод main() и опуская другие нерелевантные части)

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

Теперь, когда вы запускаете драйвер TestNPE с агентом, вы получите это

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

Итак, это указывает на invokevirtual # 5 со смещением 17! Просто КАК ОХЛАЖДЕНИЕ ЭТО?

4b9b3361

Ответ 1

Когда происходит исключение, JVM знает исходный байт-код, который вызвал исключение. Однако StackTraceElement не отслеживает индексы байт-кода.

Решением является захват индекса байт-кода с помощью JVMTI всякий раз, когда происходит исключение.

Следующий пример агента JVMTI перехватит все исключения, и если тип исключения будет NullPointerException, агент заменит его detailMessage информацией о местоположении байт-кода.

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

Скомпилируйте это в общую библиотеку и запустите java с опцией -agentpath:

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

Ответ 2

У самого исключения недостаточно информации, чтобы предоставить больше номеров строк.

Один из вариантов, который я вижу, - использовать отладчик байт-кода, например, визуализатор байт-кода, чтобы ближе локализовать инструкцию байт-кода, которая вызывает npe. Шаг вперед, пока не произойдет исключение, или добавьте точку останова для npe.

Ответ 3

Механизм трассировки стека опирается на метаданные отладки, которые могут быть скомпилированы в каждый класс (а именно атрибуты SourceFile и LineNumberTable). Насколько я знаю, смещения байт-кода нигде не сохраняются. Однако они не будут полезны для обычной Java-программы, поскольку вы все еще знаете, какой код соответствует каждой инструкции байт-кода.

Тем не менее, существует очевидное обходное решение - просто переломите рассматриваемый код на несколько строк и перекомпилируйте! Вы можете вставлять пробелы почти в любом месте Java.

Ответ 4

Вы можете разбить сложную строку на многие более мелкие, которые вы можете отслеживать, или вы используете ваш отладчик, чтобы узнать, какое значение было null, когда произошло исключение.

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