java调用c++动态库之jni调用

一、背景

存在java程序调用c++程序的场景,对常见的jni技术进行使用方法的总结。

二、技术

Java Native Interface(JNI)是一种编程框架,它允许Java代码与使用其他编程语言(如C、C++)编写的应用程序和库进行交互。JNI提供了一组API,使Java虚拟机(JVM)能够调用本地代码(native code),反之亦然。

jni的两个常见使用:

  1. 从Java程序调用C/C++(常用,本文也是主要介绍这个)
  2. 从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找到动态链接库文件:

  1. 将动态链接库拷贝到java.library.path目录下
  2. 给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(主要看这个,这篇很详细并且思路很清晰)

posted @ 2024-07-16 01:58  circlelll  阅读(121)  评论(0编辑  收藏  举报