?
由于工作需要,要写一段c++代码来调用java的api。下面把实现和调研的过程总结出来。
?
1. 如何解决?
首选JNI,首先对JNI的原理和使用方法简单调研一下,JNI的权威资料是:
http://java.sun.com/docs/books/jni/html/jniTOC.html
?
简单点说,JNI可以帮助我们解决两个问题:
1)实现java代码调用其他代码(c,c++,...)
大致的做法:
a)写java 类
?
class HelloWorld { private native void print(); public static void main(String[] args) { new HelloWorld().print(); } static { System.loadLibrary("HelloWorld"); } }
?
b)把需要由其他语言实现的逻辑在java类中用native关键字标示(注意native 方法只能有声明,不能有实现)
?
private native void print();
?
c)javac编译java类,获得对应的class文件
javac HelloWorld.java
?
d)javah -jni 生成本地方法对应的头文件
javah -jni HelloWorld
?
查看生成的头文件,核心部分是:
?
JNIEXPORT void JNICALL Java_HelloWorld_print (JNIEnv *, jobject);
?
e)编写本地方法的代码(c或者c++代码)
?
#include <jni.h> #include <stdio.h> #include "HelloWorld.h" JNIEXPORT void JNICALL Java_HelloWorld_print(JNIEnv *env, jobject obj) { printf("Hello World!\n"); return; }
?
?
f)编译本地方法,生成可执行文件
?
cc -G -I/java/include -I/java/include/solaris HelloWorld.c -o libHelloWorld.so
cl -Ic:\java\include -Ic:\java\include\win32 -MD -LD HelloWorld.c -FeHelloWorld.dll
?
?
g)运行java代码,你会发现c写的本地方式实现被成功调用。
?
2)实现其他代码(比如c++)调用java代码(这正是我要解决的问题)
?
jni是双向的,java可以调用其他语言实现的native方法,同样其他语言同样可以调用java代码。如何做呢?由于java代码必须(一定)要运行在JVM中,因此其他语言要调用java代码,首先必须启动一个JVM实例,然后再通过JNI规范中给出的一些方法(具体参考上面给出的url),来实现对java代码的调用。
?
具体做法,以我实际解决的问题为例吧:
a)我要调用的java API是:
?
?
public class API { public static byte[] read(String ip, String path, int maxlen) throws XiheWorkerCommException, FileNotFoundException, IOException{ .......... } }
?
?b)设计我的c++类(某些设计公司保密的组件没有贴出来)
?
#ifndef LOG_VIEW_BRIDGE_H_ #define LOG_VIEW_BRIDGE_H_ #include "jni.h" class LogViewAdaptor { private: LogViewAdaptor(); LogViewAdaptor(const LogViewAdaptor&); LogViewAdaptor& operator= (const LogViewAdaptor&); ?
//启动虚拟机 static void BeginJVM(); //创建虚拟机实例 static int CreateJVM(JavaVM **jvm, JNIEnv **env, std::vector<std::string>& opts); //输出java异常信息 static void PrintJNIErrorStack(JNIEnv *env); //最重要。通过它可以调用JNI的各个方法 static JNIEnv* env; static JavaVM* jvm; public:
//核心方法
static void ReadLog(const std::string& ip,const std::string& path,unsigned int logLength,std::vector<char>&);
};
#endif
?
?
c)实现static int CreateJVM(JavaVM **jvm, JNIEnv **env, std::vector<std::string>& opts)
?
int LogViewAdaptor::CreateJVM(JavaVM **jvm, JNIEnv **env, vector<string>& opts) { JavaVMInitArgs vm_args; JavaVMOption* options = new JavaVMOption[opts.size()]; for (size_t i = 0; i < opts.size(); ++i) { options[i].optionString = (char*) opts[i].c_str(); LOG_INFO(sLogger,("JVM Opt",options[i].optionString)); } vm_args.version = JNI_VERSION_1_6; vm_args.options = options; vm_args.nOptions = opts.size(); vm_args.ignoreUnrecognized = JNI_TRUE; return JNI_CreateJavaVM(jvm, (void**) env, &vm_args); }
?
?创建虚拟机实例的核心方法是JNI_CreateJavaVM,第三个参数类型是JavaVMInitArgs ,提供虚拟机参数,有关java虚拟机参数的介绍网上随处可见。
只需注意的是你必须指定清楚,你创建的jvm实例到哪里去加载你要调用的java class,即必须设置-Djava.class.path这个参数。
还有一点需要注意的是vm_args.version = JNI_VERSION_1_6,在jni.h中一共有三个参数,分别是JNI_VERSION_1_2,JNI_VERSION_1_4和
JNI_VERSION_1_6,我用的jdk是1.6的,所以就设置为JNI_VERSION_1_6。
?
?
JNI_CreateJavaVM方法的返回值是整数,负数代表创建失败,非常杯具的是,除了这个整数返回值外,没有任何log信息(也许是我没有找到),导致定位问题非常困难。
-1:代表虚拟机初始化失败。
其他值还需要再调查。
我在调式这段代码时遇到的问题是:由于LD_LIBRARY_PATH中指向了错误的libjvm.so,导致创建虚拟机失败,总是返回-1. 正确的libjvm.so必须是${JAVA_HOME}/jre/lib/amd64/server/下的libjvm.so。 而且不要copy它到别的目录,否则还是报错。
?
d)创建好了JVM实例,就可以调用java的方法了,先把代码贴出来:
?
jclass clazz= env->FindClass(API_PATH); if(clazz == NULL) { PrintJNIErrorStack(env); APSARA_THROW(OdpsException,"not found api class:"+string(API_PATH)); } jmethodID mid = env->GetStaticMethodID(clazz,"read","(Ljava/lang/String;Ljava/lang/String;I)[B"); if(mid == NULL) { PrintJNIErrorStack(env); APSARA_THROW(OdpsException,"not found method:read"); } LOG_INFO(sLogger,("CallJavaAPI","Begin")); int curTime = time(NULL); jstring jip = env->NewStringUTF(ip.c_str()); jstring jpath = env->NewStringUTF(path.c_str()); jint jlogLength = logLength; jbyteArray obj = (jbyteArray)env->CallStaticObjectMethod(clazz,mid,jip,jpath,jlogLength); int endTime = time(NULL); LOG_INFO(sLogger,("CallJavaAPI","End")("Cost",boost::lexical_cast<string>(endTime-curTime)+"s")); if (env->ExceptionCheck()) { PrintJNIErrorStack(env); APSARA_THROW(OdpsException,"exception occured in java."); } char* data = (char*)env->GetByteArrayElements(obj, 0); size_t len = strlen(data); for(size_t i = 0; i<len; i++) { container.push_back(data[i]); }
?
?
以上代码还是很清晰的:
a)首先找到java的class
?
jclass clazz= env->FindClass(API_PATH);
在jni中,有一套类型对应java的类型,具体参见规范。这里java的class类型 对应jni的jclass
b)找到了类,就接着找对应的方法,由于java API的方法是static的,所以这里调用
jmethodID mid = env->GetStaticMethodID(clazz,"read","(Ljava/lang/String;Ljava/lang/String;I)[B");
来找静态方法。第一个参数是class,第二个是方法名,第三个是该方法的字节码信息。为了正确地搞清楚方法的字节码可以
使用javap -c 来查看。
(Ljava/lang/String;Ljava/lang/String;I)[B 表明该方法有三个参数,分别是String,String和int,返回类型是byte数组。
L代表类型是java的完整路径;I代表int,[:代表数组,B:代表byte。更详尽的信息请参看spec
L代表类型是java的完整路径;I代表int,[:代表数组,B:代表byte。更详尽的信息请参看spec
?
?
?
?
c)找到了方法,接下来就call呗。
jstring jip = env->NewStringUTF(ip.c_str());
? ? jstring jpath = env->NewStringUTF(path.c_str());
? ? jint jlogLength = logLength;
? ? jbyteArray obj = (jbyteArray)env->CallStaticObjectMethod(clazz,mid,jip,jpath,jlogLength);
?
这里要注意的是,首先要把c++的类型转换为jni的类型。
?
另外,我的java方法返回的是byte[],而jni并没有对应的CallStaticByteArrayMethod,找了半天发现CallStaticObjectMethod可用。
?
jni的方法命名很有规律,Call<Static><ReturnType>Method.
?
CallStaticObjectMethod方法返回的是jobject类型,而
jbyteArray 是其子类,所以这里强转。接下来就是如何把jni的jbyteArray转换为c++的vector<char>了。
char* data = (char*)env->GetByteArrayElements(obj, 0);
size_t len = strlen(data);
for(size_t i = 0; i<len; i++)
{
container.push_back(data[i]);
}
回过头看,使用jni也没啥难的,就是有些繁琐,需要好好看看jni的spec。以上是我的一次jni之旅,希望对大家有帮助。
?