遇到的需求很簡單,我們有一個性能很好的分詞器,用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 》http://www.linuxidc.com/Linux/2012-12/75536.htm。那兩天正在學習這方面的東西,就順便轉載了。這段代碼功能也很簡單,就是再輸入字符串的基礎上,加上”--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中的函數。