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包)。
newProject.jpg在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>
网友评论