第六章 异常
我们已经遇到大量在本地代码中需要检查执行JNI方法后可能产生的错误。这一章将介绍本地代码如何从这些错误状况中检测和修复。
我们将会重点关注作为JNI函数调用的结果发生的错误,而不是在本地代码中发生的任意错误。如果一个本地方法进行了操作系统调用,则只需要按照文档说明的方式来检查系统调用中可能发生的错误。另一方面,如果本地方法想Java API方法进行回调,则必须按照本章中描述的步骤来正确的检查和修复方法执行期间可能产生的异常。
6.1 概述
我们通过一些列的例子来介绍JNI异常处理函数。
6.1.1 在本地代码中缓存和抛出异常
下面的程序显示如何定义一个会抛出异常的本地方法。CatchThrow类定义了一个doit方法,并且表明该方法会抛出一个IllegalArgumentException:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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"); } } |
CatchThrow.main方法调用本地方法doit,doit的实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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"); } } |
搭配本地库运行这个程序可以得到如下输出:
1 2 3 4 5 6 |
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将控制权返回给本地方法后,本地代码通过JNI方法ExceptionOccurred会检测到这个异常。在我们的例子当中,当异常被检测到,本地方法通过调用ExceptionDescribe会输出一个关于这个异常的描述性信息,使用ExceptionClear方法清除这个异常并且抛出一个IllegalArgumentException异常作为替代。
通过JNI(例如通过调用ThrowNew)引起的挂起异常不会立刻破坏本地方法的执行。这和Java编程语言中异常的行为是不同的。当使用Java编程语言抛出一个异常的时候,Java虚拟机会自动将控制流程转移到最近的符合异常类型的try/catch代码块中。然后Java虚拟机会清除这个挂起的异常并执行异常处理。相比之下,在异常发生之后,JNI程序员必须显示的进行流程控制。
6.1.2 一个有用的辅助函数
抛出一个异常,首先需要查找这个异常的类然后调用ThrowNew方法。为了简化这个任务,我们可以编写一个抛出一个命名异常的有用函数:
1 2 3 4 5 6 7 8 9 |
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表示JNI Utilities。JNU_ThrowByName首先通过FindClass方法找到异常的类。如果FindClass调用失败(返回NULL),虚拟机必须抛出一个异常(例如NoClassDefFoundError)。在本次JNU_ThrowByName不会尝试抛出另外一个异常。如果FindClass成功返回,我们通过调用ThrowNew抛出一个命名的异常。当JNU_ThrowByName调用返回的时,它保证有一个挂起的异常,尽管这个挂起的异常不一定是由name参数指定的。在这个方法中,我们确保删除引用异常类的本地引用。如果FindClass失败并返回NULL,将NULL传递给DeleteLocalRef将会是一个空操作,这将是一个恰当的操作。
6.2 恰当的异常处理
JNI程序员必须遇见所有可能的异常情况并且编写相应的代码检查和处理这些情况。适当的异常处理有时候是乏味的,但是为了提高程序的鲁棒性却是必须的。
6.2.1 异常检查
有两种方式检查是否有错误产生了。
(1)大多数JNI方法使用一个明显的返回值(例如NULL)来表明产生了一个错误。返回错误值也意味着在当前线程中产生了一个挂起的异常。(在返回值中编码错误情况是C中的常见用法)
下面的例子中表明GetFieldID返回NULL值来检查错误情况。例子包含两个部分:类Window定义了一些实例字段(handle,length和width)并且有一个本地方法用来缓存这些字段的字段ID。尽管这些字段确实已经在Window类中了,我们仍然需要检查GetFieldID可能返回的错误值,因为虚拟机可能无法分配足够的内容用于保存字段ID。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
/* a class in the Java programming language */ public class Window { long handle; int length; int width; static native void initIDs(); static { initIDs(); } } /* C code that implements Window.initIDs */ jfieldID FID_Window_handle; jfieldID FID_Window_length; jfieldID FID_Window_width; JNIEXPORT void JNICALL Java_Window_initIDs(JNIEnv *env, jclass classWindow) { FID_Window_handle =(*env)->GetFieldID(env, classWindow, "handle", "J"); if (FID_Window_handle == NULL) { /* important check. */ return; /* error occurred. */ } FID_Window_length =(*env)->GetFieldID(env, classWindow, "length", "I"); if (FID_Window_length == NULL) { /* important check. */ return; /* error occurred. */ } FID_Window_width = (*env)->GetFieldID(env, classWindow, "width", "I"); /* no checks necessary; we are about to return anyway */ } |
(2)当使用一个JNI方法,它的返回值无法标记一个错误的产生的时候,本地代码就必须依赖引起异常来进行错误检查。在当前线程中,用于检查是否有挂起的异常的JNI函数是ExceptionOccurred。(ExceptionCheck在Java 2 JDK 1.2版本中加入。)例如,JNI方法CallIntMethod不能通过编码一个错误情况来作为返回值,典型的错误情况返回值例如-1和NULL都不能很好的工作,因为当他们调用这个方法时,这些都是合理的返回值。考虑有一个Fraction类,它的floor方法返回分数值的整数部分并且有其他的本地代码调用这个函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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 异常处理
本地代码可以通过两种方式处理挂起的异常:
- 本地代码实现能够选择立即返回,在方法调用者处进行异常处理
- 本地代码可以通过调用ExceptionClear来清除异常然后执行它自己的异常处理函数
在调用任何后续JNI函数之前,检查、处理和清除挂起的异常时非常重要的。在带有挂起的异常,尚未明确清理的异常时调用大多数JNI方法都有可能导致意外的结果。当在当前线程中有一个挂起的异常时,你仅可以安全的调用一小部分JNI方法,11.8.2节列出了这些JNI函数的完整列表。一般来说,当存在一个挂起的异常时,你可以调用专门的JNI函数来处理异常和释放通过JNI暴露出来的各种虚拟机资源。
当异常发生时,经常有必要去释放各种资源。在下面的例子当中,本地方法首先通过一个GetStringChars调用获取字符串的内容。如果在后续的处理中产生错误,它会调用ReleaseStringChars:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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); } |
第一次调用ReleaseStringChars是在有挂起的线程出现的时候。本地方法实现释放字符串资源并在之后立即返回,而不需要首先清除异常。
6.2.3 有用的辅助函数中的异常
程序员编写有用的辅助函数时应特别注意确保异常传播到本地调用方法中。我们特别强调一下两点:
- 优选方案,辅助函数应该提供特殊的返回值指示发生了异常。这简化了调用者检查待处理异常的任务。
- 此外,辅助函数应遵循在管理异常代码是注意管理本地应用的规则。
为了说明,让我们介绍一个基于实例方法的名字和描述符执行回调的辅助函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
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的指针。如果在一切正常,jboolean将被设置为JNI_FALSE,如果在执行这个函数期间的任何时候发生了异常,那么jboolean将被设置为JNI_TRUE。这将给JNU_CallMethoByName的调用者一个明显的方法去检查是否有发生异常。
JNU_CallMethodByName首先确保他能够创建两个本地引用:一个用于类引用,另一个用于方法调用返回的结果。接下来,它从对象获取到类引用,并查找到方法ID。根据返回值的类型,switch语句将调度到相应的JNI方法调用函数。回调返回后,如果hasException不为NULL,我们调用ExceptionCheck来检查挂起的异常。
ExceptionCheck方法是在Java 2 SDK 1.2中新加进去的。它类似于ExceptionOccurred函数。不同之处在于ExceptionCheck不会返回对异常对象的引用,但是当有挂起的异常时返回JNI_TRUE,在没有挂起的异常的时候放回JNI_FALSE。当本地代码仅需要知道是否发生异常但是不需要获取对异常对象的引用时,ExceptionCheck简化了本地引用的管理。前面的代码在使用JDK 1.1中,将重写如下:
1 2 3 4 5 |
if (hasException) { jthrowable exc = (*env)->ExceptionOccurred(env); *hasException = exc != NULL; (*env)->DeleteLocalRef(env, exc); } |
额外的DeleteLocalRef调用时必须的,以删除对异常对象的本地引用。
使用JNU_CallMethodByName方法,我们可以重写4.2节中InstanceMethodCall.nativeMethod的实现:
1 2 3 4 5 |
JNIEXPORT void JNICALL Java_InstanceMethodCall_nativeMethod(JNIEnv *env, jobject obj) { printf("In C\n"); JNU_CallMethodByName(env, NULL, obj, "callback", "()V"); } |
在JNU_CallMethodByName调用会我们不需要检查异常,因为本地代码会立刻返回。