Java中使用JNI构建环境

  1. 环境
  2. 准备JNI Java部分代码
  3. 构建C/C++项目
  4. 编写测试用的cpp文件
  5. 打包动态库
  6. java调用动态链接库
  7. 特殊bug

如果熟悉Android的朋友应该会有影响,Gradle会帮我们把Java和JNI进行捆绑,方便开发。而Java则会复杂一点,gradle的工作都需要我们来做。

环境

macos X86_64
java 1.8
模拟环境 springboot
包结构

project
+-build
+-src
| +-main
| +-cpp
| +-java
| +-resources
| +JniLibs
|
+-CMakeLists.txt
+-pom.xml

可以看到cpp中存放我们的c/c++代码,java中有我们的业务功能以及匹配cpp的jni接口文件,resources中的JniLibs存放的是生成的静态、动态库文件。
CMakeList.txt则是我们c/c++代码的管理包,这里面需要配置本地的编译环境及生产静态、动态库的库管理。
最后build是我们在打包动态链接库的时候生成的临时文件夹。

开发工具:IDEA、CLION
这里推荐Clion是方便开发c/c++代码。

准备JNI Java部分代码

首先是构建java开发环境及工程,这里就略过了。然后在java文件夹中,创建与C沟通的JNI接口文件:

public class PrintNative {

static {
System.loadLibrary("PrintNative");
}

public native void print();
}

static让其在加载类的时候同时加载本地的资源PrintNative文件。但是目前还没有,我们后续在建。

接下来,需要对这个文件进行编译,转为头文件:

cd project/src/main/java

javac com/xx/x/PrintNative.java
javah com.xx.x.PrintNative

完成后会生成对应的class文件和头文件,class文件可以删除掉,我们需要的是.h的头文件。我们需要将头文件移动到src/main/cpp/include文件夹下面。

构建C/C++项目

首先创建CMakeLists.txt文件,根据上面的路径创建,填入下面内容:

cmake_minimum_required(VERSION 3.4.1)

project(test)
add_definitions(-std=c++11)

set(java_home "xxxxx/jdk1.8.0_101.jdk/Contents/Home/")
set(jdk_home "xxxxxx/jdk1.8.0_101.jdk/Contents/Home/")

include_directories(${jdk_home}/include/
${jdk_home}/include/darwin/
src/main/cpp/include)

add_library(
PrintNative
SHARED
src/main/cpp/print-native.cpp)

target_link_libraries(PrintNative)

首先建立了cmake版本、项目的名称、c++版本。
然后创建了两个path路径,这里需要根据自己电脑来获取。
接下来需要指定include文件夹路径,这里我们需要三个,前两个是java中的头文件路径,最后一个是我们项目中的头文件路径。
最后就是建立当前库名称、动态/静态、cpp文件进行打包、链接,这里我们并没有其他的库添加到项目中,因此比较简单。

上面我们添加了一个cpp文件,因此我们就在这个目录下创建这个文件即可。

编写测试用的cpp文件

下面是我们创建的头文件

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class xx_xx_x_PrintNative */

#ifndef _Included_xx_xx_x_PrintNative
#define _Included_xx_xx_x_PrintNative
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: xx_xx_x_PrintNative
* Method: print
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT void JNICALL Java_xx_xx_x_PrintNative_print
(JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

需要注意,在使用clion打开这个文件的时候,如果cmake配置错误,头文件第二行的jni.h库引用失败导致编译报错。

接下来就需要将这个定义的jni接口函数,复制到cpp中,并配置好括号即可。

#include "Java_xx_xx_xx_PrintNative.h"

JNIEXPORT void JNICALL Java_xx_xx_x_PrintNative_print
(JNIEnv *, jobject) {

}

这里引用了我们加入的头文件,以及对应的Jni接口函数。
接下来就可以进行编写工作。

打包动态库

完成工作后,我们就需要开始打包。
首先在CMakeLists.txt文件的同级目录下,创建build文件夹。
然后在build文件的目录下运行下面的指令:

cmake -DCMAKE_BUILD_TYPE=Release ..

cmake --build .

此时就会在该目录下生成动态链接库了,注意如果报错,部分问题可能是代码错误,部分问题可能是CMakeLists.txt配置错误。
由于我的电脑是mac环境,因此生产的文件是libPrintNative.dylib。因此将它复制到resources中的JniLibs`即可。

java调用动态链接库

最后我们就需要在java调用了。
首先需要修改PrintNative中的代码:

public class PrintNative {

static {
URL url = PrintNative.class.getClassLoader().getResource("jniLibs/libPrintNative.dylib");
System.load(url.getPath());
}

这里看到是根据路径进行添加,此时是为了我们后续更好的兼容其他版本的架构,因为并不可能只支持x86(假如)也有arm等架构需要支持,此时就需要在jniLibs文件夹下增加一层文件夹,命名就是架构名称,当服务启动时,首先需要检查当前架构名称,然后对该字符串进行拼接得到正确的链接库地址,进行链接即可。
修改完成后,就可以编写一个main来进行测试啦。

特殊bug

需要注意在运行时可能会出现下面的异常

Caused by: java.lang.UnsatisfiedLinkError: /xxx/target/classes/lib/xxx.so: dlopen(/xxx/target/classes/lib/xxx.so, 1): no suitable image found.  Did find:
/xxx/target/classes/lib/xxx.so: unknown file type, first eight bytes: 0xEF 0xBF 0xBD 0xEF 0xBF 0xBD 0xEF 0xBF
/xxx/target/classes/lib/xxx.so: unknown file type, first eight bytes: 0xEF 0xBF 0xBD 0xEF 0xBF 0xBD 0xEF 0xBF

这需要注意,使用idea编译代码后,由于将二进制文件从resource拷贝到输出路径可能会导致文件格式被破坏。
因此需要在pom下提示:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.1</version>
<configuration>
<nonFilteredFileExtensions>
<nonFilteredFileExtension>so</nonFilteredFileExtension>
<nonFilteredFileExtension>dylib</nonFilteredFileExtension>
</nonFilteredFileExtensions>
</configuration>
</plugin>
</plugins>
</build>

重新编译即可。