首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 程式設計入門

Java JNI 開發完整指南:調用本地 C/C++ 庫實現系統級功能

     編輯:Java 程式設計入門

前言:JNI 是 Java 通往本地世界的橋樑

Java 「一次編寫,到處運行」的跨平台特性,源於 JVM 屏蔽了作業系統差異。但這也意味著 Java 無法直接訪問硬體、呼叫作業系統原生 API、使用高效能 C/C++ 庫、整合舊有 C 代碼庫。**Java Native Interface (JNI)** 打破了這層屏障,允許 Java 代碼調用本地共享庫 (.dll Windows / .so Linux/Unix / .dylib macOS) 中的函數。本文完整演示 JNI 開發全流程:Java 宣告 → javah 生成頭文件 → C/C++ 實現 → 編譯鏈接 → Java 載入調用,並解析關鍵資料類型映射、記憶體管理、異常處理、效能優化與現代替代方案。

一、JNI 開發標準流程

  1. Java 類別宣告 native 方法,System.loadLibrary 載入庫。
  2. javac 編譯 Java 源碼生成 .class。
  3. javah -jni ClassName (JDK 8-) 或 javac -h . ClassName.java (JDK 10+) 生成 C/C++ 頭文件 .h。
  4. 編寫 C/C++ 實現 (.c/.cpp),包含頭文件,實現 JNIEXPORT 函數。
  5. 編譯鏈接生成共享庫 (.dll/.so/.dylib)。
  6. 庫放入 java.library.path (或 -Djava.library.path=...),Java 運行調用。

二、完整實戰範例:計數器庫

2.1 Java 端宣告


public class Counter {
    // 靜態載入庫 (無副檔名,平台自動匹配)
    static {
        System.loadLibrary("counter"); // Windows: counter.dll, Linux: libcounter.so
    }

    // 宣告 native 方法 (僅宣告,無實現)
    public native static void reset();
    public native static int get();
    public native static void increment();
    public native static void add(int value);

    // 測試主程式
    public static void main(String[] args) {
        reset();
        System.out.println("Initial: " + get()); // 0
        increment();
        System.out.println("After inc: " + get()); // 1
        add(10);
        System.out.println("After add 10: " + get()); // 11
    }
}

2.2 生成頭文件


# JDK 8 及之前
javac Counter.java
javah -jni Counter

# JDK 10+ (javah 已移除,javac 內建支援)
javac -h . Counter.java

生成 Counter.h (關鍵片段):


/* DO NOT EDIT - machine generated */
#include 
#ifndef _Included_Counter
#define _Included_Counter
#ifdef __cplusplus
extern "C" {
#endif

JNIEXPORT void JNICALL Java_Counter_reset
  (JNIEnv *, jclass);

JNIEXPORT jint JNICALL Java_Counter_get
  (JNIEnv *, jclass);

JNIEXPORT void JNICALL Java_Counter_increment
  (JNIEnv *, jclass);

JNIEXPORT void JNICALL Java_Counter_add
  (JNIEnv *, jclass, jint);

#ifdef __cplusplus
}
#endif
#endif

2.3 C 實現


#include "Counter.h"
#include 

static int counter = 0; // 本地靜態變數,生命週期 = 庫載入期間

JNIEXPORT void JNICALL Java_Counter_reset(JNIEnv *env, jclass cls) {
    counter = 0;
    printf("[Native] Counter reset to 0\n");
}

JNIEXPORT jint JNICALL Java_Counter_get(JNIEnv *env, jclass cls) {
    return (jint)counter;
}

JNIEXPORT void JNICALL Java_Counter_increment(JNIEnv *env, jclass cls) {
    counter++;
    printf("[Native] Counter incremented to %d\n", counter);
}

JNIEXPORT void JNICALL Java_Counter_add(JNIEnv *env, jclass cls, jint value) {
    counter += value;
    printf("[Native] Counter added %d, now %d\n", value, counter);
}

2.4 編譯鏈接 (跨平台)

關鍵:包含 jni.h 及平台特定目錄 (win32/linux/darwin)。

2.5 運行測試


# 庫放在當前目錄或 java.library.path
java -Djava.library.path=. Counter

輸出:


[Native] Counter reset to 0
Initial: 0
[Native] Counter incremented to 1
After inc: 1
[Native] Counter added 10, now 11
After add 10: 11

三、JNI 核心資料類型映射表

平台編譯命令 (GCC/Clang/MSVC)輸出檔案
Linux/macOSgcc -shared -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux Counter.c -o libcounter.solibcounter.so
Windows (MinGW)gcc -shared -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" Counter.c -o counter.dllcounter.dll
Windows (MSVC)cl /LD /I"%JAVA_HOME%\include" /I"%JAVA_HOME%\include\win32" Counter.c /Fecounter.dllcounter.dll
Java 類型JNI 類型C/C++ 類型說明
booleanjbooleanunsigned char (8-bit)JNI_TRUE/JNI_FALSE
bytejbytesigned char (8-bit)
charjcharunsigned short (16-bit)Unicode 碼單元
shortjshortshort (16-bit)
intjintint (32-bit)
longjlonglong long / int64_t (64-bit)
floatjfloatfloat (32-bit)
doublejdoubledouble (64-bit)
Objectjobject_jobject*所有物件統一類型
Classjclass_jclass*
Stringjstring_jstring*需轉換才能訪問內容
int[]jintArray_jintArray*陣列類型各異
Throwablejthrowable_jthrowable*

四、關鍵進階操作:JNIEnv API 精選

4.1 字串處理 (最常用、最易出錯)


// Java String → C char* (UTF-8)
const char *cstr = (*env)->GetStringUTFChars(env, jstr, NULL);
// 使用 cstr...
(*env)->ReleaseStringUTFChars(env, jstr, cstr); // 必須釋放!

// Java String → C jchar* (UTF-16)
const jchar *uchars = (*env)->GetStringChars(env, jstr, NULL);
jsize len = (*env)->GetStringLength(env, jstr);
// 使用 uchars[0..len-1]...
(*env)->ReleaseStringChars(env, jstr, uchars);

// C char* (UTF-8) → Java String
jstring jstr = (*env)->NewStringUTF(env, "Hello JNI");

// C jchar* (UTF-16) → Java String
jstring jstr = (*env)->NewString(env, uchars, len);

警告:GetStringUTFChars 返回的指針可能指向 JVM 內部緩衝區或複製,**必須配對 Release**,否則記憶體洩漏/崩潰。

4.2 陣列操作


// 獲取陣列元素指針 (可能複製)
jint *arr = (*env)->GetIntArrayElements(env, jarr, NULL);
jsize len = (*env)->GetArrayLength(env, jarr);
// 修改 arr[0..len-1]...
// 模式: 0=複製回去並釋放, JNI_COMMIT=複製回去不釋放, JNI_ABORT=不複製直接釋放
(*env)->ReleaseIntArrayElements(env, jarr, arr, 0);

// 獲取直接指針 (無複製,高效能,但可能阻塞 GC 移動物件) - 謹慎使用
jint *arr = (*env)->GetPrimitiveArrayCritical(env, jarr, NULL);
// ... 短時間內快速讀寫 ...
(*env)->ReleasePrimitiveArrayCritical(env, jarr, arr, 0);

4.3 物件欄位/方法存取


// 獲取類別
jclass cls = (*env)->GetObjectClass(env, obj); // 實例
// 或 jclass cls = (*env)->FindClass(env, "java/lang/String"); // 靜態

// 獲取欄位 ID
jfieldID fid = (*env)->GetFieldID(env, cls, "fieldName", "I"); // 簽名 "I"=int
// 讀寫 int 欄位
jint val = (*env)->GetIntField(env, obj, fid);
(*env)->SetIntField(env, obj, fid, newVal);

// 調用方法
jmethodID mid = (*env)->GetMethodID(env, cls, "methodName", "(I)V"); // 簽名
(*env)->CallVoidMethod(env, obj, mid, arg);
// 靜態方法
jmethodID smid = (*env)->GetStaticMethodID(env, cls, "staticMethod", "()Ljava/lang/String;");
jstring result = (jstring)(*env)->CallStaticObjectMethod(env, cls, smid);

簽名格式:()V 無參無返回,(I)V 一個 int 參數無返回,(ILjava/lang/String;)Z int+String 參數返回 boolean。用 javap -s ClassName 查看。

4.4 異常處理


// 拋出 Java 異常
(*env)->ThrowNew(env, (*env)->FindClass(env, "java/lang/IllegalArgumentException"), "Invalid value from native");

// 檢查/清除異常
if ((*env)->ExceptionCheck(env)) {
    (*env)->ExceptionDescribe(env); // 列印堆疊
    (*env)->ExceptionClear(env);    // 清除,繼續執行
}

五、效能優化與最佳實踐

  1. 最小化 JNI 呼叫頻率:每次呼叫有開銷 (參數檢查、狀態切換)。批量傳遞資料 (陣列/緩衝區) 而非逐元素呼叫。
  2. 避免在 JNI 中長時間阻塞:阻塞本地執行緒會阻塞 Java 執行緒,影響 JVM 調度。耗時操作異步化或用 Java 執行緒池。
  3. 使用直接緩衝區:ByteBuffer.allocateDirect() + GetDirectBufferAddress 實現零拷貝共享記憶體。
  4. 靜態庫連結優於動態載入:System.loadLibrary 載入延遲,靜態連結 (GraalVM Native Image) 消除啟動開銷。
  5. 資源管理:本地資源 (檔案描述符、記憶體、鎖) 需在 Java 端提供 close()/release() native 方法,配合 try-with-resources 或 Cleaner 確保釋放。

六、現代替代方案:JNA、JNR、GraalVM Native Image、Panama

結語

JNI 是 Java 連接本地世界的強大但危險的工具。**掌握類型映射、JNIEnv API、字串/陣列/物件操作、異常傳遞、記憶體管理、編譯鏈接流程**,是正確使用 JNI 的前提。在現代架構中,**優先評估 JNA/JNR/Panama 等更安全、開發效率更高的替代方案**;僅在極致效能、整合大型 C/C++ 代碼庫、硬體驅動開發、特殊平台 API 呼叫等場景,才投入 JNI 開發。無論選擇哪種技術,**清晰的 Java/本地邊界設計、嚴格的資源生命週期管理、完善的錯誤處理與測試**,始終是構建穩健混合系統的核心。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved
方案特點適用場景
JNA (Java Native Access)純 Java,無需編譯 C 代碼,反射調用本地庫,效能略低於 JNI快速整合現有 .dll/.so、原型驗證、跨平台部署簡化
JNR-FFIJNA 衍生,更現代 API、更好效能、支援更多類型高效能本地互操作、Netty、JRuby 等專案採用
GraalVM Native ImageAOT 編譯 Java 為原生可執行檔,本地互調零開銷微服務、CLI 工具、Serverless、極致啟動速度
Project Panama (Foreign Function & Memory API)JDK 19+ 預覽/21+ 正式,純 Java 安全訪問本地記憶體/函數**未來標準**,替代 JNI/JNA,無 unsafe、無編譯、記憶體安全