JNI技术解析第六章 异常

前面几章也提到过在原生代码中对JNI函数调用后总会检查错误,如果有异常,则直接中断执行,直接返回。本章介绍在原生代码中检测错误并从异常中恢复。本章关注的异常处理是调用JNI函数之后产生的异常,而不是其他的异常。比如,在原生代码中使用了系统调用,那么对错误的处理就要遵循系统调用的规则;如果使用了Java API,那么就应该遵循Java的规则。本章值关注JNI函数对异常处理的规则。

6.1 概述

通过几个例子来介绍JNI提供的异常处理函数。

6.1.1 在原生代码中缓存并抛出异常

下面的程序展示如何生命一个抛出异常的原生函数。CatchThrow类声明了doit()原生函数,该函数抛出IllegalArgumentException:

class CatchThrow {
private native void doit() throws IllegalArgumentException;
//回调函数,在doit()中调用它
private void callback() throws NullPointerException {
throw new NullPointerException(“CatchThrow.callback”);
}

public static void main(String args[]) {
    CatchThrow c = new CatchThrow();
    try {
        c.doit();//调用doit(),该原生函数会抛出异常
    } catch (Exception e) {
        System.out.println("In Java:nt" + e);
    }
} 
static {
    System.loadLibrary("CatchThrow");
}

}




doit()原生函数的代码如下:

JNIEXPORT void JNICALL Java_CatchThrow_doit(JNIEnv env, jobject obj)
{
jthrowable exc;
jclass cls = (
env)->GetObjectClass(env, obj);
//找到CatchThrow类的callback()方法的ID
jmethodID mid = (env)->GetMethodID(env, cls, “callback”, “()V”);
if (mid == NULL)
return;
//调用callback()方法,它抛出NullPointerException异常。
(
env)->CallVoidMethod(env, obj, mid);
exc = (env)->ExceptionOccurred(env);
if (exc) {
//如果捕获到了异常
jclass newExcCls;
//打印异常的描述信息
(env)->ExceptionDescribe(env);
//清除异常
(env)->ExceptionClear(env);
//找到IllegalArgumentException类
newExcCls = (env)->FindClass(env, “java/lang/IllegalArgumentException”);
if (newExcCls == NULL)
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





在上面的程序中,原生函数使用CallVoidMethod()函数回调了Java中的callback()方法,这个方法抛出NullPointerException异常。当CallVoidMethod()函数返回时,原生函数使用ExceptionOccurred()函数检查调用CallVoidMethod()函数期间是否抛出了异常,如果有异常抛出,就使用ExceptionDescribe()函数打印描述信息,使用ExceptionClear()函数清除异常。清除完Java回调函数中抛出的异常后,原生代码自己抛出一个IllegalArgumentException异常,并返回,该异常被Java的catch语句捕获。

在处理异常时,原生代码与Java代码有所区别。在Java中,当抛出了异常,Java虚拟机自动将代码控制流转换到与异常匹配的try/catch语句块,然后清除异常,执行异常处理代码。而原生代码中,当调用JNI函数期间抛出了异常(出现了错误),程序员需要显式的自己控制执行流。

6.1.2 为抛出异常制作工具函数

在原生代码中抛出异常包括两步:首先使用FincClass()函数找到要抛出的异常的类(IllegalArgumentException类),然后使用ThrowNew()函数抛出该异常。为了简化任务,我们可以编写一个原生工具函数封装在原生代码中抛出异常的工作。

下面我们编写了一个抛出指定名称和内容的异常的工具函数:

void JNU_ThrowByName(JNIEnv env, const char name, const char msg)
{
jclass cls = (
env)->FindClass(env, name);
/ 如果cls是NULL,说明已经有异常被抛出了 /
if (cls != NULL) {
(env)->ThrowNew(env, cls, msg);
}
/
在工具函数中一定要特别注意释放局部引用 /
(
env)->DeleteLocalRef(env, cls);
}





在本文种,JNU前缀代表JNI Utilities(JNI工具函数)。

该工具函数使用FindClass()函数索引name指定的类名,如果返回NULL,说明没有找到这个name的类,Java虚拟机会抛出NoClassDefFoundError的异常。JNU_ThrowByName()工具函数只打算返回一个挂起的(未处理、未清除的)异常,因此,若JNI函数本身发生异常的话,该工具函数直接调用DeleteLocalRef()函数释放空的(cls == NULL)局部引用,这个操作不会做任何事情,当工具函数返回时,挂起的异常(这时这个异常由FindClass查找name类失败而抛出的)就会被Java代码捕获。

如果FindClass()函数不返回NULL,说明找到了name指定的类,并创建了局部引用。此时cls != NULL,工具函数就抛出cls代表的异常,并在返回前释放了cls。当工具函数返回时,挂起的异常(此时的异常由ThrowNew()手动抛出的)也会被Java代码捕获。

6.2 适当的异常处理

在编写JNI程序的时候,程序员必须要特别注意发生异常的条件,并编写代码检查和处理这些异常。适当的异常处理虽然有些单调,但却是健壮程序的必要部分。

6.2.1 检查异常

在JNI中有两种方法检测是否抛出了异常:

1.大多数的JNI函数会返回一个特定值(如NULL)来表示调用此函数期间发生了错误,同时该返回值也表明了当前线程存在一个挂起的(未处理的)异常。

2.当JNI函数的返回值无法表明是否发生了异常时,就必须使用JNI提供的函数来检查是否发生了异常。在当前线程中检查是否有异常抛出可以使用ExceptionOccurred()函数和ExceptionCheck()函数。ExceptionCheck()函数是Java SDK 1.2中新推出的函数。

6.2.2 处理异常

原生函数在检测到有异常抛出后,同样有两种方法处理异常:

1.直接返回,由原生函数的调用者进行异常处理。

2.使用JNI提供的ExceptionClear()函数清理异常,并执行自己的异常处理代码。

切记:在进行后续调用JNI函数之前必须检查、处理并清除掉抛出的异常。在当前线程中如果有未处理的(未清除的)异常时,调用大部分的JNI函数的结果是未定义的(可能不会有影响,可能会导致程序崩溃),只有少部分的JNI函数可以被安全调用。一般来讲,当有异常抛出时,你只能调用那些用来检查、处理和清除这个异常的相关的JNI函数。当检测到异常被抛出时,及时的释放引用。

6.2.3 公共工具函数中处理异常

在编写工具函数时应特别注意要把异常传递到其调用者那里,尤其注意一下两方面:

1.工具函数应该尽量返回一个能够标识是否抛出了异常的错误返回码,这样可以简化其调用者对异常检查和处理的工作。

2.工具函数在其自己的异常处理代码中应该格外注意管理好引用(见第五章)。

我们使用一个例子来说明,下面的工具函数根据参数中指定的名字和方法描述符调用Java类中的某个方法。

jvalue JNU_CallMethodByName(JNIEnv env, jboolean hasException, jobject obj, const char name, const char descriptor, …)
{
va_list args;
jclass clazz;
jmethodID mid;
jvalue result;
//申请创建2个局部引用
if ((env)->EnsureLocalCapacity(env, 2) == JNI_OK) {
clazz = (
env)->GetObjectClass(env, obj);
//获取clazz所指类的名为name、签名为descriptor的方法的ID
mid = (env)->GetMethodID(env, clazz, name, descriptor);
if (mid) {
const char
p = descriptor;
//跳过方法签名中的参数部分
while (p != ‘)’) p++;
//跳过’)’
p++;
//对方法签名中的返回值进行处理
va_start(args, descriptor);
switch (
p) {
case ‘V’: //返回void
(env)->CallVoidMethodV(env, obj, mid, args);
break;
case ‘[‘://返回数组类型
case ‘L’:
result.l = (
env)->CallObjectMethodV(env, obj, mid, args);
break;
case ‘Z’://返回boolean类型
result.z = (env)->CallBooleanMethodV(env, obj, mid, args);
break;
case ‘B’://返回byte类型
result.b = (
env)->CallByteMethodV(env, obj, mid, args);
break;
case ‘C’://返回char类型
result.c = (env)->CallCharMethodV(env, obj, mid, args);
break;
case ‘S’://返回short类型
result.s = (
env)->CallShortMethodV(env, obj, mid, args);
break;
case ‘I’://返回int类型
result.i = (env)->CallIntMethodV(env, obj, mid, args);
break;
case ‘J’://返回long类型
result.j = (
env)->CallLongMethodV(env, obj, mid, args);
break;
case ‘F’://返回float类型
result.f = (env)->CallFloatMethodV(env, obj, mid, args);
break;
case ‘D’://返回double类型
result.d = (
env)->CallDoubleMethodV(env, obj, mid, args);
break;
default://非法
(env)->FatalError(env, “illegal descriptor”);
}
va_end(args);
}
//及时释放不再使用的局部引用
(
env)->DeleteLocalRef(env, clazz);
}
//如果传入的jbollean不是NULL的话
if (hasException) {
//检查是否发生异常。如果有异常,则返回JNI_TRUE,否则返回JNI_FALSE
hasException = (env)->ExceptionCheck(env);
}
return result;
}

在上述工具函数中,传入的jboolean hasException参数是该工具函数的调用者传入的一个boolean类型的变量的地址。如果传入的地址不是NULL的话,hasException就能够指明在调用工具函数期间是否抛出了异常。

工具函数首先向虚拟机申请创建两个局部引用:一个是clazz,保存相应类的引用;一个是result,保存Call<Typye>Method()函数的返回值。接下来获取obj的类的引用和方法的ID,根据方法的返回类型,switch语句块调用不同的Call<Type>Method()方法。当hasExcepton不是NULL时,就调用ExceptionCheck()函数检查在上述步骤中是否抛出了异常。

ExceptonCheck()函数是Java SDK 1.2推出的新的函数。它的功能与ExceptionOccurred()函数类似,不同之处在于ExceptionCheck()函数在有异常抛出时返回JNI_TRUE,无异常时返回JNI_FALSE,而不会像ExceptionOccurred()函数一样返回异常类对象的引用。当原生代码中只是想要知道是否发生异常(不需要异常类对象的引用)时,ExceptionCheck()函数就简化了局部引用的管理。上述代码中的ExceptionCheck()函数部分可以用ExceptionOccurred()函数改写:

if (hasException) {
//ExceptionOccurred()函数返回异常类对象的引用
jthrowable exc = (env)->ExceptionOccurred(env);
//如果exc!=NULL的话,说明抛出了异常,
hasException为JNI_TRUE,否则,没有发生异常,hasException为JNI_FALSE。 hasException = exc != NULL;
(env)->DeleteLocalRef(env, exc);
}





上面的(env)->DeleteLocalRef(env, exc);这条语句是必须的,在原生代码的异常处理代码中要记得释放不再使用的局部引用

之后,我们可以使用上面编写的JNU_CallMethodByName()工具函数了,在调用这个工具函数时,我们不必检查异常了,因为工具函数内部就已经完成了这些工作,并用hasException所指向的值来表示。

本章介绍了异常的检查、处理和清除等内容,始终注意,在原生代码中处理异常并不像Java中那样方便,需要程序员自己时刻注意。