美文网首页Java知识
JNI 入门到工程化

JNI 入门到工程化

作者: Cliven_q | 来源:发表于2018-06-23 11:36 被阅读43次

    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.javaprint方法。

    按照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,需要下面准备

    1. 工程化TestJni,并封装jar
    2. 重新制作jni的.so库文件

    3.1.1 工程化TestJni

    为了方便起见,下面直接使用IDEA进行先关操作。

    创建Maven项目(采用了Maven是为了更加轻易的封装jar包)。

    newProject.jpg

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

    createPackage.jpg

    创建我们的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

    testJni.jpg

    目前为止,已经将一个项目简单的建立起来了,现在需要把这个封装成jar供其他程序调用

    3.1.2 封装

    如果采用的是IEAD,那么从侧栏目找到Maven Projects

    mavenProject.jpg

    直接运行Lifecycleinstall

    install.jpg

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

    gen.jpg

    上述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包

    1. helloworld-1.0.0.jar 放置到resources/lib目录中
    2. build > resources >下面加入下面内容
    <resource>
        <directory>${basedir}/src/main/resources</directory>
        <targetPath>BOOT-INF/lib/</targetPath>
        <includes>
            <include>**/*.jar</include>
        </includes>
    </resource>
    
    1. 增加依赖项 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>
    

    参考

    使用JNI进行Java与C/C++语言混合编程(1)--在Java中调用C/C++本地库

    Linux下JNI的使用

    在 Linux 平台下使用 JNI

    gcc编译参数-fPIC的一些问题

    JNI 传递参数和返回值

    SpringBoot使用本地jar包

    相关文章

      网友评论

        本文标题:JNI 入门到工程化

        本文链接:https://www.haomeiwen.com/subject/qjuryftx.html