Jni在Hadoop上的使用

遇到的需求很简单,我们有一个性能很好的分词器,用c++实现的,现在想在java写的Hadoop的程序中使用它,咋办?

如果只是使用hadoop,用c++ pipes实现hadoop程序,再调用c++实现的分词器(源代码调用或者动态库调用)就很简单,不存在上面的问题。不过,由于Legacy原因(其实就是种种原因),不能放弃java版本的hadoop程序,才会有以上问题。

上网上搜了一下,Java调用c++用的是JNI(java native interface)技术,只是JNI怎么放到hadoop中?而且分词器要读取资源文件(词表),这个文件在hadoop中的路径设定有什么规矩?我就不知道了。

尝试分三阶段进行:

阶段一:在linux跑通一个单机版的JNI程序,即用java调用c++。

阶段二:将上面的程序放到hadoop上跑通。

阶段三:让c++编出来的动态库(so文件)load资源,并在hadoop上跑通。

现在进行阶段一的工作。

1. 写一个Java类,用来包装c++代码的接口。这里面我只是写了示意性代码,毕竟是要尝试么,如下:

package FakeSegmentForJni ; 
 
public class FakeSegmentForJni { 
    public static native String SegmentALine (String line); 
 
    static 
    { 
        System.loadLibrary("FakeSegmentForJni"); 
    } 

这里面声明了静态函数接口,并用了”native“关键字,表示是native函数(非java的、本地函数)。在”static“语句块儿中,用LoadLibrary调用(即将生成的)c++动态库。

2. 用javac命令编译FakeSegmentForJni类,生成.class文件,命令如下:

javac -d ./bin ./src/*.java

3. 在FakeSegmentForJni.class的基础上,用javah命令生成c++函数的头文件,命令如下:

javah -jni -classpath . FakeSegmentForJni.FakeSegmentForJni

其中classpath表示.class文件所在目录,“.”表示当前目录;后面的参数,第一个“FakeSegmentForJni”表示package名称,第二个“FakeSegmentForJni”表示class名称。敲完命令后,就能在当前目录下发现c++函数的头文件FakeSegmentForJni_FakeSegmentForJni.h。打开看一下,主要将java类中的static函数转成了c++接口,内容如下:

/* DO NOT EDIT THIS FILE - it is machine generated */ 
#include <jni.h> 
/* Header for class FakeSegmentForJni_FakeSegmentForJni */ 
 
#ifndef _Included_FakeSegmentForJni_FakeSegmentForJni 
#define _Included_FakeSegmentForJni_FakeSegmentForJni 
#ifdef __cplusplus 
extern "C" { 
#endif 
/*
 * Class:    FakeSegmentForJni_FakeSegmentForJni
 * Method:    SegmentALine
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */ 
JNIEXPORT jstring JNICALL Java_FakeSegmentForJni_FakeSegmentForJni_SegmentALine 
  (JNIEnv *, jclass, jstring); 
 
#ifdef __cplusplus 

#endif 

文件上第一句“DO NOT EDIT THIS FILE ......”表示这是个自动生成的文件。其时,不必用javah命令来生成这个文件,手写也没问题。不过毕竟自动生成方便,尤其是在接口函数比较多的情况。

4. 费了这么半天事情,就是生成了一个c++头文件,正经事还没干呢。什么是正经事?既然非用c++不可,正经事就是用c++对所需功能的实现。其时用c++的理由是尽量利用现有的、成熟的代码,所以这一步,一般不是功能性开发,而是写个wrapper包装现有的代码——如果是纯功能性开发,那直接用java的了,费这么多事干嘛?!

废话不说了,wrapper的代码如下:

#include <jni.h> 
#include <stdio.h> 
#include <string.h> 
#include "FakeSegmentForJni_FakeSegmentForJni.h" 
 
/* * Class:    FakeSegmentForJni_FakeSegmentForJni
 * * Method:    SegmentALine
 * * Signature: (Ljava/lang/String;)Ljava/lang/String;
 * */ 
 
JNIEXPORT jstring JNICALL Java_FakeSegmentForJni_FakeSegmentForJni_SegmentALine 
  (JNIEnv *env, jclass obj, jstring line) 

    char buf[128]; 
    const char *str = NULL; 
    str = env->GetStringUTFChars(line, false); 
    if (str == NULL) 
        return NULL; 
    strcpy (buf, str); 
    strcat (buf, "--copy that\n"); 
    env->ReleaseStringUTFChars(line, str); 
    return env->NewStringUTF(buf); 

5. 在本地环境下编译出c++动态库。为啥要强调“在本地环境”?字面上的意思就是,你在windows下用JNI,就到windows下编译FakeSegmentForJni_FakeSegmentForJni.cpp文件,生成dll;在linux下,就到linux下(32位还是64位自己搞清楚)编译FakeSegmentForJni_FakeSegmentForJni.cpp文件,生成.so文件。为啥非要这样?这就涉及到动态库的加载过程,每个系统都不一样。

编译命令为:

g++ -I/System/Library/Frameworks/JavaVM.framework/Versions/A/Headers FakeSegmentForJni_FakeSegmentForJni.cpp -fPIC -shared -o FakeSegmentForJni.so

命令有点长,不过意思很容易。“-I”表示要包含的头文件。正常来讲,系统路径都已经为g++设置好了。不过jni.h是java的头文件,不是c++的,g++找不到,只好在编译的时候告诉编译器。我这里用的是mac os 10.8.2的hadoop伪分布式,所以头文件是在framework里的,路径“/System/Library/Frameworks/JavaVM.framework/Versions/A/Headers”看起来有点怪怪的。”-shared“表示输出的是动态库(共享库,有别于静态库的.a文件);”-o“表示输出文件名。

顺利的话,就能生成FakeSegmentForJni.so文件。

6. 本地java程序调用FakeSegmentForJni.so,代码很简单,如下:

package FakeSegmentForJni; 
 
import java.io.IOException; 
import java.net.URI; 
import java.net.URL; 
 
/**
 * This class is for verifying the jni technology. 
 * It call the function defined in FakeSegmentForJni.java
 * 
 */ 
 
public class TestFakeSegmentForJni { 
     
    public static void main(String[] args) throws Exception { 
 
        System.out.println ("In this project, we test jni!\n"); 
         
        // test jni on linux local 
        String s = FakeSegmentForJni.SegmentALine("now we test FakeSegmentForJni"); 
        System.out.print(s); 
         
    } // main 
 
} // TestFakeSegmentForJni 

测试代码也很简单,就是输入给FakeSegmentForJni.SegmentALine一个字符串,并且打印它的返回结果。

在linux上面打了一个jar包,输入如下命令运行上述代码:

java -Djava.library.path='/xxx/TestJni/' -jar /xxx/TestFakeSegmentForJni.jar FakeSegmentForJni.TestFakeSegmentForJni

(用-Djava.library.path指明FakeSegmentForJni_FakeSegmentForJni.so文件所在的路径,否则jvm找不到;后面FakeSegmentForJni.TestFakeSegmentForJni是main函数所在的路径)

程序运行结果是在屏幕上输出

now we test FakeSegmentForJni--copy that

这样的字样,表示成功调用FakeSegmentForJni_FakeSegmentForJni.so中的函数。

相关推荐