随着近几年分布式程序的发展,Java在该领域扮演着越来越重要的角色,特别是Hadoop生态圈的兴起,更是让Java成为很多互联网公司主要的开发语言。但是对于Java而言,虽然我们在构建高层应用时优势得天独厚,但是在底层资源的控制方面却远不及C/C++,特别是Linux在服务器市场处于决定性的领先地位。为了使Java分布式平台能够更好的结合底层资源,很多互联网公司都采取了“高+低”的方式,即“高层Java应用+底层Linux接口调用”的方式,而JNI则是这两者之间的通道。腾讯盖娅资源调度平台就采用了Yarn+(JNI)+CGroup的方式。
这里以Ubuntu为例,介绍一下Linux下开发JNI程序的步骤:
一、正常编写自己的Java业务,并将需要调用底层资源的方法声明为native本地方法。
package org.study.jni; public class TestJni { static { //要加载的动态链接库,由C或C++代码编译而成,JVM会自动查找指定Library路径下的 //动态链接库并加载。这里编译后的链接库名字为libTestJni.so。 //(Linux下的动态链接库一般用lib作为前缀,链接库的名字是内嵌到.so文件的, //物理文件的前缀不会影响这里的loadLibrary) System.loadLibrary("TestJni"); } //声明要用到的本地方法原型,native方法可以像abstract方法一样只是一个声明, //native方法的具体实现是在C/C++中实现的 public native void sayHello(String name); //这里增加一个操作JavaBean的方法,来增强复杂度 public native void changePersonAge(JniPerson person); public static void main(String[] args) { TestJni jni = new TestJni(); jni.sayHello("Join"); JniPerson person = new JniPerson(); person.setAge(10); jni.changePersonAge(person); System.out.println("After: " + person.getAge()); } }
这里还有一个辅助的JavaBean对象:JniPerson.java
package org.study.jni; import java.io.Serializable; public class JniPerson implements Serializable { private static final long serialVersionUID = 1554409252901146643L; private String name = null; private int age = 20; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
二、通过javah命令,从编译后的字节码文件中提取本地方法信息,并自动生成头文件。
在Java项目的字节码文件夹下(eclipse的话一般是项目下的bin目录),执行“javah org.study.jni.TestJni” Shell命令,会自动生成一个org_study_jni_TestJni.h,内容如下:
三、编写C/C++的业务实现,我们可以借助包含CDT的Eclipse来开发。这里我用Eclipse新建一个C类型的项目(如果要采用C++的话也一样),将生成的org_study_jni_TestJni.h头文件放到项目下,并建立一个代码实现文件,这里起名为org_study_jni_TestJni.c。/* DO NOT EDIT THIS FILE - it is machine generated */ #include
/* Header for class org_study_jni_TestJni */ #ifndef _Included_org_study_jni_TestJni #define _Included_org_study_jni_TestJni #ifdef __cplusplus extern "C" { #endif /* * Class: org_study_jni_TestJni * Method: sayHello * Signature: (Ljava/lang/String;)V */ JNIEXPORT void JNICALL Java_org_study_jni_TestJni_sayHello (JNIEnv *, jobject, jstring); /* * Class: org_study_jni_TestJni * Method: changePersonAge * Signature: (Lorg/study/jni/JniPerson;)V */ JNIEXPORT void JNICALL Java_org_study_jni_TestJni_changePersonAge (JNIEnv *, jobject, jobject); #ifdef __cplusplus } #endif #endif
Eclipse项目结构:
org_study_jni_TestJni.c实现代码如下:
#include
#include #include #include "../header/org_study_jni_TestJni.h" JNIEXPORT void JNICALL Java_org_study_jni_TestJni_sayHello (JNIEnv *env, jobject obj, jstring name) { // 从 instring 字符串取得指向字符串 UTF 编码的指针,注意C语言必须“(*env)->”,C++可以“env->” const jbyte *pname = (const jbyte *) (*env)->GetStringUTFChars(env, name, JNI_FALSE); printf("Hello, %s\n", pname); // 通知虚拟机本地代码不再需要通过 str 访问 Java 字符串。 (*env)->ReleaseStringUTFChars(env, name, (const char *) pname); } JNIEXPORT void JNICALL Java_org_study_jni_TestJni_changePersonAge (JNIEnv *env, jobject obj, jobject person) { //获取person对象的类型信息,在native方法中操作Java对象的方式有点类似于反射 jclass person_clazz = (*env)->GetObjectClass(env, person); //这里不通过调用setAge()方法获取 jfieldID age_fid = (*env)->GetFieldID(env, person_clazz, "age", "I"); printf("GetIntFiled: %d \n", (*env)->GetIntField(env, person, age_fid)); (*env)->SetIntField(env, person, age_fid, 123); }
四、将C/C++代码编译为动态链接库。如果使用gcc命令行编译,可以到代码目录下执行“gcc -fPIC -shared -I /usr/local/jdk1_7/include/ -I /usr/local/jdk1_7/include/linux/ -o libTestJni.so org_study_jni_TestJni.c”命令来生成libTestJni.so。如果使用Eclipse编译,则有几个选项需要设定一下:
五、终于到了激动人心的时刻,现在可以运行我们的Java程序。将动态链接库libTestJni.so文件复制到Java项目的bin目录下,运行“java -Djava.library.path='.' org.study.jni.TestJni”,就出现如下运行结果:1、将JDK的JNI头文件引用到项目路径下。对于JDK1.7,JNI头文件放在$JAVA_HOME/include和$JAVA_HOME/include/linux两个文件夹下。
2、将项目编译选项设定为动态链接库,而非可执行文件。
![]()
3、为gcc编译器增加-fPIC编译选项,这里建的是C项目,所以更改C编译器的设置即可,如果编译时出错,可以将其它几个编译器的Miscellaneous选项也加上“-fPIC”。
4、按下Ctrl+B来进行编译,如果编译成功可以在项目的Debug或Release目录下生成libTestJni.so文件。
至此,我们的JNI程序就调用成功了,有了JNI的连通,Java就可以借助C/C++的帮助实现对底层资源的获取和干预,特别是对内核的操作。Hello, Join GetIntFiled: 10 After: 123