美文网首页
异常处理

异常处理

作者: 738bc070cd74 | 来源:发表于2016-02-29 18:56 被阅读482次

    很多情况下,本地代码做JNI调用后都要检查是否有错误发生,本章讲的就是怎么样检查错误和处理错误。
    我重点放在JNI函数调用引发的错误上面。如果一个本地方法中调用了一个JNI函数,它必须遵守下面几个步骤来检查和处理这个JNI函数调用时可能引发的错误。至于其它可能的错误,比如本地代码中调用了一个可能引发错误的系统方法,那只需要按照该系统方法的标准文档中规定的来处理就可以了。
    6.1 概述
    我们通过一些例子来介绍一些JNI异常处理函数
    6.1.1 本地代码中如何缓存和抛出异常
    下面的代码中演示了如何声明一个会抛出异常的本地方法。CatchThrow这个类声明了一个会抛出IllegalArgumentException异常的名叫doit的本地方法。
    class CatchThrow {
    private native void doit()
    throws IllegalArgumentException;
    private void callback() throws NullPointerException {
    throw new NullPointerException("CatchThrow.callback");
    }

     public static void main(String args[]) {
         CatchThrow c = new CatchThrow();
         try {
             c.doit();
         } catch (Exception e) {
             System.out.println("In Java:\n\t" + e);
         }
     }
     static {
         System.loadLibrary("CatchThrow");
     }
    

    }
    Main方法调用本地方法doit,doit方法的实现如下:
    JNIEXPORT void JNICALL
    Java_CatchThrow_doit(JNIEnv env, jobject obj)
    {
    jthrowable exc;
    jclass cls = (
    env)->GetObjectClass(env, obj);
    jmethodID mid =
    (env)->GetMethodID(env, cls, "callback", "()V");
    if (mid == NULL) {
    return;
    }
    (
    env)->CallVoidMethod(env, obj, mid);
    exc = (env)->ExceptionOccurred(env);
    if (exc) {
    /
    We don't do much with the exception, except that
    we print a debug message for it, clear it, and
    throw a new exception. /
    jclass newExcCls;
    (
    env)->ExceptionDescribe(env);
    (env)->ExceptionClear(env);
    newExcCls = (
    env)->FindClass(env,
    "java/lang/IllegalArgumentException");
    if (newExcCls == NULL) {
    /* Unable to find the exception class, give up. /
    return;
    }
    (
    env)->ThrowNew(env, newExcCls, "thrown from C code");
    }
    }
    运行程序,输出是:
    java.lang.NullPointerException:
    at CatchThrow.callback(CatchThrow.java)
    at CatchThrow.doit(Native Method)
    at CatchThrow.main(CatchThrow.java)
    In Java:
    java.lang.IllegalArgumentException: thrown from C code
    回调方法抛出一个NullPointerException异常。当CallVoidMethod把控制权交给本地方法时,本地代码会通过ExceptionOccurred来检查这个异常。在我们的例子中,当一个异常被检测到时,本地代码通过调用ExceptionDescribe来输出一个关于这个异常的描述信息,然后通过调用ExceptionClear清除异常信息,最后,抛出一个IllegalArgumentException。
    和JAVA中的异常机制不一样,JNI抛出的异常(例如,通过ThrowNew方法)不被处理的话,不会立即终止本地方法的执行。异常发生后,JNI程序员必须手动处理。
    6.1.2 制作一个抛出异常的工具函数
    抛出异常通常需要两步:通过FindClass找到异常类、调用ThrowNew函数生成异常。为了简化这个过程,我们写了一个工具函数专门用来生成一个指定名字的异常。
    void
    JNU_ThrowByName(JNIEnv env, const char name, const char msg)
    {
    jclass cls = (
    env)->FindClass(env, name);
    /
    if cls is NULL, an exception has already been thrown /
    if (cls != NULL) {
    (
    env)->ThrowNew(env, cls, msg);
    }
    /
    free the local ref /
    (
    env)->DeleteLocalRef(env, cls);
    }
    本书中,如果一个函数有JNU前缀的话,意味它是一个工具函数。JNU_ThrowByName这个工具函数首先使用FindClass函数来找到异常类,如果FindClass执行失败(返回NULL),VM会抛出一个异常(比如NowClassDefFoundError),这种情况下JNI_ThrowByName不会再抛出另外一个异常。如果FindClass执行成功的话,我们就通过ThrowNew来抛出一个指定名字的异常。当函数JNU_ThrowByName返回时,它会保证有一个异常需要处理,但这个异常不一定是name参数指定的异常。当函数返回时,记得要删除指向异常类的局部引用。向DeleteLocalRef传递NULL不会产生作用。
    6.2 妥善地处理异常
    JNI程序员必须能够预测到可能会发生异常的地方,并编写代码进行检查。妥善地异常处理有时很繁锁,但是一个高质量的程序不可或缺的。
    6.2.1 异常检查
    检查一个异常是否发生有两种方式。
    第一种方式是:大部分JNI函数会通过特定的返回值(比如NULL)来表示已经发生了一个错误,并且当前线程中有一个异常需要处理。在C语言中,用返回值来标识错误信息是一个很常见的方式。下面的例子中演示了如何通过GetFieldID的返回值来检查错误。这个例子包含两部分,定义了一些实例字段(handle、length、width)的类Window和一个缓存这些字段的字段ID的本地方法。虽然这些字段位于Window类中,调用GetFieldID时,我们仍然需要检查是否有错误发生,因为VM可能没有足够的内存分配给字段ID。

    1. /* a class in the Java programming language */
    2. public class Window {
    3.  long handle;
      
    4.  int length;
      
    5.  int width;
      
    6.  static native void initIDs();
      
    7.  static {
      
    8.      initIDs();
      
    9.  }
      
    10. }
    11. /* C code that implements Window.initIDs */
    12. jfieldID FID_Window_handle;
    13. jfieldID FID_Window_length;
    14. jfieldID FID_Window_width;
    15. JNIEXPORT void JNICALL
    16. Java_Window_initIDs(JNIEnv *env, jclass classWindow)
    17. {
    18.  FID_Window_handle =
      
    19.      (*env)->GetFieldID(env, classWindow, "handle", "J");
      
    20.  if (FID_Window_handle == NULL) {  /* important check. */
      
    21.      return; /* error occurred. */
      
    22.  }
      
    23.  FID_Window_length =
      
    24.      (*env)->GetFieldID(env, classWindow, "length", "I");
      
    25.  if (FID_Window_length == NULL) {  /* important check. */
      
    26.      return; /* error occurred. */
      
    27.  }
      
    28.  FID_Window_width =
      
    29.      (*env)->GetFieldID(env, classWindow, "width", "I");
      
    30.  /* no checks necessary; we are about to return anyway */
      
    31. }
      第二种方式:
      public class Fraction {
      // details such as constructors omitted
      int over, under;
      public int floor() {
      return Math.floor((double)over/under);
      }
      }
      /* Native code that calls Fraction.floor. Assume method ID
      MID_Fraction_floor has been initialized elsewhere. /
      void f(JNIEnv env, jobject fraction)
      {
      jint floor = (
      env)->CallIntMethod(env, fraction,
      MID_Fraction_floor);
      /
      important: check if an exception was raised /
      if ((
      env)->ExceptionCheck(env)) {
      return;
      }
      ... /* use floor /
      }
      当一个JNI函数返回一个明确的错误码时,你仍然可以用ExceptionCheck来检查是否有异常发生。但是,用返回的错误码来判断比较高效。一旦JNI函数的返回值是一个错误码,那么接下来调用ExceptionCheck肯定会返回JNI_TRUE。
      6.2.2 异常处理
      本地代码通常有两种方式来处理一个异常:
      1、 一旦发生异常,立即返回,让调用者处理这个异常。
      2、 通过ExceptionClear清除异常,然后执行自己的异常处理代码。
      当一个异常发生后,必须先检查、处理、清除异常后再做其它JNI函数调用,否则的话,结果未知。当前线程中有异常的时候,你可以调用的JNI函数非常少,11.8.2节列出了这些JNI函数的详细列表。通常来说,当有一个未处理的异常时,你只可以调用两种JNI函数:异常处理函数和清除VM资源的函数。
      当异常发生时,释放资源是一件很重要的事,下面的例子中,调用GetStringChars函数后,如果后面的代码发生异常,不要忘了调用ReleaseStringChars释放资源。
      JNIEXPORT void JNICALL
      Java_pkg_Cls_f(JNIEnv env, jclass cls, jstring jstr)
      {
      const jchar cstr = (env)->GetStringChars(env, jstr);
      if (c_str == NULL) {
      return;
      }
      ...
      if (...) { /
      exception occurred /
      (
      env)->ReleaseStringChars(env, jstr, cstr);
      return;
      }
      ...
      /
      normal return /
      (
      env)->ReleaseStringChars(env, jstr, cstr);
      }
      6.2.3 工具函数中的异常
      程序员编写工具函数时,一定要把工具函数内部分发生的异常传播到调用它的方法中去。这里有两个需要注意的地方:
      1、 对调用者来说,工具函数提供一个错误返回码比简单地把异常传播过去更方便一些。
      2、 工具函数在发生异常时尤其需要注意管理局部引用的方式。
      为了说明这两点,我们写了一个工具函数,这个工具函数根据对象实例方法的名字和描述符做一些方法回调。
      · jvalue
      · JNU_CallMethodByName(JNIEnv env,
      · jboolean hasException,
      · jobject obj,
      · const char name,
      · const char descriptor, ...)
      · {
      · va_list args;
      · jclass clazz;
      · jmethodID mid;
      · jvalue result;
      · if ((
      env)->EnsureLocalCapacity(env, 2) == JNI_OK) {
      · clazz = (
      env)->GetObjectClass(env, obj);
      · mid = (
      env)->GetMethodID(env, clazz, name,
      · descriptor);
      · if (mid) {
      · const char p = descriptor;
      · /
      skip over argument types to find out the
      · return type /
      · while (
      p != ')') p++;
      · /
      skip ')' /
      · p++;
      · va_start(args, descriptor);
      · switch (
      p) {
      · case 'V':
      · (env)->CallVoidMethodV(env, obj, mid, args);
      · break;
      · case '[':
      · case 'L':
      · result.l = (
      env)->CallObjectMethodV(
      · env, obj, mid, args);
      · break;
      · case 'Z':
      · result.z = (env)->CallBooleanMethodV(
      · env, obj, mid, args);
      · break;
      · case 'B':
      · result.b = (
      env)->CallByteMethodV(
      · env, obj, mid, args);
      · break;
      · case 'C':
      · result.c = (env)->CallCharMethodV(
      · env, obj, mid, args);
      · break;
      · case 'S':
      · result.s = (
      env)->CallShortMethodV(
      · env, obj, mid, args);
      · break;
      · case 'I':
      · result.i = (env)->CallIntMethodV(
      · env, obj, mid, args);
      · break;
      · case 'J':
      · result.j = (
      env)->CallLongMethodV(
      · env, obj, mid, args);
      · break;
      · case 'F':
      · result.f = (env)->CallFloatMethodV(
      · env, obj, mid, args);
      · break;
      · case 'D':
      · result.d = (
      env)->CallDoubleMethodV(
      · env, obj, mid, args);
      · break;
      · default:
      · (env)->FatalError(env, "illegal descriptor");
      · }
      · va_end(args);
      · }
      · (
      env)->DeleteLocalRef(env, clazz);
      · }
      · if (hasException) {
      · hasException = (env)->ExceptionCheck(env);
      · }
      · return result;
      · }
      JNU_CallMethodByName的参数当中有一个jboolean指针,如果函数执行成功的话,指针指向的值会被设置为JNI_TRUE,如果有异常发生的话,会被设置成JNI_FALSE。这就可以让调用者方便地检查异常。
      JNU_CallMethodByName首先通过EnsureLocalCapacity来确保可以创建两个局部引用,一个类引用,一个返回值。接下来,它从对象中获取类引用并查找方法ID。根据返回类型,switch语句调用相应的JNI方法调用函数。回调过程完成后,如果hasException不是NULL,我们调用ExceptionCheck检查异常。
      函数ExceptionCheck和ExceptionOccurred非常相似,不同的地方是,当有异常发生时,ExceptionCheck不会返回一个指向异常对象的引用,而是返回JNI_TRUE,没有异常时,返回JNI_FALSE。而ExceptionCheck这个函数不会返回一个指向异常对象的引用,它只简单地告诉本地代码是否有异常发生。上面的代码如果使用ExceptionOccurred的话,应该这么写:
      · if (hasException) {
      · jthrowable exc = (*env)->ExceptionOccurred(env);
      · hasException = exc != NULL;
      · (
      env)->DeleteLocalRef(env, exc);
      }
      为了删除指向异常对象的局部引用,DeleteLocalRef方法必须被调用。
      使用JNU_CallMethodByName这个工具函数,我们可以重写Instance-MethodCall.nativeMethod方法的实现:
      · JNIEXPORT void JNICALL
      · Java_InstanceMethodCall_nativeMethod(JNIEnv *env, jobject obj)
      · {
      · printf("In C\n");
      · JNU_CallMethodByName(env, NULL, obj, "callback", "()V");
      · }
      调用JNU_CallMethodByName函数后,我们不需要检查异常,因为本地方法后面会立即返回。

    相关文章

      网友评论

          本文标题:异常处理

          本文链接:https://www.haomeiwen.com/subject/uuadkttx.html