2018-6-21 JNI
Cliven
2018-6-21
[TOC]
1. Hello World
在Linux下使用yum安装openjdk
yum install -y java-1.8.0-openjdk-devel gcc
安装完成后就可以使用javac命令编译java文件。
public class TestJni
{
      //声明原生函数:参数为String类型
      public native void print(String content);
      //加载本地库代码     
      static
      {
           System.loadLibrary("TestJni");
      }
}
TestJni就是即将编写的库名称。
编译
javac ./*.java -d . 
在TestJni.class同级的目录中运行下面命令生成对应的.h文件
javah -jni TestJni
运行结束后在当前目录中可以看到生成的TestJni.h文件
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class TestJni */
#ifndef _Included_TestJni
#define _Included_TestJni
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     TestJni
 * Method:    print
 * Signature: (Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_TestJni_print
  (JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif
Java_TestJni_print 就对应着TestJni.java 的print方法。
按照c语言的开发思路,对应的需要创建一个TestJni.c文件,实现.h文件中函数
#include <jni.h>
#include <stdio.h>
#include <TestJni.h>
JNIEXPORT void JNICALL
      Java_TestJni_print(JNIEnv *env,jobject obj, jstring content){
    // 从 instring 字符串取得指向字符串 UTF 编码的指针
    // 注意C语言必须(*env)->
    const jbyte *str = (const jbyte *)(*env)->GetStringUTFChars(env,content, JNI_FALSE);
    printf("Hello --> %s\n",str);
    // 通知虚拟机本地代码不再需要通过 str 访问 Java 字符串。
    (*env)->ReleaseStringUTFChars(env, content, (const char *)str);
    return;
}
- 
JNIEnv使得我们可以使用Java的方法 - 
jobject指向在此Java代码中实例化的Java对象LocalFunction的一个句柄,相当于this指针。 - 
jstring参数类型对应java中的String。每一个Java里的类型这里有对应的与之匹配。 
GetStringUTFChars这个方法是用来在Java和C之间转换字符串的, 因为Java本身都使用了UTF8(可变长)字符, 而C语言本身都是单字节的字符;
ReleaseStringUTFChars用于回收内存,在C语言中, 这些对象必须手动回收, 否则可能造成内存泄漏
编译连接生成动态链接库.so文件,得先安装gcc(yum install -y gcc)
cc -I/usr/lib/jvm/java/include/linux \
   -I/usr/lib/jvm/java/include \
   -I/home/jni \
   -fPIC -shared \
   -o libTestJni.so TestJni.c
GCC命令
-I指定需连接的库名,与库名之间不需要空格直接-Ixxx,在编译的时候回到-I指定的位置寻找头文件
-fPIC作用于编译阶段,告诉编译器产生与位置无关代码(Position-Independent Code),则产生的代码中,没有绝对地址,全部使用相对地址,故而代码可以被加载器加载到内存的任意位置,都可以正确的执行。共享库被加载时,在内存的位置不是固定的。
这里/usr/lib/jvm/java/include目录是jdk目录中的,得根据实际安装的jdk开发环境来决定。
libTestJni.so是动态链接库名称,一个库的必须要是下面格式
lib + 库名 + .so
链接的时候只需要提供库名(TestJni)就可以了。
-I/home/qgy/jni使我们刚才生成的.h文件的位置
完成上面操作之后,需要把.so文件放到java系统默认的库寻找目录才可以被jni正常调用。
如何查看该目录的位置?
可以写一个简单的程序来查询
public class Main{
    public static void main(String[] args){
        String[] split = System.getProperty("java.library.path").split(":");  
        for (String string : split) {  
            System.out.println(string);  
        }  
    }
}
编译运行上面程序代码就可以,输出java.library.path的位置,下面列出的每一个位置都是放置我们刚才生成的.os文件
/usr/java/packages/lib/amd64
/usr/lib64
/lib64
/lib
/usr/lib
将刚才生成的libTestJni.so放置到/lib目录下,这样编写的库就可以在java被加载,然后调用到
主程序
import java.util.*;
public class HelloWorld{
    public static void main(String[] args){
        new TestJni().print("Hello,Wolrd!");
    }
}
编译运行就可以看到结果。
java HelloWorld
如果
.os文件位置错误,可能会抛出异常Exception in thread "main" java.lang.UnsatisfiedLinkError: no TestJni in java.library.path,运行提供的查询程序,获取到位置之后,正确放置就可以解决问题。
2. JNI 详解
2.1 Java & 类型对应
| Java | C/C++ | 字节数 | 
|---|---|---|
| boolean | jboolean | 1 | 
| byte | jbyte | 1 | 
| char | jchar | 2 | 
| short | jshort | 2 | 
| int | jint | 4 | 
| long | jlong | 8 | 
| float | jfloat | 4 | 
| double | jdouble | 8 | 
数组类型
| Java | C/C++ | 
|---|---|
| boolean[ ] | JbooleanArray | 
| byte[ ] | JbyteArray | 
| char[ ] | JcharArray | 
| short[ ] | JshortArray | 
| int[ ] | JintArray | 
| long[ ] | JlongArray | 
| float[ ] | JfloatArray | 
| double[ ] | JdoubleArray | 
2.2 S0生成
gcc SOURCE_FILES -fPIC -shared -o TARGET
SOURCE_FILES可以是.c文件,也可以是经过-c编译出来的.o文件
2.3 参数传递/返还
2.3.1 参数传入/出
    public native void giveArray(int[] array);
    int[] array = {9,100,10,37,5,10};
        //排序
    t.giveArray(array);
    for (int i : array) {
        System.out.println(i);
    }
c实现代码
int compare(int *a,int *b){
    return (*a) - (*b);
}
//传入
JNIEXPORT void JNICALL Java_com_study_jni_JniTest_giveArray
(JNIEnv *env, jobject jobj, jintArray arr){
    //jintArray -> jint指针 -> c int 数组
    jint *elems = (*env)->GetIntArrayElements(env, arr, NULL);
    //printf("%#x,%#x\n", &elems, &arr);
    //数组的长度
    int len = (*env)->GetArrayLength(env, arr);
    //排序
    qsort(elems, len, sizeof(jint), compare);   
    (*env)->ReleaseIntArrayElements(env, arr, elems, JNI_COMMIT);
}
ReleaseXXXXArrayElements 方法中mode参数意义:
- 
0,Java数组进行更新,并且释放C/C++数组。 - 
JNI_ABORT,Java数组不进行更新,但是释放C/C++数组。 - 
JNI_COMMIT,Java数组进行更新,不释放C/C++数组(函数执行完,数组还是会释放)。 
2.3.2 参数返还
  int[] array2 = t.getArray(10);
    System.out.println("------------");
    for (int i : array2) {
        System.out.println(i);
    }
c实现
//返回数组
JNIEXPORT jintArray JNICALL Java_com_study_jni_JniTest_getArray(JNIEnv *env, jobject jobj, jint len){
    //创建一个指定大小的数组
    jintArray jint_arr = (*env)->NewIntArray(env, len);
    jint *elems = (*env)->GetIntArrayElements(env, jint_arr, NULL); 
    int i = 0;
    for (; i < len; i++){
        elems[i] = i;
    }
    //同步
    (*env)->ReleaseIntArrayElements(env, jint_arr, elems, 0);   
    return jint_arr;
}
3. 工程化
3.1 API封装
为了在各种各样的地方使用我们封装好的jni,需要下面准备
- 工程化TestJni,并封装jar
 - 重新制作jni的
.so库文件 
3.1.1 工程化TestJni
为了方便起见,下面直接使用IDEA进行先关操作。
创建Maven项目(采用了Maven是为了更加轻易的封装jar包)。

在java目录的下面创建刚才创建的包名com.demo

创建我们的jni类,复制TestJni.java内容如下
package com.demo;
/**
 * create by Cliven on 2018-06-23 10:34
 */
public class TestJni {
    /**
     * 声明原生函数,jni接口
     *
     * @param content 输入参数
     */
    public native static void print(String content);
    //加载本地库代码
    static {
        System.loadLibrary("TestJni");
    }
}
就是比刚才的TestJni多了package行

目前为止,已经将一个项目简单的建立起来了,现在需要把这个封装成jar供其他程序调用
3.1.2 封装
如果采用的是IEAD,那么从侧栏目找到Maven Projects

直接运行Lifecycle的install

运行完成后可以在target目录下找到刚才生成的jar包

上述install 命令就是使用maven的install命令
3.2 库文件重做
经过上面的封装TestJni被加上了包,所以需要重新生成so文件
将带有包名的TestJni.java文件复制到Linux系统中,然后执行命令编译
javac ./TestJni.java -d . 
编译完成后会在目录下生成一个由包名称组成的目录com/demo,编译好的文件就在这里面
[root@localhost jni]# ll
总用量 4
-rw-r--r--. 1 root root 341 6月  23 10:54 TestJni.java
[root@localhost jni]# javac TestJni.java -d .
[root@localhost jni]# ll
总用量 4
drwxr-xr-x. 3 root root  18 6月  23 10:54 com
-rw-r--r--. 1 root root 341 6月  23 10:54 TestJni.java
[root@localhost jni]# 
现在使用TestJni.java所在目录下运行javah生成.h接口文件,这里必须使用完整的包名和类名才可运行否则会提示错误: 找不到 'TestJni' 的类文件。
javah -jni com.demo.TestJni
运行后会在目录中生成com_demo_TestJni.h,找上面的思路,实现这个.h文件,内容与上面的TestJni.c相同
#include <jni.h>
#include <stdio.h>
#include "com_demo_TestJni.h"
/*
 * Class:     com_demo_TestJni
 * Method:    print
 * Signature: (Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_com_demo_TestJni_print
(JNIEnv *env,jobject obj, jstring content){
    const jbyte *str = (const jbyte *)(*env)->GetStringUTFChars(env,content, JNI_FALSE);
    printf("Hello --> %s\n",str);
    (*env)->ReleaseStringUTFChars(env, content, (const char *)str);
    return;
}
编译,然后生成.so库文件
cc -I/usr/lib/jvm/java/include/linux \
   -I/usr/lib/jvm/java/include \
   -I./ \
   -fPIC -shared \
   -o libTestJni.so com_demo_TestJni.c
保存生成的.so文件,以后这个文件将和jar配套使用。
测试
复制.so文件到/lib中
cp libTestJni.so /lib
编写测试主函数
import com.demo.TestJni;
public class Main{
    public static void main(String[] args){
        TestJni.print("Guest");
    }
}
编译运行,测试
javac Main.java -d .
java Main
[root@localhost jni]# vim Main.java
[root@localhost jni]# javac Main.java -d .
[root@localhost jni]# java Main
Hello --> Guest
[root@localhost jni]# ll
总用量 28
drwxr-xr-x. 3 root root   18 6月  23 10:54 com
-rw-r--r--. 1 root root  411 6月  23 11:12 com_demo_TestJni.c
-rw-r--r--. 1 root root  433 6月  23 11:11 com_demo_TestJni.h
-rwxr-xr-x. 1 root root 8040 6月  23 11:12 libTestJni.so
-rw-r--r--. 1 root root  337 6月  23 11:14 Main.class
-rw-r--r--. 1 root root  129 6月  23 11:13 Main.java
-rw-r--r--. 1 root root  348 6月  23 11:11 TestJni.java
到此已经测试jni的.so文件是可用的。
3.3 其他 Springboot 引入jar包
如何在springboot项目中使用上面的jar包
- 将
helloworld-1.0.0.jar放置到resources/lib目录中 - 在
build > resources >下面加入下面内容 
<resource>
    <directory>${basedir}/src/main/resources</directory>
    <targetPath>BOOT-INF/lib/</targetPath>
    <includes>
        <include>**/*.jar</include>
    </includes>
</resource>
- 增加依赖项 
dependencies > 
<dependency>
    <groupId>com.demo</groupId>
    <artifactId>hellowrd</artifactId>
    <version>1.0.0</version>
    <scope>system</scope>
    <systemPath>${project.basedir}/src/main/resources/lib/helloworld-1.0.0.jar</systemPath>
</dependency>