遇到的需求很简单,我们有一个性能很好的分词器,用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);
}
在实现中,jni.h这个头文件是必须要包含的,将来在编译的时候,也要在系统搜索路径上。“JNIEnv *env, jclass obj, jstring line”这些奇奇怪怪的东西到底是什么意思,怎么用,请参考《 在 Linux 平台下使用 JNI 》。那两天正在学习这方面的东西,就顺便转载了。这段代码功能也很简单,就是再输入字符串的基础上,加上”--copy that“的字样,并且返回。
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中的函数。