Java知识

JNI 入门到工程化

2018-06-23  本文已影响43人  Cliven_q

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;
}

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参数意义:

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包

上一篇下一篇

猜你喜欢

热点阅读