Можно ли отслеживать, какое выражение вызвало 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! Просто КАК ОХЛАЖДЕНИЕ ЭТО?
Ответы
Ответ 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
, если вы не знаете, что это невозможно)