一:JNI是什么?
JNI即Java原生接口(Java Native Interface,JNI),简单的讲就是在Java中调用非Java代码,在看王森的《java深度历险》时看到Java与MS Office,用Visual Studio.net来操控Java虚拟机器等文章发现Java还真是厉害,不仅Java可以调用非Java代码而且还可以反过来操作,当然这里面研究一下Java中调用非Java代码情况,即JNI接口。调用过程就是先加载含有JNI实现码的动态库然后调用JNI函数,它会自动去动态库中找相应的非Java实现码。
二:简单实例 HelloWorld (C和C++版及相关问题分析)
待补充
三:背景
现在有一个通讯中间件,通过C编写并封装在libSDK.a或者libSDK.so中,后台为C应用程序。现在需要Java程序与C后台应用程序通讯,而Java虽然有强大的网络编程能力,但为了使用现成的中间件所以需要通过Java调用中间件通讯API。通讯无非是请求包文发至后台再由后台响应数据包到请求方。
四:过程
1:由于我是一个没接触过Java的人,但我要给别人写一个用中间件的API产生的发到台后的封装函数,别人用Java来调用我这个接口,所以我想当然的把接口定义为:
int MonitorCommToCent(int tradeid, char *rspXMLBuff, const char *reqXMLBuff, size_t reqBuffLen)
交易码参数是告知后台中间件守护进程来调用相应的后台应用服务程序,我只要求Java给我一个XML的包文和长度就返回一个服务端的XML包文的响应。(当然这里跟XML没关系,只是我们的应用要求)
2:这样的定义当然我是没有管Java是怎么来调用的,因为我不知道JNI怎么实现,那是懂Java的人的任务,但当我周未正好有空去了解一下JNI的时候,发现我的接口定义的参数类型为const char *,size_t,char *,而我想Java中应该有对应的类型转换才能调用我的这个接口吧,可Java中只有final没有const,没有指针的概念,只有byte[]可以和char *相对应,因为在Java中传String应该更方便,在它的成员函数中找到byte[] getBytes(String charsetName),嗯,应该转换这个是没问题。而size_t在C中是一个unsigned int类型,而Java中都是有符号的类型。那别人怎么调用我的接口呢?后来想想,我是提供接口,Java并不关心JNI native function 是怎么实现的,怎么把请求发到后台,只要Java提供足够JNI需要的参数就行了,对于这样的需求那就是Java提供请求String并接收返回String。这是定义接口应该做到的功能。Java并不关心细节。
3:也许是因为我不懂Java的原因,所以站在C接口这边考虑问题,只要别人提供我C接口需的参数就行,而不管Java怎么提供怎么使用,而实际上定义接口是应该站在使用接口那端来考虑问题,只要提供你要的并返回我想要的就行。这是一个很正常的思路,但我一开始错了。
4:所以我站在Java应用接口的角度来设计这个接口,所以提供了这样的一个接口:
public native String MoniCommToCent(int tradeid, String reqBuff);
可以把Java端加载动态库并提供JNI原型的一个类写出来。如下:
public class CommToCent { static { try { System.loadLibrary("MoniCommCent"); //加载动态库 } catch(UnsatisfiedLinkError e) { System.err.println("Cannot load MoniCommCent library:\n " + e.toString()); } } public native String MoniCommToCent(int tradeid, String reqBuff); //JNI原型 public CommToCent() { // } }
这样的话别人就可以直接通过:
String rspStr = (new CommToCent()).MoniCommToCent(id, reqStr);
来应用我的接口了,嗯,这样很方便,接口就应该这样。 现在是需要实现我们的JNI的时候了,先要产生相应的头文件,再写C实现码,然后编译成动态库,先执行javac CommToCent.java 编译成CommToCent.class,不然直接执行javah CommToCent是产生不了头文件的,它需要 .class文件,然后执行javah CommToCent产生CommToCent.h 头文件,由javah通过JNI原型产生的C接口变成:
JNIEXPORT jstring JNICALL Java_CommToCent_MoniCommToCent(JNIEnv *, jobject, jint, jstring);
5:C程序实现如下:
JNIEXPORT jstring JNICALL Java_src_comm_CommToCent_MoniCommToCent (JNIEnv *env, jobject arg, jint tradeid, jstring reqStr) { unsigned char reqBuff[4096] = {0}; unsigned char rspBuff[4096] = {0}; char svrAddr[32] = {0}; int svrPort = 0; int reqBuffLen = 0; const jbyte *reqXMLBuff = NULL; jstring rspStr; int ret = -1;
//必需初始化成空字串,否则出错返回时rspStr还未初始化(接收端无法判断返回结果长度) //因为不允许直接在定义时初始化如:jstring rspStr = ""; rspStr = (*env)->NewStringUTF(env, "");
reqXMLBuff = (*env)->GetStringUTFChars(env, reqStr, JNI_FALSE); reqBuffLen = (*env)->GetStringUTFLength(env, reqStr);
//设置svrAddr, svrPort参数 memcpy(reqBuff, reqXMLBuff, reqBuffLen); if((ret = cliSndRcv(svrAddr, svrPort, reqBuff, NULL, rspBuff, NULL, 30)) == 0) { ret = head.Sleng; rspStr = (*env)->NewStringUTF(env, rspBuff); }
//通知虚拟机本地代码不再需要通过reqXMLBuff访问Java字符串。 (*env)->ReleaseStringUTFChars(env, reqStr, (const char *)reqXMLBuff);
return rspStr; }
编译: gcc -I$HOME/include -I$JAVA_HOME/include -I$JAVA_HOME/include/linux -fPIC -c CommToCent.c
因为我的JDK是在LINUX上的,如果是UnixWare等可能include/linux为include/unixware.而且在unixware上需要使用UDK来编译而非gcc.
gcc -shared -o libMoniCommCent.so CommToCent.o –L. -L$HOME/lib -L/usr/lib –lSDK
libSDK我用的是.a版本,因为使用.so版本的话加载MoniCommCent时需要两个libSDK.so,而利用静态库只需要一个,比较方便点。 设置LD_LIBRARY_PATH:export LD_LIBRARY_PATH=`pwd`当前目录。
写一个Java程序来测试一下。不用单独写个CommToCentApp.java,只需要在CommToCent.java中加入一个main即可,这也是Java编码的习惯,在每个 .java中写个main以便测试。如下:
public static void main(String args[]) { String reqBuff = ""; String rspBuff = ""; int tradeid = 1001;
if (args.length { System.err.println("usage: java CommToCent reqString [reqString2 ...]"); return ; }
for (int i = 0; i reqBuff += args;
CommToCent comm = new CommToCent(); rspBuff = comm.MoniCommToCent(tradeid, reqBuff); if (rspBuff.length() == 0) System.out.println("CommToCent Return Error!"); else System.out.println("rspBuffLen = [" + rspBuff.length() + "]" + "rspBuff = [" + rspBuff + "]"); }
重新编译CommToCent.java然后执行:java CommToCent this is a test string 返回thisisateststring 后台测试程序将请求包原样返回。这是正确的。
四:还没完?
大家知道在正式项目开发中要把Java用package来管理,这是比较好的方式,所以我们也要把我们的通讯接口放在package中。如src.comm 这样别人就可以用inmport src.comm来调用我的接口类了。 在CommToCent.java中加入package src.comm; 把CommToCent.java放在 ./src/comm/CommToCent.java 按上面的方式编译.java产生.class如:java src/comm/CommToCent.java,接着就执行java src.comm.CommToCent test string,出问题了,加载动态库出错。什么原因?原来加入package后,完整的类名应该是src.comm.CommToCent,而我的JNI还以为完整的类名是CommToCent,所以我们需要重新产生一个.so出来,需要重新编译C代码,重新产生.h。如:javah src.comm.CommToCent 会在当前目录产生一个src_comm_CommToCent.h文件,而非src/comm/下面,所以修改.c程序,换个头文件,并修改成新的JNI接口原型,如:
JNIEXPORT jstring JNICALL Java_src_comm_CommToCent_MoniCommToCent(JNIEnv *, jobject, jint, jstring);
看到函数名都带路径的。这就是有没有package的区别所在了。重新编译成.so后执行成功。
五:参数资料
http://java.ccidnet.com/art/3539/20060719/646879_1.html
http://java.ccidnet.com/art/3539/20060719/646921_1.html
一开始以为JNI是很偏的东西,因为相关资料不多,可能实际应用其实并不多吧,所以没想到javadoc上去参考。其实那才是真正的资料。
|
用户点评