一、背景
存在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:指定動態庫的絕對路徑,需要加上字首和字尾
針對方式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
動態庫在當前目錄下:
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 #臨時
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呼叫的動態庫存在其他依賴動態庫的情況下,只需要在系統中執行依賴路徑即可(此路徑包括呼叫動態庫路徑和依賴動態庫路徑)
(五)錯誤及解決
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(主要看這個,這篇很詳細並且思路很清晰)