1. 简介
Java 的一大优势是“一次编写,到处运行”——编译后的字节码可以在任何支持 JVM 的设备上运行,无需修改。
但有些场景下,我们不得不调用特定平台的原生代码(native code),比如:
- 需要直接操作硬件设备
- 某些计算密集型任务对性能要求极高
- 已有成熟的 C/C++ 库,不想用 Java 重写
为了解决这个问题,JDK 提供了 JNI(Java Native Interface),作为 JVM 字节码与原生代码(通常是 C/C++)之间的桥梁。
本文将带你从零实现一个 JNI 项目,并深入常用特性。
2. JNI 原理
2.1. native 方法:JVM 与原生代码的连接点
Java 提供了 native
关键字,用于声明一个方法的实现由原生代码提供:
private native void aNativeMethod();
这相当于一个“抽象方法”,但它的实现不在 Java 类中,而是在一个独立的动态链接库(shared library)里。
JNI 会在运行时建立一张函数指针表,将 Java 中的 native 方法与原生库中的实际函数绑定起来,实现跨语言调用。
✅ 动态库 vs 静态库
- 静态库:编译时直接打包进可执行文件,体积大但独立
- 动态库(.so/.dll/.dylib):运行时动态加载,体积小但依赖环境
JNI 使用动态库,因为字节码和原生代码无法合并为单一二进制文件。
2.2. 核心组件
要使用 JNI,你需要准备以下几样东西:
- Java 代码:包含至少一个
native
方法的类 - 原生代码:用 C/C++ 实现 native 方法的具体逻辑
- JNI 头文件:
$JAVA_HOME/include/jni.h
,定义了所有 JNI 接口 - C/C++ 编译器:如 GCC、Clang、MSVC,用于生成动态库
2.3. 关键术语与接口
Java 侧
native
:标记方法为原生实现System.loadLibrary("libname")
:加载名为libname
的动态库(无需加前缀lib
或后缀.so/.dll
)
C/C++ 侧(来自 jni.h
)
JNIEXPORT
:标记函数可被 JNI 导出JNICALL
:确保函数调用约定与 JNI 兼容JNIEnv*
:核心环境指针,提供操作 Java 对象、调用方法、转换类型等能力JavaVM*
:JVM 实例指针,可用于启动/控制 JVM(高级用法)
3. 实战:Hello World
我们使用 C++ 和 G++ 编译器演示完整流程。
⚠️ 编译器安装:
- Ubuntu:
sudo apt-get install build-essential
- Windows: 安装 MinGW-w64
- macOS: 终端执行
g++
,系统会提示安装命令行工具
3.1. 编写 Java 类
package com.baeldung.jni;
public class HelloWorldJNI {
static {
System.loadLibrary("native"); // 加载 libnative.so / native.dll / libnative.dylib
}
public static void main(String[] args) {
new HelloWorldJNI().sayHello();
}
private native void sayHello();
}
✅ 建议在 static
块中加载库,确保类加载时库就绪。
3.2. 生成 C++ 头文件并实现
使用 javac -h
自动生成 C++ 头文件:
javac -h . HelloWorldJNI.java
生成文件:com_baeldung_jni_HelloWorldJNI.h
JNIEXPORT void JNICALL Java_com_baeldung_jni_HelloWorldJNI_sayHello
(JNIEnv *, jobject);
函数命名规则:Java_包名_类名_方法名
,其中包名用下划线 _
替代点 .
。
创建 com_baeldung_jni_HelloWorldJNI.cpp
实现函数:
#include <iostream>
#include "com_baeldung_jni_HelloWorldJNI.h"
JNIEXPORT void JNICALL Java_com_baeldung_jni_HelloWorldJNI_sayHello
(JNIEnv* env, jobject thisObject) {
std::cout << "Hello from C++ !!" << std::endl;
}
参数说明:
JNIEnv* env
:JNI 环境指针jobject thisObject
:当前 Java 对象实例(即this
)
3.3. 编译与链接
编译为目标文件(.o)
Ubuntu / Linux:
g++ -c -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux com_baeldung_jni_HelloWorldJNI.cpp -o com_baeldung_jni_HelloWorldJNI.o
Windows:
g++ -c -I%JAVA_HOME%\include -I%JAVA_HOME%\include\win32 com_baeldung_jni_HelloWorldJNI.cpp -o com_baeldung_jni_HelloWorldJNI.o
macOS:
g++ -c -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/darwin com_baeldung_jni_HelloWorldJNI.cpp -o com_baeldung_jni_HelloWorldJNI.o
✅
-fPIC
:生成位置无关代码,动态库必需
链接为动态库
Ubuntu / Linux:
g++ -shared -fPIC -o libnative.so com_baeldung_jni_HelloWorldJNI.o -lc
Windows:
g++ -shared -o native.dll com_baeldung_jni_HelloWorldJNI.o -Wl,--add-stdcall-alias
macOS:
g++ -dynamiclib -o libnative.dylib com_baeldung_jni_HelloWorldJNI.o -lc
✅ 库名必须与
System.loadLibrary("native")
一致
3.4. 运行程序
java -cp . -Djava.library.path=/path/to/so/dir com.baeldung.jni.HelloWorldJNI
输出:
Hello from C++ !!
✅ -Djava.library.path
指定动态库所在目录,否则会报 UnsatisfiedLinkError
。
4. 高级特性:数据交互与对象操作
4.1. 传递参数与返回值
Java 类:
public class ExampleParametersJNI {
static {
System.loadLibrary("native");
}
public static void main(String[] args) {
ExampleParametersJNI example = new ExampleParametersJNI();
System.out.println("Sum: " + example.sumIntegers(5, 7));
System.out.println(example.sayHelloToMe("Alice", true));
}
private native long sumIntegers(int first, int second);
private native String sayHelloToMe(String name, boolean isFemale);
}
C++ 实现:
#include <iostream>
#include <string>
#include "com_baeldung_jni_ExampleParametersJNI.h"
JNIEXPORT jlong JNICALL Java_com_baeldung_jni_ExampleParametersJNI_sumIntegers
(JNIEnv* env, jobject thisObject, jint first, jint second) {
std::cout << "C++: The numbers received are : " << first << " and " << second << std::endl;
return (long)first + (long)second;
}
JNIEXPORT jstring JNICALL Java_com_baeldung_jni_ExampleParametersJNI_sayHelloToMe
(JNIEnv* env, jobject thisObject, jstring name, jboolean isFemale) {
const char* nameCharPointer = env->GetStringUTFChars(name, NULL);
std::string title = isFemale ? "Ms. " : "Mr. ";
std::string fullName = title + std::string(nameCharPointer);
// 释放 GetStringUTFChars 分配的内存
env->ReleaseStringUTFChars(name, nameCharPointer);
return env->NewStringUTF(fullName.c_str());
}
⚠️ 注意:
- Java 字符串转 C 字符串:
GetStringUTFChars
,用完必须ReleaseStringUTFChars
- C 字符串转 Java 字符串:
NewStringUTF
- 类型映射参考:Oracle JNI 官方文档
4.2. 操作 Java 对象并调用其方法
定义 Java 对象类
public class UserData {
public String name;
public double balance;
public String getUserInfo() {
return "[name]=" + name + ", [balance]=" + balance;
}
}
原生方法操作对象
public class ExampleObjectsJNI {
static {
System.loadLibrary("native");
}
public static void main(String[] args) {
ExampleObjectsJNI example = new ExampleObjectsJNI();
UserData user = example.createUser("Bob", 1000.0);
System.out.println(example.printUserData(user));
}
public native UserData createUser(String name, double balance);
public native String printUserData(UserData user);
}
C++ 实现
#include "com_baeldung_jni_ExampleObjectsJNI.h"
JNIEXPORT jobject JNICALL Java_com_baeldung_jni_ExampleObjectsJNI_createUser
(JNIEnv *env, jobject thisObject, jstring name, jdouble balance) {
// 1. 获取类引用
jclass userDataClass = env->FindClass("com/baeldung/jni/UserData");
if (userDataClass == nullptr) return nullptr;
// 2. 创建对象实例(不调用构造函数)
jobject newUserData = env->AllocObject(userDataClass);
if (newUserData == nullptr) return nullptr;
// 3. 获取字段 ID
jfieldID nameField = env->GetFieldID(userDataClass, "name", "Ljava/lang/String;");
jfieldID balanceField = env->GetFieldID(userDataClass, "balance", "D");
// 4. 设置字段值
env->SetObjectField(newUserData, nameField, name);
env->SetDoubleField(newUserData, balanceField, balance);
return newUserData;
}
JNIEXPORT jstring JNICALL Java_com_baeldung_jni_ExampleObjectsJNI_printUserData
(JNIEnv *env, jobject thisObject, jobject userData) {
// 1. 获取对象的类
jclass userDataClass = env->GetObjectClass(userData);
// 2. 获取方法 ID(方法名 + 签名)
jmethodID methodId = env->GetMethodID(userDataClass, "getUserInfo", "()Ljava/lang/String;");
if (methodId == nullptr) return nullptr;
// 3. 调用 Java 方法
jstring result = (jstring)env->CallObjectMethod(userData, methodId);
return result;
}
✅ 核心思路:
- 通过
FindClass
找到类 - 通过
GetFieldID
/GetMethodID
获取字段或方法的“句柄” - 通过
SetXxxField
/CallXxxMethod
操作对象
这类似于 Java 反射,但发生在 native 层。
5. JNI 的缺点
虽然 JNI 能提升性能或复用现有库,但也带来明显代价:
❌ 平台依赖性
你失去了 Java 的跨平台优势。每支持一个平台(Windows/Linux/macOS)和架构(x86/ARM),就要编译一个对应的动态库。
❌ 通信开销
JVM 与 native 代码之间的数据传递需要序列化/反序列化(marshaling/unmarshaling),尤其是对象、字符串等复杂类型,性能损耗不可忽视。
❌ 类型映射复杂
不是所有 Java 类型都能直接映射到 C/C++,有时需要手动转换,容易出错。
❌ 调试困难
一旦 crash,堆栈可能跨越 Java 和 native,排查难度陡增。
6. 总结
JNI 是一把双刃剑:
✅ 适用场景:
- 调用硬件驱动或系统 API
- 性能敏感的计算(如图像处理、加密)
- 复用成熟 C/C++ 库(如 OpenCV、FFmpeg)
❌ 慎用场景:
- 纯业务逻辑
- 可用 Java 高效实现的功能
- 跨平台要求严格的项目
📌 原则:只有在没有 Java 替代方案时才使用 JNI。
本文完整代码已托管至 GitHub:https://github.com/eugenp/tutorials/tree/master/core-java-modules/java-native