是否有可能找到导致NPE的表达式?

时间:2022-05-10 20:42:40

When I get an NPE, I'll get a stack trace with line number. That's helpful, but if the line is very dense and/or contains nested expression, it's still impossible to figure out which reference was null.

当我得到NPE时,我将得到一个带有行号的堆栈跟踪。这是很有帮助的,但是如果一行非常密集并且/或包含嵌套表达式,仍然无法确定哪个引用是空的。

Surely, this information must've been available somewhere. Is there a way to figure this out? (If not java expression, then at least the bytecode instruction that caused NPE would be helpful as well)

当然,这些信息一定在什么地方都有。有办法解决这个问题吗?(如果不是java表达式,那么至少导致NPE的字节码指令也会有帮助)

Edit #1: I've seen a few comments suggesting breaking up the line, etc, which, no offence, is really non-constructive and irrelevant. If I could do that, I would have ! Let just say this modifying the source is out of the question.

编辑#1:我看到过一些建议分手的评论,等等,没有冒犯的意思,真的没有建设性和无关紧要。如果我能做到,我就会做到!假设修改源文件是不可能的。

Edit #2: apangin has posted an excellent answer below, which I accepted. But it's SOOO COOL that I had to include the output here for anyone who doesn't want to try it out themselves! ;)

编辑#2:apangin发布了一个很好的答案,我接受了。但是,我必须在这里为那些不想自己尝试的人提供输出,这太酷了!,)

So suppose I have this driver program TestNPE.java

假设我有这个驱动程序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  }

The bytecode looks like this (showing only the main() method and omitting other irrelevant parts)

字节码看起来是这样的(只显示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

Now when you run the TestNPE driver with the agent, you'll get this

现在,当您使用代理运行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)

So that points to the invokevirtual #5 at offset 17! Just HOW COOL IS THAT?

这就指向了位于偏移量17处的invokevirtual #5 !这有多酷?

4 个解决方案

#1


7  

When an exception happens, JVM knows the original bytecode that caused the exception. However, StackTraceElement does not track bytecode indices.

当异常发生时,JVM知道导致异常的原始字节码。然而,StackTraceElement并不跟踪字节码索引。

The solution is to capture bytecode index using JVMTI whenever exception occurs.

解决方案是在异常发生时使用JVMTI捕获字节码索引。

The following sample JVMTI agent will intercept all exceptions, and if exception type is NullPointerException, the agent will replace its detailMessage with the bytecode location information.

以下示例JVMTI代理将拦截所有异常,如果异常类型为NullPointerException,则代理将用字节码位置信息替换其详细消息。

#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 this into a shared library and run java with -agentpath option:

将其编译为共享库,并使用-agentpath选项运行java:

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

#2


1  

The exception itself does not have enough information to provide more than line numbers.

异常本身没有足够的信息来提供更多的行号。

One option i see is to use a bytecode debugger like bytecode visualizer to closer localize the bytecode instruction that causes the npe. Step forward until the exception occurs, or add a breakpoint for npe.

我看到的一种选择是使用字节码调试器,比如字节码可视化工具,以更紧密地定位导致npe的字节码指令。向前迈进一步,直到发生异常,或者为npe添加断点。

#3


0  

The stack trace mechanism relies on the debugging metadata optionally compiled into each class (namely the SourceFile and LineNumberTable attributes). As far as I know, bytecode offsets are not preserved anywhere. However, these would not be useful for a typical Java program, since you still have know what code each bytecode instruction corresponds to.

堆栈跟踪机制依赖于可选地编译到每个类(即SourceFile和LineNumberTable属性)中的调试元数据。据我所知,字节码偏移在任何地方都不会被保留。但是,对于一个典型的Java程序来说,这些都不是有用的,因为您仍然知道每个字节码指令对应的代码是什么。

However, there is an obvious workaround - just break the code in question up into multiple lines and recompile! You can insert whitespace almost anywhere in Java.

但是,这里有一个明显的解决方案—将问题中的代码分解为多行并重新编译!在Java中几乎任何地方都可以插入空格。

#4


0  

You can either break up the complex line into many smaller ones you can trace, or you use your debugger to see what value was null when the exception occurred.

您可以将复杂的行分解为许多可以跟踪的小行,或者使用调试器查看异常发生时什么值为null。

While you could try to look at the byte code where this happened, this will only be the start of a complex journey. I suggest making your code simpler to understand and you might work out which values could be null (Note: it could be null unless you know it's not possible)

虽然您可以尝试查看发生这种情况的字节码,但这仅仅是一次复杂旅程的开始。我建议让您的代码更容易理解,您可以计算出哪些值可以为null(注意:它可以为null,除非您知道这是不可能的)

#1


7  

When an exception happens, JVM knows the original bytecode that caused the exception. However, StackTraceElement does not track bytecode indices.

当异常发生时,JVM知道导致异常的原始字节码。然而,StackTraceElement并不跟踪字节码索引。

The solution is to capture bytecode index using JVMTI whenever exception occurs.

解决方案是在异常发生时使用JVMTI捕获字节码索引。

The following sample JVMTI agent will intercept all exceptions, and if exception type is NullPointerException, the agent will replace its detailMessage with the bytecode location information.

以下示例JVMTI代理将拦截所有异常,如果异常类型为NullPointerException,则代理将用字节码位置信息替换其详细消息。

#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 this into a shared library and run java with -agentpath option:

将其编译为共享库,并使用-agentpath选项运行java:

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

#2


1  

The exception itself does not have enough information to provide more than line numbers.

异常本身没有足够的信息来提供更多的行号。

One option i see is to use a bytecode debugger like bytecode visualizer to closer localize the bytecode instruction that causes the npe. Step forward until the exception occurs, or add a breakpoint for npe.

我看到的一种选择是使用字节码调试器,比如字节码可视化工具,以更紧密地定位导致npe的字节码指令。向前迈进一步,直到发生异常,或者为npe添加断点。

#3


0  

The stack trace mechanism relies on the debugging metadata optionally compiled into each class (namely the SourceFile and LineNumberTable attributes). As far as I know, bytecode offsets are not preserved anywhere. However, these would not be useful for a typical Java program, since you still have know what code each bytecode instruction corresponds to.

堆栈跟踪机制依赖于可选地编译到每个类(即SourceFile和LineNumberTable属性)中的调试元数据。据我所知,字节码偏移在任何地方都不会被保留。但是,对于一个典型的Java程序来说,这些都不是有用的,因为您仍然知道每个字节码指令对应的代码是什么。

However, there is an obvious workaround - just break the code in question up into multiple lines and recompile! You can insert whitespace almost anywhere in Java.

但是,这里有一个明显的解决方案—将问题中的代码分解为多行并重新编译!在Java中几乎任何地方都可以插入空格。

#4


0  

You can either break up the complex line into many smaller ones you can trace, or you use your debugger to see what value was null when the exception occurred.

您可以将复杂的行分解为许多可以跟踪的小行,或者使用调试器查看异常发生时什么值为null。

While you could try to look at the byte code where this happened, this will only be the start of a complex journey. I suggest making your code simpler to understand and you might work out which values could be null (Note: it could be null unless you know it's not possible)

虽然您可以尝试查看发生这种情况的字节码,但这仅仅是一次复杂旅程的开始。我建议让您的代码更容易理解,您可以计算出哪些值可以为null(注意:它可以为null,除非您知道这是不可能的)