遇到的需求很简单,我们有一个性能很好的分词器,用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
#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”这些奇奇怪怪的东西到底是什么意思,怎么用,请参考我前两天转载的博文《【java学习】jni之JNIEnv* 》。那两天正在学习这方面的东西,就顺便转载了。这段代码功能也很简单,就是再输入字符串的基础上,加上”--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 libFakeSegmentForJni.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“表示输出文件名。

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


6. 本地java程序调用libFakeSegmentForJni.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中的函数。


Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐