java调用c++动态库之jni调用
一、背景
存在java程序调用c++程序的场景,对常见的jni技术进行使用方法的总结。
二、技术
Java Native Interface(JNI)是一种编程框架,它允许Java代码与使用其他编程语言(如C、C++)编写的应用程序和库进行交互。JNI提供了一组API,使Java虚拟机(JVM)能够调用本地代码(native code),反之亦然。
jni的两个常见使用:
- 从Java程序调用C/C++(常用,本文也是主要介绍这个)
- 从C/C++程序调用Java代码
三、基本使用
(一)所需工具
javac.exe: Java 编译器:随JDK一起提供的
java.exe: Java 虚拟机(JVM):随 JDK一起提供的 。
javah.exe: 本机方法 C 文件生成器:随JDK一起提供的 。
(二)java加载文件的方法
方式1:只需要指定动态库的名字,不需要加lib前缀,也不要加.so、.dll和.jnilib后缀
方式2:指定动态库的绝对路径,需要加上前缀和后缀
此两种方式的另一种叫法为静态调用和动态调用:
jni的静态调用和动态调用
1、静态调用:静态调用指的是Java应用在编译时就知道所调用的本地库,并且在加载时会使用固定的库名称。这种方式通常会通过System.loadLibrary方法来加载本地库。
System.loadLibrary("myNativeLibrary");
步骤;
创建Java类,并声明本地方法。
生成C/C++头文件。
实现本地方法。
编译生成动态库。
在Java程序中加载动态库并调用本地方法。
2、动态调用:动态调用是指在运行时动态加载和调用本地方法,而不需要在编译时就知道具体的本地库。这种方式通常通过System.load方法来加载本地库,可以灵活地指定库的路径。
System.load("/path/to/myNativeLibrary.so");
步骤:
创建Java类,并声明本地方法。
在运行时加载本地库。
针对方式1:让java从java.library.path找到动态链接库文件:
- 将动态链接库拷贝到java.library.path目录下
- 给jvm添加“-Djava.library.path=动态链接库搜索目录”参数,指定系统属性java.library.path的值
Linux/Unix环境下可以通过设置LD_LIBRARY_PATH环境变量,指定库的搜索目录。
在linux中添加系统动态库依赖方式如下:
1、临时添加
export LD_LIBRARY_PATH=动态库所在的目录:$LD_LIBRARY_PATH
如:
动态库在/home/test/下:
export LD_LIBRARY_PATH=/home/test/:$LD_LIBRARY_PATH
动态库在当前目录下:(.表示当前目录,也可使用`pwd`)
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
2、永久添加
vim ~/.bashrc中添加export LD_LIBRARY_PATH=/your/custom/library/path:$LD_LIBRARY_PATH(bash -c 'echo "export LD_LIBRARY_PATH=/your/custom/library/path:$LD_LIBRARY_PATH">>~/.bashrc')
source ~/.bashrc
示例:
static {
System.loadLibrary("native-lib"); // 须在系统或者调用参数中指定动态库路径
}
static {
System.load(/home/libnative-lib.so);
}
(三)基本调用过程
1、编写声明了native方法的Java类
2、将Java源代码编译成class字节码文件
3、用javah -jni命令生成.h头文件(javah是jdk自带的一个命令,-jni参数表示将class中用native声明的函数生成jni规则的函数)
4、用本地代码(c、c++、其他语言)实现.h头文件中的函数
5、将本地代码编译成动态库(windows:.dll,linux/unix:.so,mac-os x:*.jnilib)
6、拷贝动态库至 java.library.path 本地库搜索目录下,或设置jvm参数-Djava.library.path=链接库所在目录,并运行Java程序
(替代:linux下直接设置LD_LIBRARY_PATH环境变量,指定库的搜索目录)
7、执行java程序测试java调用
四、示例
(一)基础示例
1.GetInfo.java
2.生成头文件GetInfo.h
javac GetInfo.java
javac -h . GetInfo.java
3.编写cpp实现文件GetInfo.cpp
4.生成动态库libextract.so
g++ -shared -fPIC -o libextract.so GetInfo.cpp -I/usr/lib/jvm/java-11-openjdk-amd64/include -I/usr/lib/jvm/java-11-openjdk-amd64/include/linux
5.使用java测试调用
javac GetInfo.java
运行程序前将动态库路径添加到系统中:此处的点可以替换为实际动态库存放路径
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH #临时,.表示当前目录,也可使用`pwd`
java GetInfo
(二)示例环境
使用openjdk
前提环境:
1、使用ubuntu环境
2、系统中无jdk
3、使用以下命令安装jdk
apt-get install default-jdk
4、通过find命令查找jni需要的jni.h文件的目录,jni.h中依赖的jni
jni.h所在目录:/usr/lib/jvm/java-11-openjdk-amd64/include/
jni_md所在目录:/usr/lib/jvm/java-11-openjdk-amd64/include/linux
(三)预设场景
场景一:使用openjdk
场景二:场景一+增加包名
场景三:场景一+动态库依赖其他动态库
场景四:场景一+增加包名+动态库依赖其他动态库
针对场景二:包名对头文件和方法生成有所影响:包名为com.util,生成方法出现报名会出现Java_com_util_GetInfo_getHeadInfo,头文件名也会相应改变
(四)具体示例
场景一
文件目录结构:部分为手动添加,部分为生成
|--GetInfo.h #生成,jni生成,使用java -h命令
|--GetInfo.java #手动,添加和编写
|--GetInfo.class #生成,javac编译生成
|--GetInfo.cpp #手动,根据动态生成的头文件编写cpp文件
|--libextract.so #生成,c++动态库
1.GetInfo.java
public class GetInfo {
static {
System.loadLibrary("extract"); // // 加载本地库Load native library at runtime, libextract.so
}
// 声明本地方法
public native String getHeadInfo(String filePath); // the native method
public static void main(String[] args) {
GetInfo getInfo = new GetInfo();
String info = getInfo.getHeadInfo(""); // invoke the native method
System.out.println(info);
}
}
2.生成头文件GetInfo.h
javac GetInfo.java
javac -h . GetInfo.java
生成内容:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class GetInfo */
#ifndef _Included_GetInfo
#define _Included_GetInfo
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: GetInfo
* Method: getHeadInfo
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_GetInfo_getHeadInfo
(JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif
3.编写cpp实现文件GetInfo.cpp
#include "GetInfo.h"
extern "C"
JNIEXPORT jstring JNICALL Java_GetInfo_getHeadInfo(JNIEnv *env, jobject obj, jstring filePath)
{
// 创建一个 C 字符串
const char *greeting = "Hello from JNI!";
// 将 C 字符串转换为 jstring 并返回
return env->NewStringUTF(greeting);
}
4.生成动态库libextract.so
g++ -shared -fPIC -o libextract.so GetInfo.cpp -I/usr/lib/jvm/java-11-openjdk-amd64/include -I/usr/lib/jvm/java-11-openjdk-amd64/include/linux
5.使用java测试调用
javac GetInfo.java
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
java GetInfo
场景二
文件目录结构:部分为手动添加,部分为生成
|--com
|--util
|--GetInfo.java #手动,添加和编写
|--GetInfo.class #生成,javac编译生成
|--com_util_GetInfo.h #生成,jni生成,使用java -h命令
|--com_util_GetInfo.cpp #手动,根据动态生成的头文件编写cpp文件
|--libextract.so #生成,c++动态库
GetInfo.java:增加包名com.util,代码package com.util;
package com.util;
public class GetInfo {
static {
System.loadLibrary("extract"); // 加载本地库Load native library at runtime, libextract.so, 静态代码块在类被加载时执行,调用 System.loadLibrary 方法加载名为 extract 的本地库
}
// 声明本地方法:native 关键字表明 getHeadInfo 方法是在本地代码(如 C 或 C++)中实现的。
public native String getHeadInfo(String filePath); // the native method
public static void main(String[] args) {
GetInfo getInfo = new GetInfo();
String info = getInfo.getHeadInfo(""); // invoke the native method
System.out.println(info);
}
}
生成的com_util_GetInfo.h:生成的方法中自动增加包的标识
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_util_GetInfo */
#ifndef _Included_com_util_GetInfo
#define _Included_com_util_GetInfo
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_util_GetInfo
* Method: getHeadInfo
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_util_GetInfo_getHeadInfo
(JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif
创建com_util_GetInfo.cpp文件
#include "com_util_GetInfo.h"
extern "C"
JNIEXPORT jstring JNICALL Java_com_util_GetInfo_getHeadInfo(JNIEnv *env, jobject obj, jstring filePath)
{
// 创建一个 C 字符串
const char *greeting = "Hello from JNI!";
// 将 C 字符串转换为 jstring 并返回
return env->NewStringUTF(greeting);
}
编译成动态库:
g++ -shared -fPIC -o libextract.so GetInfo.cpp -I/usr/lib/jvm/java-11-openjdk-amd64/include -I/usr/lib/jvm/java-11-openjdk-amd64/include/linux -I./com/util
测试的java文件:当前创建com/utilwen文件夹,放入java文件
javac com/util/GetInfo.java
java com/util/GetInfo
总结:
增加包名后:
①自动生成的头文件和方法增加包名
②使用java程序测试时,需按照包结构进行编译和执行
场景三
场景一的基础上增加动态库的依赖
文件目录结构:部分为手动添加,部分为生成
|--GetInfo.java #手动,添加和编写
|--GetInfo.class #生成,javac编译生成
|--GetInfo.h #生成,jni生成,使用java -h命令
|--GetInfo.cpp #手动,根据动态生成的头文件编写cpp文件
|--libextract.so #生成,c++动态库
|--base.h #手动
|--base.cpp #手动
|--libbase.so #生成,libextract.so的依赖
示例解释:java调用动态库依赖A,动态库A依赖动态库B
动态库B制作:返回要打印的字符串
base.h
#ifndef _Included_Base
#define _Included_Base
#include <string>
#ifdef __cplusplus
extern "C" {
#endif
std::string getString();
#ifdef __cplusplus
}
#endif
#endif
base.cpp
#include "GetInfo.h"
#include "base.h"
extern "C"
JNIEXPORT jstring JNICALL Java_GetInfo_getHeadInfo(JNIEnv *env, jobject obj, jstring filePath)
{
// 创建一个 C 字符串
//const char *greeting = "Hello from JNI!";
std::string greeting = getString();
// 将 C 字符串转换为 jstring 并返回
return env->NewStringUTF(greeting.c_str());
}
编译成动态库:
g++ -shared -fPIC -o libbase.so base.cpp
动态库A制作:参照1GetInfo.cpp中修改调用B中的方法
#include "GetInfo.h"
#include "base.h"
extern "C"
JNIEXPORT jstring JNICALL Java_GetInfo_getHeadInfo(JNIEnv *env, jobject obj, jstring filePath)
{
// 创建一个 C 字符串
//const char *greeting = "Hello from JNI!";
std::string greeting = getString();
// 将 C 字符串转换为 jstring 并返回
return env->NewStringUTF(greeting.c_str());
}
编译成动态库:
g++ -shared -fPIC -o libextract.so GetInfo.cpp -I/usr/lib/jvm/java-11-openjdk-amd64/include -I/usr/lib/jvm/java-11-openjdk-amd64/include/linux -L./ -lbase
java测试程序调用:
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
java GetInfo
总结:
由此可见java调用的动态库存在其他依赖动态库的情况下,只需要在系统中执行依赖路径即可(此路径包括调用动态库路径和依赖动态库路径)
场景四
前三种场景的结合
总结,增加包名后的:
1.GetInfo.java(此处增加包名,如com.util)
2.生成头文件GetInfo.h(增加包名后,自动生成的头文件名及方法包含包名,如com_util_GetInfo.h)
javac GetInfo.java
javac -h . GetInfo.java
3.编写cpp实现文件GetInfo.cpp(增加包名后,可使用com_util_GetInfo.cpp)
4.生成动态库libextract.so:(若有其他依赖,-l添加其他依赖)
g++ -shared -fPIC -o libextract.so GetInfo.cpp -I/usr/lib/jvm/java-11-openjdk-amd64/include -I/usr/lib/jvm/java-11-openjdk-amd64/include/linux
5.使用java测试调用
javac GetInfo.java(增加包名后,需要按照包结果,如当前目录下创建com/util文件夹,里面方GetInfo.java,调用程序编程javac com/util/GetInfo.java)
运行程序前将动态库路径添加到系统中:此处的点可以替换为实际动态库存放路径
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
java GetInfo(增加包名后执行命令java com/util/GetInfo)
总结-重要
1、增加包名后:
①自动生成的头文件和方法增加包名,
②使用java程序测试时,需按照包结构进行编译和执行
2、java调用的动态库存在其他依赖动态库的情况下,只需要在系统中执行依赖路径即可(此路径包括调用动态库路径和依赖动态库路径)
(五)执行jar包进行调用
1、新建maven项目,取名jniDemo,使用java1.8
2、新建包:src/main/java下新建包:com.alibaba.datax.plugin.writer.platformwriter
3、新建java文件:包com.alibaba.datax.plugin.writer.platformwriter下新建GetInfo.java
将上面示例中的GetInfo.java的内容复制进去
4、pom.xml增加打包:注意替换主类
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.4.0</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass>com.alibaba.datax.plugin.writer.platformwriter.GetInfo</mainClass> <!-- 替换为你的主类 -->
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
5、maven执行clean,再执行package进行打包,生成的package包括两个文件jniDemo-1.0-SNAPSHOT.jar和jniDemo-1.0-SNAPSHOT-jar-with-dependencies.jar
6、将此jar上传到linux环境中,执行调用
java -jar jniDemo-1.0-SNAPSHOT-jar-with-dependencies.jar
(六)错误及解决
1、系统中存在jni,但是编译时提示没有那个文件或目录
【解决】
编译动态库指定头文件路径:
g++ -shared -fPIC GetInfo.cpp -I/usr/lib/jvm/java-11-openjdk-amd64/include -I/usr/lib/jvm/java-11-openjdk-amd64/include/linux
/usr/lib/jvm/java-11-openjdk-amd64/include:jni.h路径
/usr/lib/jvm/java-11-openjdk-amd64/include/linux:jni.h中引用的jni
2、返回值jstring找不到方法
【报错信息】
g++ -shared -fPIC GetInfo.cpp -I/usr/lib/jvm/java-11-openjdk-amd64/include -I/usr/lib/jvm/java-11-openjdk-amd64/include/linux
GetInfo.cpp: In function ‘_jstring* Java_GetInfo_getHeadInfo(JNIEnv*, jobject, jstring)’:
GetInfo.cpp:9:18: error: base operand of ‘->’ has non-pointer type ‘JNIEnv {aka JNIEnv_}’
return (*env)->NewStringUTF(env, greeting);
【原因】
env本身为指针类型-->不同的编译器和jni实现方式不同
【解决】
将return (*env)->NewStringUTF(env, greeting);
改为return env->NewStringUTF(greeting);
3、调用时产生错误java.lang.UnsatisfiedLinkError
【报错信息】
root@lym-vm:/home/work/testuse/jni# java GetInfo
Exception in thread "main" java.lang.UnsatisfiedLinkError: no extract in java.library.path: [/usr/java/packages/lib, /usr/lib/x86_64-linux-gnu/jni, /lib/x86_64-linux-gnu, /usr/lib/x86_64-linux-gnu, /usr/lib/jni, /lib, /usr/lib]
at java.base/java.lang.ClassLoader.loadLibrary(ClassLoader.java:2673)
at java.base/java.lang.Runtime.loadLibrary0(Runtime.java:830)
at java.base/java.lang.System.loadLibrary(System.java:1873)
at GetInfo.<clinit>(GetInfo.java:3)
【原因】
java调用动态库找不到库
【解决】
将动态库添加进系统
①临时添加
此处的点可以替换为实际动态库存放路径,如/opt/libso
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
②永久添加
sudo bash -c 'echo "export LD_LIBRARY_PATH=/opt/libso:$LD_LIBRARY_PATH">>~/.bashrc'
source ~/.bashrc
4、javac调用错误
【背景】
java文件增加包名,package com.util;,执行javac时报错
【错误】
# javac GetInfo
错误: 仅当显式请求注释处理时才接受类名称 'GetInfo'
1 个错误
【原因】
包结构影响
【解决】
确保代码保存为 GetInfo.java 并放在适当的目录中。例如,确保路径为 com/util/GetInfo.java
编译文件
javac com/util/GetInfo.java
运行程序
java com.util.GetInfo
5、jni调用动态库的依赖还未指定
【错误】
# java com/alibaba/datax/plugin/writer/platformwriter/GetEmlHeadInfoJNI
Exception in thread "main" java.lang.UnsatisfiedLinkError: 'java.lang.String com.alibaba.datax.plugin.writer.platformwriter.GetEmlHeadInfoJNI.getEmlHeadInfo(java.lang.String)'
at com.alibaba.datax.plugin.writer.platformwriter.GetEmlHeadInfoJNI.getEmlHeadInfo(Native Method)
at com.alibaba.datax.plugin.writer.platformwriter.GetEmlHeadInfoJNI.main(GetEmlHeadInfoJNI.java:16)
【解决】
同3
参考
1、https://blog.csdn.net/qq877728715/article/details/116664282(主要看这个,这篇很详细并且思路很清晰)
本文来自博客园,作者:circlelll,转载请注明原文链接:https://www.cnblogs.com/circlelll/p/18304364