一、基本介绍
1、JNI是什么?
Java本机接口(Java Native Interface (JNI))是本机编程接口,它是JDK的一部分,JNI它提供了若干的API,实现了和Java和其他通信(主要是C&C++)。
2、JNI有什么用?
JNI最常见的两个应用:从Java程序调用C/C++,以及从C/C++程序调用Java代码。
3、使用JNI需要什么环境?
(1)、JDK
工具及组件:(Java编译器:javac.exe 、JVM :java.exe 、本地方法C文件生成器:javah.exe)
库文件和头文件:jni.h( C头文件)、jvm.lib 和jvm.dll(windows下) 或libjvm.so(linux下)。
(2)、能够创建动态链接库的C和C++编译器
最常见的两个C编译器是用于Windows的Visual C++ 或用于基于UNIX系统的gcc/g++。
二、Java调用C++代码的完美方法
JNI是Java与C++之间的桥梁,它们之间的层次关系如下图所示:、
(1)、实现java端代码
根据c++代码的接口定义,封装一个java版本的接口实现,以分词为例,在c++中定义的接口为:
那么在java中对应这些接口的实现,new一个java文件WordSegment.java:
在java只需要定义函数,在JNI层中实现,跟普通的java函数定义唯一区别是多了一个native关键字。
(2)、生成JNI层的header文件
将上面的java文件用javac编译:
如果Windows下就进入cmd,linux直接终端中,cd到WordSegment.java所在的目录下,执行
javac WordSegment.java |
---|
执行成功,会在当前路径下生成一个WordSegment.class文件,这个是java编译出来的文件。
然后再执行(注意,是WordSegment,不带.class后缀)
javah WordSegment |
---|
生成JNI的头文件。执行成功会生成WordSegment.h文件,在头文件中可以看到JNI对java接口做了转译
从.h文件中函数名称可以看到,jd_com这部分是对应WordSegment.java中package的路径,WordSegment是java的类名,后边是接口函数名称,
这个也是JNI与java中函数进行连接的一个标识,就是说如果你改了java文件中的类名、package名称或者接口名称,其实也没有必要再走一遍以上步骤
重新生成.h文件,直接把JNI层的函数名称改一下就可以了。另外这个WordSegment.h的文件名也不重要,改了也没关系,这几个函数定义,你可以拷贝到任意
一个头文件中,只要将它们实现即可。
函数参数的部分
- 第一个JNIEnv*,这个参数是java的环境参数,JNI也是主要通过它实现C++与java的互通
- 第二个Jobject是调用这个接口的java对象,在我们例子中就是WordSegment.java中WordSegment类的对象
- 后边接下来才是接口函数真正的参数
在JNI对java中的各种数据类型也进行的转译,这些定义都在jni.h中。
比如 String ->jstring ,int->jint 。java中所有的对象类型,在JNI中都统一标识为jobject
(3)、实现JNI接口
接口的实现就比较简单了,可以直接在.h中实现,也可以再new一个WordSegment.cpp文件,在cpp文件中实现函数
在我们的例子中比较简单,直接调用分词那边提供好的接口就可以了。
在JNI实现函数中调用C++的接口StringSegment(),是java对c++的调用,接下来就以上面Java_jd_com_WordSegment_StringSegment函数中的ArrayList为例,看一下怎么样实现jc++调用java的
- 获取一个java对象类,jclass cls_ArrayList = env->FindClass("java/util/ArrayList");
- 获取这个类的构造函数的函数ID:jmethodID construct = env->GetMethodID(cls_ArrayList,"
","()V"); - 通过构造函数ID,构建一个java类对象实例 :jobject obj_ArrayList = env->NewObject(cls_ArrayList,construct,"");
- 获取类Arraylist的add函数的函数ID:jmethodID arrayList_add = env->GetMethodID(cls_ArrayList,"add","(Ljava/lang/Object;)Z"); //第二个参数是函数名称(字符串),第三个是这个函数的参数、返回值,也是个字符串格式的
- 调用Arraylist的add函数:env->CallObjectMethod(obj_ArrayList, arrayList_add, jstr);
这里有个问题是,JNI下java和c++的各种标准基本数据类型是可以通用的,比如int long 、bool、float这些,但只要是对象类型就不可以了
所以string 到jstring需要做一个转换,另外vector也要转换成java下的ArrayList。下面一段代码是c++的string转换成JNI的jstring的函数。
(4)、生成动态库
JNI接口实现完成以后,可以有两种方法生成动态库
1、将JNI的实现WordSegment.h、WordSegment.cpp和分词的代码合到一个工程中,然后生成一个WordSegment.so动态库
2、分词代码自己打包成一个静态库libsegment.a,然后再单独编译WordSegment.cpp,编译时加入参数-lsegment,引入静态库,生成WordSegment.so
在java程序中又是怎么调用c++动态库的呢?
很简单,跟c++类似
- 如果你只是在你的java工程中调用C++的动态库使用的话,需要指定动态库的存放位置
|
---|
java.library.path是非java类包的位置如(dll,so)
如果你是用eclipse的话,工程右键properties->java Build Path->Libraries,找到JRE System Library下边的Native library location 然后edit,改成你的动态库存放位置
- 这里需要注意的是,如果你的java程序也是包装的一个接口的话,也就是说你要搞成一个jar包给别人用的时候,那么需要将WordSegment.so动态库一起打包起来,
解决方法是是在java的project中,加入一个natives文件夹,将WordSegment.so放进去,然后打jar包的时候natives文件夹也打进去
上面代码可以看到在load动态库的时候,做了一点小技巧,就是用的
NativeUtils.loadLibraryFromJar("/natives/"+System.mapLibraryName("WordSegment"));
这个util也比较简单,就是创建一个临时目录,把动态库拷贝过去,然后load进去
NativeUtil的git地址:https://github.com/adamheinrich/native-utils
(5)、环境、配置等其他设置
JNI要依赖jni.h这个头文件,这个需要在jdk的安装目录下的include目录下找,例如我的jdk安装目录在C:\Program Files\Java\jdk1.8.0_181\include下,
另外在include下的win32文件夹中有个jni_md.h文件,需要把这两个头文件拷贝到WordSegment.h的目录下,把 #include
(6)如果接口是C++的,怎么存放指针?
jni提供的是存c的接口,只有函数,调用c++接口init中会new一个对象出来,怎么样保存它呢?因为访问其他接口的时候要通过对象指针来调用c++函数。
解决方案:在java中定义一个long成员来保存对象指针
在jni中存取方法: