- 编写 Java 代码:定义一个包含
native方法的 Java 类。 - 生成 JNI 头文件:使用
javac和javah(或新工具javac -h) 工具生成 C/C++ 头文件,该文件包含了native方法的函数签名。 - 编写 C/C++ 代码:实现头文件中定义的函数,并在这些函数中调用 Java 的代码。
- 编译生成动态链接库 (DLL/SO):将 C/C++ 源代码编译成 Java 虚拟机可以加载的库文件。
- 运行 Java 程序:在 Java 程序中加载这个动态库,并调用
native方法。
准备工作
为了演示,我们假设你有一个标准的 Java 项目结构:

jni-demo/
├── src/
│ └── com/
│ └── example/
│ └── JniDemo.java
└── jni/
└── JniDemo.c
步骤 1: 编写 Java 代码
我们创建一个 Java 类,其中包含一个 native 方法,这个 native 方法本身不实现任何逻辑,它只是一个“桥梁”,由 C/C++ 代码来真正实现。
src/com/example/JniDemo.java
package com.example;
public class JniDemo {
// 声明一个 native 方法,注意没有方法体
public native void callJavaMethod();
public static void main(String[] args) {
// 1. 加载包含 native 方法的动态库
// 注意:这里只需要写库名,不需要写 .dll 或 .so
System.loadLibrary("JniDemo");
// 2. 创建 Java 对象实例
JniDemo demo = new JniDemo();
// 3. 调用 native 方法
System.out.println("Java: 准备调用 C/C++ 代码...");
demo.callJavaMethod();
System.out.println("Java: C/C++ 代码执行完毕。");
}
}
关键点:
native关键字告诉编译器,这个方法的实现不在 Java 中,而是在本地代码(C/C++)中。System.loadLibrary("JniDemo")会尝试加载名为JniDemo.dll(Windows) 或libJniDemo.so(Linux/macOS) 的动态库,JVM 会在库路径(如java.library.path)中查找这个文件。
步骤 2: 生成 JNI 头文件
我们需要一个“桥梁”文件来告诉 C/C++ 编译器,这个 callJavaMethod 函数应该是什么样子,这个文件由 JDK 的工具自动生成。

编译 Java 文件
进入 src 目录,编译 Java 文件:
cd src javac com/example/JniDemo.java
这会生成 com/example/JniDemo.class 文件。
生成头文件

有两种方式生成头文件:
方式一 (传统方式,使用 javah)
javah 在较新版本的 JDK 中已被弃用,但很多旧项目仍在使用。
# -jni 选项可以省略,因为它是默认值 # -classpath 指定 .class 文件所在的目录 javah -classpath . com.example.JniDemo
执行后,会在当前目录(src)下生成一个 com_example_JniDemo.h 文件。
方式二 (推荐方式,使用 javac -h)
这是现代 JDK 推荐的方式。
# -h 后面指定头文件输出的目录 # -d 指定 .class 文件输出的目录,这里我们直接输出到当前目录 javac -h . -d . com/example/JniDemo.java
同样会生成 com_example_JniDemo.h 文件。
生成的头文件 com_example_JniDemo.h 内容如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_JniDemo */
#ifndef _Included_com_example_JniDemo
#define _Included_com_example_JniDemo
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_JniDemo
* Method: callJavaMethod
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_example_JniDemo_callJavaMethod
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
解读头文件:
#include <jni.h>: 包含了 JNI 的核心定义,所有 JNI 程序都必须包含它。JNIEXPORT和JNICALL: 这是 JNI 函数的修饰符,用于告诉编译器和链接器这个函数是 JNI 导出的。Java_com_example_JniDemo_callJavaMethod: 这是 C/C++ 函数的命名规则:Java_+ 包名(用下划线_替换) + 类名 + 方法名。(JNIEnv *, jobject): 这是函数的参数。JNIEnv *: 这是一个指向 JNI 环境的指针,通过这个指针,你可以在 C/C++ 代码中调用所有 JNI 函数(比如创建对象、调用方法、获取字段等)。jobject: 这是 Java 对象this的引用,当callJavaMethod是一个实例方法时,这个参数就是调用该方法的对象实例,如果是static native方法,这个参数的类型会是jclass,代表类的 Class 对象。
void: 返回值类型,与 Java 方法void对应。
步骤 3: 编写 C/C++ 代码
现在我们来实现 Java_com_example_JniDemo_callJavaMethod 函数,并在其中调用 Java 的代码。
jni/JniDemo.c
#include <stdio.h>
#include "com_example_JniDemo.h" // 包含生成的头文件
// 实现 Java_com_example_JniDemo_callJavaMethod 函数
JNIEXPORT void JNICALL Java_com_example_JniDemo_callJavaMethod
(JNIEnv *env, jobject this_obj) {
printf("C: 成功进入 C/C++ 代码!\n");
// --- 核心部分:在 C/C++ 中调用 Java 代码 ---
// 1. 获取 Java 的 System 类
jclass system_class = (*env)->FindClass(env, "java/lang/System");
// 2. 获取 out 对象 (java.lang.System.out)
jfieldID out_field = (*env)->GetStaticFieldID(env, system_class, "out", "Ljava/io/PrintStream;");
jobject out_obj = (*env)->GetStaticObjectField(env, system_class, out_field);
// 3. 获取 println 方法 (java.io.PrintStream.println(String))
jclass print_stream_class = (*env)->FindClass(env, "java/io/PrintStream");
jmethodID println_method = (*env)->GetMethodID(env, print_stream_class, "println", "(Ljava/lang/String;)V");
// 4. 创建一个 Java String 对象
const char *c_str = "Hello from C/C++!";
jstring j_str = (*env)->NewStringUTF(env, c_str);
// 5. 调用 Java 的 System.out.println() 方法
(*env)->CallVoidMethod(env, out_obj, println_method, j_str);
// 6. 释放局部引用 (非常重要!)
(*env)->DeleteLocalRef(env, j_str);
(*env)->DeleteLocalRef(env, out_obj);
(*env)->DeleteLocalRef(env, print_stream_class);
(*env)->DeleteLocalRef(env, system_class);
printf("C: Java 代码调用完毕,返回 C/C++ 代码,\n");
}
关键 JNI 函数解释:
| 函数 | 作用 |
|---|---|
(*env)->FindClass(env, "className") |
根据类名(用 代替 )查找 jclass 对象。 |
(*env)->GetStaticFieldID(env, jclass, "fieldName", "fieldSignature") |
获取静态字段的 ID,签名 Ljava/lang/String; 表示一个 String 对象。 |
(*env)->GetStaticObjectField(env, jclass, fieldID) |
获取静态字段的值(一个 jobject)。 |
(*env)->FindClass(env, "className") |
查找另一个类(如 PrintStream)。 |
(*env)->GetMethodID(env, jclass, "methodName", "methodSignature") |
获取方法的 ID,签名 (Ljava/lang/String;)V 表示接收一个 String 参数,返回 void。 |
(*env)->NewStringUTF(env, "c_string") |
将 C 风格的字符串转换为 Java 的 jstring 对象。 |
(*env)->CallVoidMethod(env, obj, methodID, ...) |
调用一个实例方法。obj 是调用该方法的对象。 |
(*env)->DeleteLocalRef(env, jobj) |
非常重要! JNI 中的局部引用(通过 JNI 函数创建的)不会自动被垃圾回收,必须在不再使用时手动删除,否则会导致内存泄漏。 |
步骤 4: 编译生成动态链接库
这一步与你的操作系统密切相关。
在 Windows 上 (使用 MinGW)
你需要安装 MinGW 或 MSYS2,并确保 gcc 在你的 PATH 中。
# 进入 jni 目录 cd ../jni # 编译命令 # -I 指定 jni.h 的路径,通常在 JDK 的 include 目录下 # -shared 生成动态链接库 (.dll) # -o 指定输出的文件名 # JniDemo.c 是你的源文件 # -lws2_32 是可能需要的库,具体看情况 gcc -I"C:/Program Files/Java/jdk-11.0.12/include" -I"C:/Program Files/Java/jdk-11.0.12/include/win32" -shared -o JniDemo.dll JniDemo.c
在 Linux/macOS 上
使用 GCC 编译器。
# 进入 jni 目录 cd ../jni # 编译命令 # -I 指定 jni.h 的路径,通常在 JDK 的 include 目录下 # -shared 生成动态链接库 (.so) # -o 指定输出的文件名 # JniDemo.c 是你的源文件 # JDK 路径可能需要根据你的系统调整 gcc -I/usr/lib/jvm/java-11-openjdk-amd64/include -I/usr/lib/jvm/java-11-openjdk-amd64/include/linux -shared -o libJniDemo.so JniDemo.c
编译成功后,你会在 jni 目录下得到 JniDemo.dll (Windows) 或 libJniDemo.so (Linux/macOS)。
步骤 5: 运行 Java 程序
运行你的 Java 程序,JVM 需要能找到你刚刚生成的动态库。
在 Windows 上
- 将
JniDemo.dll复制到jni-demo根目录。 - 打开命令行,在
jni-demo目录下运行:
java -Djava.library.path=src com.example.JniDemo
或者,你也可以将 JniDemo.dll 放在 System32 目录下,然后直接运行:
java com.example.JniDemo
在 Linux/macOS 上
- 将
libJniDemo.so复制到jni-demo根目录。 - 打开终端,在
jni-demo目录下运行:
java -Djava.library.path=src com.example.JniDemo
预期输出
无论在哪个系统,你都应该看到类似下面的输出:
Java: 准备调用 C/C++ 代码...
C: 成功进入 C/C++ 代码!
Hello from C/C++!
C: Java 代码调用完毕,返回 C/C++ 代码。
Java: C/C++ 代码执行完毕。
这个输出清晰地展示了 Java 代码如何调用 C/C++ 代码,而 C/C++ 代码又如何反过来调用 Java 的方法,完成了完整的 JNI 交互。
