1. 程式人生 > >java異常檢視利器之使用 jvmti 的Callback_JVMTI_EVENT_EXCEPTION 事件檢視異常

java異常檢視利器之使用 jvmti 的Callback_JVMTI_EVENT_EXCEPTION 事件檢視異常

  閱讀本文前需要了解什麼是jvmtijvmti全稱稱之為 JVM Tool Interface,有關jvmti更詳細的知識,本文不再詳細列出。大家可以藉助百度來了解有關它更為詳盡的內容。

  在開原始檔大行其道的今天,基於java種種解決方案和框架紛繪踏至而來,浩瀚如海看不完也學不盡。在採用這些解決方案和框架進行專案開發時,往往會出現當程式卡殼時,既無異常提示資訊亦沒有與之對應的日誌輸出的局面。每每出現這樣的困境時,往往只能通過打斷點來一步步除錯跟蹤來解決。更有甚者,基於某一底層的框架進行相應的開發時,受限於框架開發者的精力和時間等因素的影響,如果框架針對某異常處理設計的不合理,處理異常時沒有向外丟擲異常,同時又沒有輸出日誌資訊。當出現問題時,雪上加霜的是框架又沒有提供原始碼用於打斷點除錯,此時只能藉助通過反編譯工具,閱讀框架原始碼來嘗試解決問題。每每出現這些困境,真希望有一種工具能夠洞悉那些被框架“吃掉”沒有嚮往丟擲的異常,以便加快問題的解決步伐。

  為了方便開發,一直都想做一個有關java異常檢視的小工具。想了很長時間,想到了如下幾種實現方式:

  • 藉助位元組碼工具,在每一個方法開頭和結尾處插入java異常捕獲程式碼。這種方式實現起來效率太低了,況且如果在方法體內,捕獲異常並沒有向外丟擲的話,就算採用這種方式也看不到異常。
  • SpringMVC框架針對異常進行了統一的封裝和處理,只要進行相應的擴充套件就能捕獲到程式丟擲的異常。這種實現方式較前一種比較看來,效率大大提高了,但是仍然沒有解決前者提到的,如果應用程式內部自己“吃掉異常”,不向外丟擲異常的話,依然無法捕捉到異常,而且這種實現實現方式僅僅侷限於使用了SpringMVC框架的WEB應用程式,如果使用了其它的WEB架構或者非WEB的應用程式就會無能為力,侷限性太強。

  思來索去,想到java應用程式的執行肯定是離不開jvm的,不妨看一下jvm中有沒有提供這樣的擴充套件。在網上搜索了一番,發現jvm還真提供了這樣的擴充套件。

示例程式碼,在main方法中吃掉異常之後,不作任何處理。

 1 package com.github.torlight.jvmtit;
 2 
 3 /**
 4  * Hello world!
 5  *
 6  */
 7 public class App {
 8     
 9     public static void main( String[] args ){
10         
11
System.out.println( "Hello World!" ); 12 13 try { 14 throw new NullPointerException("QQQ"); 15 } catch (Exception e) { 16 17 } 18 } 19 }

  程式載入相應的擴充套件,執行之後效果如下所示,可以看到在控制檯上面,打印出空指標異常。如果不借助jvmti提供的異常事件進行相應的擴充套件話,控制檯上就不會列印空指標異常資訊。其實現原理也很簡單,藉助jvmti提供的異常事件進行相應的擴充套件,當jvm捕獲到異常時,會回撥針對該事件的擴充套件方法,在該方法體內部呼叫 printStackTrace 方法,列印異常提示資訊。

 1 java.lang.ClassNotFoundException: com.github.torlight.jvmtit.App
 2     at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
 3     at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
 4     at java.lang.ClassLoader.loadClass(ClassLoader.java:411)
 5     at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
 6     at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
 7     at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)
 8 java.lang.ClassNotFoundException: com.github.torlight.jvmtit.App
 9     at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
10     at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
11     at java.lang.ClassLoader.loadClass(ClassLoader.java:411)
12     at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
13     at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
14     at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)
15 java.lang.NullPointerException: QQQ
16     at com.github.torlight.jvmtit.App.main(App.java:14)
17 Hello World!
18 loaded class name=run in Callback_JVMTI_EVENT_EXCEPTION method
19  Exception: Ljava/lang/ClassNotFoundException;
20 loaded class name=run in Callback_JVMTI_EVENT_EXCEPTION method
21  Exception: Ljava/lang/ClassNotFoundException;
22 loaded class name=run in Callback_JVMTI_EVENT_EXCEPTION method
23  Exception: Ljava/lang/NullPointerException;
24 agent onload

下面貼出針對jvmti Callback_JVMTI_EVENT_EXCEPTION 事件進行擴充套件的agent程式碼。

  1 // 這是主 DLL 檔案。
  2 
  3 #include "stdafx.h"
  4 
  5 #include "jvmti_evt_ex.h"
  6 #include <stdio.h>
  7 #include <memory.h>
  8 #include <string.h>
  9 #include <jvmti.h>
 10 
 11 void printStackTrace(JNIEnv* env, jobject exception) {
 12     jclass throwable_class = (*env).FindClass("java/lang/Throwable");
 13     jmethodID print_method = (*env).GetMethodID(throwable_class, "printStackTrace", "()V");
 14     (*env).CallVoidMethod(exception, print_method);
 15 }
 16 
 17 void JNICALL Callback_JVMTI_EVENT_EXCEPTION (jvmtiEnv *jvmti_env,
 18     JNIEnv* jni_env,
 19     jthread thread,
 20     jmethodID method,
 21     jlocation location,
 22     jobject exception,
 23     jmethodID catch_method,
 24     jlocation catch_location) {
 25 
 26     printf("loaded class name=%s\n ", "run in Callback_JVMTI_EVENT_EXCEPTION method");
 27     char* class_name;
 28 
 29     jclass exception_class = jni_env->GetObjectClass(exception);
 30     jvmti_env->GetClassSignature(exception_class, &class_name, NULL);
 31     printf("Exception: %s\n", class_name);    
 32 
 33     printStackTrace(jni_env, exception);
 34 }
 35 
 36 
 37 void JNICALL Callback_JVMTI_EVENT_Exception_Catch (jvmtiEnv *jvmti_env,
 38     JNIEnv* jni_env,
 39     jthread thread,
 40     jmethodID method,
 41     jlocation location,
 42     jobject exception)    {
 43 
 44     char* class_name;
 45     jclass exception_class = jni_env->GetObjectClass(exception);
 46     jvmti_env->GetClassSignature(exception_class, &class_name, NULL);
 47     printf("Exception: %s\n", class_name);    
 48 
 49     printStackTrace(jni_env, exception);    
 50 }
 51 
 52 
 53 JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved){
 54     jvmtiEnv *jvmti = NULL;
 55     
 56     fprintf(stderr,"agent onload");
 57 
 58     //獲取JVMTI environment
 59     jint erno = vm->GetEnv((void **)&jvmti, JVMTI_VERSION_1_1);
 60     if (erno != JNI_OK) {
 61         fprintf(stderr, "ERROR: Couldn't get JVMTI environment");
 62         return JNI_ERR;
 63     }
 64     
 65     //註冊功能
 66     jvmtiCapabilities capabilities;
 67     (void)memset(&capabilities, 0, sizeof(jvmtiCapabilities));
 68     capabilities.can_generate_exception_events=1;
 69 
 70     jvmtiError error = jvmti->AddCapabilities(&capabilities);
 71     if(error != JVMTI_ERROR_NONE) {
 72         fprintf(stderr, "ERROR: Unable to AddCapabilities JVMTI");
 73         return  error;
 74     }
 75 
 76     //設定JVM事件 (JVMTI_EVENT_EXCEPTION) 回撥
 77     jvmtiEventCallbacks ex_callbacks;
 78     ex_callbacks.Exception = &Callback_JVMTI_EVENT_EXCEPTION;
 79     error = jvmti->SetEventCallbacks(&ex_callbacks, (jint)sizeof(ex_callbacks));
 80     if(error != JVMTI_ERROR_NONE) {
 81         fprintf(stderr, "ERROR: Unable to SetEventCallbacks JVMTI!");        
 82         return error;
 83     }
 84 
 85     //設定事件通知
 86     error = jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_EXCEPTION, (jthread)NULL);
 87     if(error != JVMTI_ERROR_NONE) {
 88         fprintf(stderr, " ERROR: Unable to SetEventNotificationMode JVMTI!,the error code=%d",error);
 89         return  error;
 90     }
 91 
 92     return JNI_OK;
 93 }
 94 
 95 JNIEXPORT jint JNICALL
 96     Agent_OnAttach(JavaVM* vm, char *options, void *reserved){
 97         //do nothing
 98         
 99     return JNI_OK;
100 }
101 
102 JNIEXPORT void JNICALL
103     Agent_OnUnload(JavaVM *vm){
104         //do nothing
105     
106 }

  示例程式碼和agent程式碼均已經上傳至github上面(連結地址:https://github.com/gittorlight/java-other/tree/master/jvmti_evt_ex),我是用 visual studio 2010 來編譯agent的,編譯的時候需要根據所下載的jdk是32位還是64位來選擇相對應的標頭檔案。我使用的是64位的jdk 1.8,所以使用的是64位的標頭檔案。截圖如下所示:

  編譯agent截圖(一)

編譯agent截圖(二)

編譯agent截圖(三)

 編譯完成agent之後,在應用程式的啟動引數上面使用-agentpath 引數來載入該agent。以eclipse為例,截圖如下所示:

載入agent截圖(一)

 

載入agent截圖(二)

 

載入agent截圖(三)