JAVA本地接口(JNI),是JAVA比较特殊的课题,因为JAVA本地接口(JNI)设计不只是JAVA语言设计,它还是JAVA与C或C++程序设计语言结合的课题.
通常来说一般会在如下情况使用JNI技术:
1,应用需要调用JAVA语言不支持的依赖于系统平台的特性.
2,为了整合一些遗留下来的非JAVA语言开发的系统.
3,为了创建节省时间的应用,不得不采用低级语言.然后通过JAVA调用.
JNI实现了java和c,c++ ,汇编等语言的双向调用。
简单的例子:
JAVA:
Employee中的一个本地方法native showSalary();
C
头文件:*.h
JNI对于本地方法的声明格式有特殊要求,但是JAVA提供了一个非常好用的工具,操作如下:
>javac Employee.java // 得到class文件
>javah Employee // 为Employee.class文件生成对应的Employee.h
生成文件大致如下(省略一些注释和没用的代码,熟悉c语言的人应该很容易看懂):
#include <jni.h> //该文件存在于jdk厂商提供的jdk安装包内,通常位于安装目录include目录下
#ifndef _Included_Employee
#define _Included_Employee
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT void JNICALL Java_Employee_showSalary(JNIEnv*,jobject);
//这个方法就是本地方法的声明部分,本地方法在c语言端的声明
#ifdef __cplusplus
}
#endif
#endif
创建c语言的主文件:
创建*.c文件:
编译成库:
之后放入指定的文件夹就可以被引用到了。
windows :cl -Ic:\java\include -Ic:\java\include\win32 -LD Employee.c -F employee
linux :gcc -shared -i$JDK_HOME/include -I/usr/include Employee.c -o libemployee.co
UNIX/Solaris:cc -G -I/usr/local/java/include -I/usr/local/java/include/solaris Employee.c -o libemployee.so
执行:
将库文件放入系统目录,WINDOWS为WINNT/System32,linux/unix为LD_LIBRARY_PATH,也可以设置系统路径
setenv LD_LIBRARY_PATH /*.so路径/:$LD_LIBRARY_PATH
>java -cp . Employee
JNI技术中数据类型与处理方法
在jni.h文件中定义有对访问对象实例函数与用来访问对象实例属性的方法.GetXXXField/GetStaticXXXField用来获取相应变量域的值和静态域的值,还有对应的用来设置值的函数SetXXXField/SetStaticXXXField函数.除了对域访问的函数以外还有对应的访问方法的函数一般格式如下CallXXXMethod/CallStaticXXXMethod,其中XXX代表访问类型.由于函数众多不一一介绍,具体定义都在jni.h文件里
访问JNI本地数据类型的方法
如下:
JNIEXPORT jstring JNICALL Java_Prompt_getLine(JNIEnv* env, jobject obj, jstring prompt) {
char buff[128];
const char *str = (*env)->GetStringUTFChars(env, prompt,0); //从java获取数据
printf("%s",str);
(*env)->ReleaseStringUTFChars(env,prompt,str); //释放资源
scanf("%s\n",buff);
return (*env)->NewStringUTF(env,buff); // 返回jstring类型
}
使用JNIEnv接口指针:
本地方法中使用JAVA对象,例如String,都要通过本地环境接口指针来完成,如上例.而且他本身也是调用函数的第一个参数.
在JNI本地方法中访问数组:
JNIEXPORT jint JNICALL Java_IntArray_releaseArray(JNIEnv *env, jobject obj, jintArray arr){
...
int i;
int sum = 0;
jsize len = (*env)->GetArrayLength(env,arr);
jint *body = (*env)->GetFloatArrayElements(env,arr,0);
for (i=0;i<len;i++){ sum+= body[i];}
(*env)->ReleaseIntArrayElements(env,arr,body,0);
return sum;
}
当然也有对应于不同基本类型数组的模版函数Get<type>ArrayElements/Release<type>ArrayElements,但是如果是一个非常巨大的数组的话,也要一次取全部数组?这样会有很大的内存消耗的.JNI因此也提供了一个可以指定读取某一处元素的函数Get/Set<type>ArratRegin();
当元素为一个对象的时候可以使用GetObjectArrayElement()/SetObjectArrayElement()来取和更新指定元素.
JNI中的主要技术:
局部引用与全局引用
由于JNI一般情况下将为JAVA对象创建一个本地引用,这是因为本地引用可以保证被JAVA虚拟机最终在系统中释放,归还系统资源,当程序在那个创建局部引用的本地方法中执行了返回操作后,这个局部引用也就无效了.因此应该避免如下写法:
static jclass cls = 0;
JNIEXPORT void JNICALL Java_FieldAccess_accessFields(JNIEnv* env, jobject obj) {
cls = (*env)->GetObjectClass(env,obj);
......
}
当第二次调用该函数的时候,由于cls指向的引用已经失效,所以会引起错误.正确的方法是:
static jclass cls = 0;
JNIEXPORT void JNICALL Java_FieldAccess_accessFields(JNIEnv* env, jobject obj) {
jclass clsTmp = (*env)->GetObjectClass(env,obj);
clsTmp= (*env)->NewGlobalRef(env,cls1);
......
}
这样这个引用对象会一直存在,当不在使用的时候会自动被JVM回收或直到调用函数DeleteGlobalRef(),所以要记得用完后释放该全局引用对象.但是当创建大量局部引用,或者大数据量对象的时候,为了避免局部引用表溢出,需要显式调用DeleteGlobalRef(),
处理本地方法引起的JAVA错误:
...
(*env)->ExceptionDescribe(env); // 输出一些调试信息
(*env)->ExceptionClear(env); // 清除以前的异常信息
jthrowable newExceCls = (*env)->FindClass(env,"java/lang/IllegalArgumentException");
if(newExceCls==0) return ;
(*env)->ThrowNew(env,newExecCls,"thrown from c code"); //如果存在异常,抛出交由JAVA端处理.
...
当然也可以使用ExceptionOccurred()
...
jthrowable cls = (*env)->ExceptionOccurred(env);
if(cls != 0) (*env)->ThrowNew(env,cls ,"thrown from c code");
线程与本地方法
1,JNI接口指针JNIEnv*仅在当前线程有效,不能传递或者保留到全局引用中以备后用.
2,绝对不可以将一个局部引用从一个线程传递给另一个线程.
3,合理仔细使用全局变量.
本地方法的线程同步
...
(*env)->MonitorEnter(env,obj);
...//同步代码
(*env)->MonitorExit(env,obj);
...
通常来说一般会在如下情况使用JNI技术:
1,应用需要调用JAVA语言不支持的依赖于系统平台的特性.
2,为了整合一些遗留下来的非JAVA语言开发的系统.
3,为了创建节省时间的应用,不得不采用低级语言.然后通过JAVA调用.
JNI实现了java和c,c++ ,汇编等语言的双向调用。
简单的例子:
JAVA:
Employee中的一个本地方法native showSalary();
class Employee {
[color=red]public native void showSalary();[/color]
static {
System.loadLibrary("Employee"); // c/c++语言的库文件,windows用dll,linux/unix用.so文件,本例中是Employee.dll文件
该文件必须放在java应用可以找到的路径下,windows下一般放在Windows/System下,这样java应用就可以装载这个库文件了。
}
public static void main(String[] args) {
(new Employee()).showSalary();
}
}
C
头文件:*.h
JNI对于本地方法的声明格式有特殊要求,但是JAVA提供了一个非常好用的工具,操作如下:
>javac Employee.java // 得到class文件
>javah Employee // 为Employee.class文件生成对应的Employee.h
生成文件大致如下(省略一些注释和没用的代码,熟悉c语言的人应该很容易看懂):
#include <jni.h> //该文件存在于jdk厂商提供的jdk安装包内,通常位于安装目录include目录下
#ifndef _Included_Employee
#define _Included_Employee
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT void JNICALL Java_Employee_showSalary(JNIEnv*,jobject);
//这个方法就是本地方法的声明部分,本地方法在c语言端的声明
#ifdef __cplusplus
}
#endif
#endif
创建c语言的主文件:
创建*.c文件:
#include <jni.h>
#include "Employee.h"
#include <stdio.h>
JNIEXPORT void JNICALL Java_Employee_showSalary(JNIEnv* env, jobject obj){
printf("HELLO WORLD");
return;
}
编译成库:
之后放入指定的文件夹就可以被引用到了。
windows :cl -Ic:\java\include -Ic:\java\include\win32 -LD Employee.c -F employee
linux :gcc -shared -i$JDK_HOME/include -I/usr/include Employee.c -o libemployee.co
UNIX/Solaris:cc -G -I/usr/local/java/include -I/usr/local/java/include/solaris Employee.c -o libemployee.so
执行:
将库文件放入系统目录,WINDOWS为WINNT/System32,linux/unix为LD_LIBRARY_PATH,也可以设置系统路径
setenv LD_LIBRARY_PATH /*.so路径/:$LD_LIBRARY_PATH
>java -cp . Employee
JNI技术中数据类型与处理方法
在jni.h文件中定义有对访问对象实例函数与用来访问对象实例属性的方法.GetXXXField/GetStaticXXXField用来获取相应变量域的值和静态域的值,还有对应的用来设置值的函数SetXXXField/SetStaticXXXField函数.除了对域访问的函数以外还有对应的访问方法的函数一般格式如下CallXXXMethod/CallStaticXXXMethod,其中XXX代表访问类型.由于函数众多不一一介绍,具体定义都在jni.h文件里
访问JNI本地数据类型的方法
如下:
JNIEXPORT jstring JNICALL Java_Prompt_getLine(JNIEnv* env, jobject obj, jstring prompt) {
char buff[128];
const char *str = (*env)->GetStringUTFChars(env, prompt,0); //从java获取数据
printf("%s",str);
(*env)->ReleaseStringUTFChars(env,prompt,str); //释放资源
scanf("%s\n",buff);
return (*env)->NewStringUTF(env,buff); // 返回jstring类型
}
使用JNIEnv接口指针:
本地方法中使用JAVA对象,例如String,都要通过本地环境接口指针来完成,如上例.而且他本身也是调用函数的第一个参数.
在JNI本地方法中访问数组:
JNIEXPORT jint JNICALL Java_IntArray_releaseArray(JNIEnv *env, jobject obj, jintArray arr){
...
int i;
int sum = 0;
jsize len = (*env)->GetArrayLength(env,arr);
jint *body = (*env)->GetFloatArrayElements(env,arr,0);
for (i=0;i<len;i++){ sum+= body[i];}
(*env)->ReleaseIntArrayElements(env,arr,body,0);
return sum;
}
当然也有对应于不同基本类型数组的模版函数Get<type>ArrayElements/Release<type>ArrayElements,但是如果是一个非常巨大的数组的话,也要一次取全部数组?这样会有很大的内存消耗的.JNI因此也提供了一个可以指定读取某一处元素的函数Get/Set<type>ArratRegin();
当元素为一个对象的时候可以使用GetObjectArrayElement()/SetObjectArrayElement()来取和更新指定元素.
JNI中的主要技术:
局部引用与全局引用
由于JNI一般情况下将为JAVA对象创建一个本地引用,这是因为本地引用可以保证被JAVA虚拟机最终在系统中释放,归还系统资源,当程序在那个创建局部引用的本地方法中执行了返回操作后,这个局部引用也就无效了.因此应该避免如下写法:
static jclass cls = 0;
JNIEXPORT void JNICALL Java_FieldAccess_accessFields(JNIEnv* env, jobject obj) {
cls = (*env)->GetObjectClass(env,obj);
......
}
当第二次调用该函数的时候,由于cls指向的引用已经失效,所以会引起错误.正确的方法是:
static jclass cls = 0;
JNIEXPORT void JNICALL Java_FieldAccess_accessFields(JNIEnv* env, jobject obj) {
jclass clsTmp = (*env)->GetObjectClass(env,obj);
clsTmp= (*env)->NewGlobalRef(env,cls1);
......
}
这样这个引用对象会一直存在,当不在使用的时候会自动被JVM回收或直到调用函数DeleteGlobalRef(),所以要记得用完后释放该全局引用对象.但是当创建大量局部引用,或者大数据量对象的时候,为了避免局部引用表溢出,需要显式调用DeleteGlobalRef(),
处理本地方法引起的JAVA错误:
...
(*env)->ExceptionDescribe(env); // 输出一些调试信息
(*env)->ExceptionClear(env); // 清除以前的异常信息
jthrowable newExceCls = (*env)->FindClass(env,"java/lang/IllegalArgumentException");
if(newExceCls==0) return ;
(*env)->ThrowNew(env,newExecCls,"thrown from c code"); //如果存在异常,抛出交由JAVA端处理.
...
当然也可以使用ExceptionOccurred()
...
jthrowable cls = (*env)->ExceptionOccurred(env);
if(cls != 0) (*env)->ThrowNew(env,cls ,"thrown from c code");
线程与本地方法
1,JNI接口指针JNIEnv*仅在当前线程有效,不能传递或者保留到全局引用中以备后用.
2,绝对不可以将一个局部引用从一个线程传递给另一个线程.
3,合理仔细使用全局变量.
本地方法的线程同步
...
(*env)->MonitorEnter(env,obj);
...//同步代码
(*env)->MonitorExit(env,obj);
...