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 類型 | 描述 |
| boolean | jboolean | unsigned char | 8 位 |
| byte | jbyte | signed char | 8 位 |
| char | jchar | unsigned short | 16 位 Unicode |
| short | jshort | short | 16 位 |
| int | jint | int | 32 位 |
| long | jlong | long long | 64 位 |
| float | jfloat | float | 32 位 |
| double | jdouble | double | 64 位 |
| String | jstring | opaque | 需 GetStringUTFChars |
| Object[] | jobjectArray | opaque | |
| byte[] | jbyteArray | opaque | 需 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/ASCII | Java 端 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 安全邊界,換取極致性能與舊生態互通。能不用盡量不用;必須用時,嚴守「獲取-釋放配對、異常安全、編碼明確、線程同步」四大鐵律。