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

JNI 完全實戰手冊:Java 調用 C/C++ 動態庫全流程詳解

     編輯:Java 相關知識

JNI 核心流程圖

Java 代碼 (native 方法聲明)
    ↓ javac 編譯
.class 文件
    ↓ javah -jni 生成
.h 頭文件 (函數簽名映射)
    ↓ C/C++ 實現 + 編譯鏈接
動態庫 (.dll/.so/.dylib)
    ↓ System.loadLibrary 載入
Java 運行時調用

第一階段:Java 端聲明與編譯

// SendSMS.java
package com.mobilesoft.sms;

public class SendSMS {
    static {
        // 載入動態庫:名稱不含前綴 lib 和後綴 .dll/.so
        // 必須在 java.library.path 指定的目錄下
        System.loadLibrary("sms"); 
    }

    // 僅聲明,不實現
    public native static int smsInit();
    public native static int smsSend(byte[] mobileNo, byte[] content);

    public static void main(String[] args) {
        SendSMS sms = new SendSMS();
        sms.smsInit();
        byte[] mobile = "13661630667".getBytes(StandardCharsets.US_ASCII);
        byte[] content = "早上好".getBytes("GBK"); // 按短信網關要求編碼
        int ret = sms.smsSend(mobile, content);
        System.out.println(ret >= 0 ? "發送成功" : "發送失敗");
    }
}

編譯並生成頭文件

# 1. 編譯
javac -d . SendSMS.java

# 2. 生成 JNI 頭文件 (JDK 8+ 可用 javac -h)
javah -jni -d jni/include com.mobilesoft.sms.SendSMS
# 生成: jni/include/com_mobilesoft_sms_SendSMS.h

第二階段:C/C++ 實現

生成的頭文件關鍵內容

/* DO NOT EDIT - machine generated */
#include 

#ifndef _Included_com_mobilesoft_sms_SendSMS
#define _Included_com_mobilesoft_sms_SendSMS
#ifdef __cplusplus
extern "C" {
#endif

JNIEXPORT jint JNICALL Java_com_mobilesoft_sms_SendSMS_smsInit
  (JNIEnv *, jclass);

JNIEXPORT jint JNICALL Java_com_mobilesoft_sms_SendSMS_smsSend
  (JNIEnv *, jclass, jbyteArray, jbyteArray);

#ifdef __cplusplus
}
#endif
#endif

關鍵類型映射表

Java 類型JNI 類型C 類型描述
booleanjbooleanunsigned char8 位
bytejbytesigned char8 位
charjcharunsigned short16 位 Unicode
shortjshortshort16 位
intjintint32 位
longjlonglong long64 位
floatjfloatfloat32 位
doublejdoubledouble64 位
Stringjstringopaque需 GetStringUTFChars
Object[]jobjectArrayopaque
byte[]jbyteArrayopaque需 GetByteArrayElements

實現文件:SmsJNI.c

#include "com_mobilesoft_sms_SendSMS.h"
#include "sms.h"  // 第三方短信庫頭文件

// 全局狀態 (演示用,實際應線程安全)
static int initDone = 0;

JNIEXPORT jint JNICALL Java_com_mobilesoft_sms_SendSMS_smsInit
  (JNIEnv *env, jclass clazz) {
    if (initDone) return 0;
    int ret = SmsInit(); // 調用第三方 C 庫
    if (ret == 0) initDone = 1;
    return ret;
}

JNIEXPORT jint JNICALL Java_com_mobilesoft_sms_SendSMS_smsSend
  (JNIEnv *env, jclass clazz, jbyteArray mobileArr, jbyteArray contentArr) {

    // 1. 獲取 Java byte[] 長度
    jsize mobileLen = (*env)->GetArrayLength(env, mobileArr);
    jsize contentLen = (*env)->GetArrayLength(env, contentArr);

    // 2. 釘住數組並獲取指針 (關鍵:必須 Release)
    jbyte *mobilePtr = (*env)->GetByteArrayElements(env, mobileArr, NULL);
    jbyte *contentPtr = (*env)->GetByteArrayElements(env, contentArr, NULL);
    if (mobilePtr == NULL || contentPtr == NULL) {
        // 記憶體不足或異常
        if (mobilePtr) (*env)->ReleaseByteArrayElements(env, mobileArr, mobilePtr, JNI_ABORT);
        if (contentPtr) (*env)->ReleaseByteArrayElements(env, contentArr, contentPtr, JNI_ABORT);
        return -1;
    }

    // 3. 轉為 C 字符串 (注意:Java byte[] 不保證以 \0 結尾)
    // 這裡假設第三方庫接受長度參數,否則需手動拷貝並添加 \0
    char *mobile = (char *)mobilePtr;
    char *content = (char *)contentPtr;

    // 4. 調用第三方庫
    int ret = SmsSend(mobile, content);

    // 5. 釋放釘住的數組 (模式: mode=0 刷新回 Java; JNI_ABORT 不刷新; JNI_COMMIT 刷新但不釋放)
    (*env)->ReleaseByteArrayElements(env, mobileArr, mobilePtr, JNI_ABORT);
    (*env)->ReleaseByteArrayElements(env, contentArr, contentPtr, JNI_ABORT);

    return ret;
}

第三階段:編譯動態庫

Windows (MSVC)

cl /I"%JAVA_HOME%\include" /I"%JAVA_HOME%\include\win32" /I"third_party\include" ^
   /LD SmsJNI.c /Fe:sms.dll /link third_party\lib\sms.lib

Linux/macOS (GCC/Clang)

gcc -shared -fPIC \
  -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" \
  -I"third_party/include" \
  SmsJNI.c -o libsms.so -L"third_party/lib" -lsms

第四階段:運行驗證

# 確保動態庫在 java.library.path
java -Djava.library.path=./lib com.mobilesoft.sms.SendSMS
# 或將 dll/so 放入系統路徑、或用 System.load(絕對路徑)

高頻坑點與最佳實踐

坑點解決方案
UnsatisfiedLinkError: no sms in java.library.path檢查 -Djava.library.path、LD_LIBRARY_PATH/PATH、庫文件名 (lib 前綴、.so/.dll 後綴)
UnsatisfiedLinkError: Can't find dependent libraries依賴的第三方動態庫未在搜索路徑;Windows 用 Dependency Walker、Linux 用 ldd 排查
忘記 ReleaseByteArrayElements → 內存洩漏 / GC 卡死嚴格配對 Get/Release,用 try-finally 模式或 RAII 封裝
Java byte[] 無 \0 結尾,C 庫期望字符串手動 malloc(len+1)、memcpy、buf[len]='\0'、調用後 free
編碼不一致:Java UTF-16 → C 期望 GBK/ASCIIJava 端 getBytes("GBK"),或 C 端 iconv 轉碼
異常拋出:C 代碼崩潰導致 JVM 進程崩潰JNI 函數開頭檢查 ExceptionCheck()、捕獲 C++ 異常轉 Java 異常 ThrowNew
多線程併發調用非線程安全 C 庫Java 端 synchronized 或 C 端互斥鎖保護

現代化替代方案

  • JNA (Java Native Access):純 Java、無需編譯 C、基於 libffi、開發效率極高
  • Foreign Function & Memory API (JEP 454, JDK 22+):標準化、內存安全、效能接近 JNI、Linker.nativeLinker()、MemorySegment
  • GraalVM Native Image:AOT 編譯 Java 直調 C、無 JNI 開銷

JNI 是雙刃劍:打破 Java 安全邊界,換取極致性能與舊生態互通。能不用盡量不用;必須用時,嚴守「獲取-釋放配對、異常安全、編碼明確、線程同步」四大鐵律。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved