2. javac -h生成JNI头文件并实现本地方法
从 Java 声明到 C 实现:javac -h 的自动化链路
例1中我们手写了 JNI 头文件,但真实项目中本地方法往往有几十个,手写签名极易出错。JDK 8 之后提供了 javac -h 选项,可以在编译 Java 源文件的同时,自动扫描所有 native 方法声明,生成符合 JNI 规范的头文件。本节用一个 MathTool 类演示完整链路:Java 声明 → javac -h 生成头文件 → 手动实现 C 函数 → 编译为动态库 → 运行验证。
1. 编写带 native 声明的 Java 类
创建目录结构 src/com/example/jni/,在其中新建 MathTool.java:
package com.example.jni;
public class MathTool {
// 声明两个本地方法,暂不提供 Java 实现
public native int add(int a, int b);
public native int factorial(int n);
public static void main(String[] args) {
MathTool tool = new MathTool();
System.out.println("add(3, 5) = " + tool.add(3, 5));
System.out.println("factorial(6) = " + tool.factorial(6));
}
}
注意:native 方法只有声明、没有方法体,以分号结尾。类所在的包名 com.example.jni 会直接影响生成的头文件名和函数名。
2. 用 javac -h 生成头文件
在项目根目录执行:
javac -h include src/com/example/jni/MathTool.java
参数说明:-h include 指定头文件输出目录为 include/(目录不存在会自动创建);后面是 Java 源文件路径。编译成功后,include/ 下会生成 com_example_jni_MathTool.h,同时 src/com/example/jni/ 下生成 MathTool.class。
如果提示
javac: invalid flag: -h,说明 JDK 版本低于 8,需要升级 JDK 或改用 javah(已废弃)。3. 解读生成的头文件
打开 include/com_example_jni_MathTool.h,核心内容如下(已省略版权注释):
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_jni_MathTool */
#ifndef _Included_com_example_jni_MathTool
#define _Included_com_example_jni_MathTool
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_jni_MathTool
* Method: add
* Signature: (II)I
*/
JNIEXPORT jint JNICALL Java_com_example_jni_MathTool_add
(JNIEnv *, jobject, jint, jint);
/*
* Class: com_example_jni_MathTool
* Method: factorial
* Signature: (I)I
*/
JNIEXPORT jint JNICALL Java_com_example_jni_MathTool_factorial
(JNIEnv *, jobject, jint);
#ifdef __cplusplus
}
#endif
#endif
这里有三条必须理解的命名与参数规则:
- 函数名规则:
Java_+ 完整包名(点替换为下划线)+_+ 方法名。所以com.example.jni.MathTool.add变成Java_com_example_jni_MathTool_add。若包名或类名本身含下划线,JNI 规定用_1转义,避免歧义。 - 前两个固定参数:
JNIEnv *是 JNI 环境指针,提供所有 JNI 函数入口;jobject指向调用该方法的 Java 对象实例(本例中即tool)。若是static native方法,第二个参数会变成jclass。 - 类型映射:Java 的
int映射为jint,返回值也是jint。括号中的(II)I是方法签名,表示接收两个 int、返回一个 int。
4. 手动实现本地方法
新建 native/mathtool.c,包含生成的头文件并实现两个函数:
#include "com_example_jni_MathTool.h"
JNIEXPORT jint JNICALL Java_com_example_jni_MathTool_add
(JNIEnv *env, jobject thisObj, jint a, jint b) {
return a + b;
}
JNIEXPORT jint JNICALL Java_com_example_jni_MathTool_factorial
(JNIEnv *env, jobject thisObj, jint n) {
if (n < 0) return -1;
jint result = 1;
for (jint i = 2; i <= n; i++) {
result *= i;
}
return result;
}
函数签名必须与头文件完全一致,否则编译能过但运行时 UnsatisfiedLinkError。参数名可以自定义,但类型和顺序不能改。
5. 编译动态库并运行
Linux 下编译(假设 JAVA_HOME 已设置):
gcc -shared -fPIC \
-I"$JAVA_HOME/include" \
-I"$JAVA_HOME/include/linux" \
-Iinclude \
-o libmathtool.so native/mathtool.c
Windows 下用 MinGW:
gcc -shared -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -Iinclude -o mathtool.dll native/mathtool.c
运行 Java 程序,通过 -Djava.library.path 指定动态库所在目录:
java -Djava.library.path=. -cp src com.example.jni.MathTool
预期输出:
add(3, 5) = 8
factorial(6) = 720
常见错误:
UnsatisfiedLinkError: no mathtool in java.library.path 表示库文件未找到或命名不对(Linux 需 lib 前缀,Windows 需 .dll 后缀);UnsatisfiedLinkError: Java_com_example_jni_MathTool_add 表示函数名拼写与头文件不一致。6. 验证与排查清单
| 检查项 | 正确状态 | 错误表现 |
|---|---|---|
| 头文件是否生成 | include/com_example_jni_MathTool.h 存在 | 目录为空,javac 报错 |
| 函数名是否匹配 | C 函数名与头文件逐字符一致 | 运行时 UnsatisfiedLinkError 带方法名 |
| 动态库命名 | Linux: libmathtool.so;Windows: mathtool.dll | no xxx in java.library.path |
| 库路径 | -Djava.library.path 指向库所在目录 | 同上 |
| 包名一致性 | Java 包名与函数名中的包路径一致 | 函数名不匹配 |
核心收获:
javac -h 把「Java 声明 → JNI 函数签名」这一步自动化,你只需关注 C 侧的业务逻辑实现。命名规则 Java_包名_类名_方法名 和固定前两个参数 (JNIEnv*, jobject) 是 JNI 调用约定的基石,后续所有类型转换、字段访问都建立在这个签名框架之上。