MyException - 我的异常网
当前位置:我的异常网» C++ » c++ 经过JNI技术调用java代码使用总结

c++ 经过JNI技术调用java代码使用总结

www.MyException.Cn  网友分享于:2013-11-10  浏览:101次
c++ 通过JNI技术调用java代码使用总结

 

由于工作需要,要写一段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)CreateJVM方法被BeginJVM方法调用,BeginJVM方法如下:

void LogViewAdaptor::BeginJVM()
{
    if(jvm == NULL)
    {
        ScopedLock lock(mJvmLock);
        if(jvm == NULL)
        {
            vector<string> opts;
            opts.push_back("-Xmx128m");
            opts.push_back("-Xmn64m");
        ScopedLock lock(mJvmLock);
        if(jvm == NULL)
        {
            vector<string> opts;
            opts.push_back("-Xmx128m");
            opts.push_back("-Xmn64m");
            opts.push_back("-Xss128k");
            opts.push_back("-Djava.compiler=NONE");
            opts.push_back("-Djava.class.path=.:./xihe_api/xapi4odps.jar:/xihe_api/");
            opts.push_back("-verbose:jni");
            int ret = CreateJVM(&jvm,&env,opts);
            if(ret < 0)
            {
                LOG_INFO(sLogger,("CreateJVM","Failure")("Ret",ret));
                APSARA_THROW(OdpsException,"JNI: Could not create the Java virtual machine.");
            }
            else
            {
                LOG_INFO(sLogger,("CreateJVM", "Success"));
            }
        }
        else
        {
            LOG_INFO(sLogger,("CreateJVM","jvm Not NULL"));
            jvm->AttachCurrentThread((void **) &env, NULL);
        }
    }
    else
    {
        LOG_INFO(sLogger,("CreateJVM","jvm Not NULL"));
        jvm->AttachCurrentThread((void **) &env, NULL);
    }
}

 这里需要特别注意的是 jvm->AttachCurrentThread((void **) &env, NULL);方法的调用。

首先搞清楚AttachCurrentThread方法的作用,以下是jni规范的引用:

JNI规范写道
jint AttachCurrentThread(JavaVM *vm, JNIEnv **p_env, void *thr_args);

Attaches the current thread to a Java VM. Returns a JNI interface pointer in the JNIEnv argument.

Trying to attach a thread that is already attached is a no-op.

A native thread cannot be attached simultaneously to two Java VMs.

When a thread is attached to the VM, the context class loader is the bootstrap loader.

 我们的应用都是多线程的,用户的两次请求和可能是不同的线程,因此必须调用此方法把当前线程attach到vm中,才可以让jvm work。这一点非常重要。

 

 

 

e)创建好了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

 

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规范指出,一个进程内只能开启一个JVM实例,否则就会报错。因此你必须确保JavaVM* jvm 在进程内绝对唯一。做法有singleton,static等。我采取static做法,但是我在实际测试中却发现在一个进程内重复调用两次ReadLog方法(具体做法是在UT只中运行两个case,每个case都会调用ReadLog方法,而UT的两个case是在一个进程内执行的)时,后面的一次会失败,而且非常严重,直接这样了:“A fatal error has been detected by the Java Runtime Environment”。为啥呢?我个人的分析(可能不对):我们知道一个JVM实例是在他运行的main方法退出时,自动也就结束了。第一次调用ReadLog方法结束后,当前进程启动的JVM应该也结束了,因为它已经没啥事可干了。那紧接着第二次调用ReadLog时,BeginJVM方法判断jvm这个指针 != NULL,于是它不再创建JVM了,但是这个jvm实例实际已经不可用了。以上是我的分析,有点武断,欢迎大家讨论,解惑。

 

以上的分析是完全错误的!

jvm何时退出?jni启动的jvm实例是作为子进程存在的,只要主进程仍然存在,那么jvm子进程就存在,不会退出,因此我们才不需要多次创建jvm实例。

小结

回过头看,使用jni也没啥难的,就是有些繁琐,需要好好看看jni的spec。以上是我的第一次jni之旅,希望对大家有帮助。


 

文章评论

Web开发者需具备的8个好习惯
Web开发者需具备的8个好习惯
代码女神横空出世
代码女神横空出世
每天工作4小时的程序员
每天工作4小时的程序员
为什么程序员都是夜猫子
为什么程序员都是夜猫子
Java程序员必看电影
Java程序员必看电影
如何区分一个程序员是“老手“还是“新手“?
如何区分一个程序员是“老手“还是“新手“?
为啥Android手机总会越用越慢?
为啥Android手机总会越用越慢?
漫画:程序员的工作
漫画:程序员的工作
什么才是优秀的用户界面设计
什么才是优秀的用户界面设计
不懂技术不要对懂技术的人说这很容易实现
不懂技术不要对懂技术的人说这很容易实现
程序员都该阅读的书
程序员都该阅读的书
我是如何打败拖延症的
我是如何打败拖延症的
初级 vs 高级开发者 哪个性价比更高?
初级 vs 高级开发者 哪个性价比更高?
老美怎么看待阿里赴美上市
老美怎么看待阿里赴美上市
Web开发人员为什么越来越懒了?
Web开发人员为什么越来越懒了?
 程序员的样子
程序员的样子
程序员和编码员之间的区别
程序员和编码员之间的区别
程序员的一天:一寸光阴一寸金
程序员的一天:一寸光阴一寸金
10个帮程序员减压放松的网站
10个帮程序员减压放松的网站
旅行,写作,编程
旅行,写作,编程
鲜为人知的编程真相
鲜为人知的编程真相
一个程序员的时间管理
一个程序员的时间管理
老程序员的下场
老程序员的下场
程序员周末都喜欢做什么?
程序员周末都喜欢做什么?
10个调试和排错的小建议
10个调试和排错的小建议
5款最佳正则表达式编辑调试器
5款最佳正则表达式编辑调试器
我的丈夫是个程序员
我的丈夫是个程序员
当下全球最炙手可热的八位少年创业者
当下全球最炙手可热的八位少年创业者
我跳槽是因为他们的显示器更大
我跳槽是因为他们的显示器更大
“懒”出效率是程序员的美德
“懒”出效率是程序员的美德
中美印日四国程序员比较
中美印日四国程序员比较
写给自己也写给你 自己到底该何去何从
写给自己也写给你 自己到底该何去何从
程序员眼里IE浏览器是什么样的
程序员眼里IE浏览器是什么样的
如何成为一名黑客
如何成为一名黑客
看13位CEO、创始人和高管如何提高工作效率
看13位CEO、创始人和高管如何提高工作效率
团队中“技术大拿”并非越多越好
团队中“技术大拿”并非越多越好
亲爱的项目经理,我恨你
亲爱的项目经理,我恨你
那些争议最大的编程观点
那些争议最大的编程观点
60个开发者不容错过的免费资源库
60个开发者不容错过的免费资源库
聊聊HTTPS和SSL/TLS协议
聊聊HTTPS和SSL/TLS协议
程序猿的崛起——Growth Hacker
程序猿的崛起——Growth Hacker
程序员的鄙视链
程序员的鄙视链
要嫁就嫁程序猿—钱多话少死的早
要嫁就嫁程序猿—钱多话少死的早
程序员最害怕的5件事 你中招了吗?
程序员最害怕的5件事 你中招了吗?
Java 与 .NET 的平台发展之争
Java 与 .NET 的平台发展之争
软件开发程序错误异常ExceptionCopyright © 2009-2015 MyException 版权所有