2. javac -h生成JNI头文件并实现本地方法

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。
MathTool.java package com.example.jni; public class MathTool { native int add(int,int); native int factorial(int); } 仅声明,无方法体 javac -h include src/.../MathTool.java 编译 + 扫描 native 声明 com_example_jni_MathTool.h JNIEXPORT jint JNICALL Java_com_example_jni_ MathTool_add (JNIEnv*, jobject, jint, jint); ... factorial 同理 JNI 函数命名规则拆解 Java_ + com_example_jni + MathTool + add → 包名点变下划线 参数映射关系 JNIEnv *env jobject thisObj jint a jint b → 返回 jint

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.dllno xxx in java.library.path
库路径-Djava.library.path 指向库所在目录同上
包名一致性Java 包名与函数名中的包路径一致函数名不匹配
核心收获:javac -h 把「Java 声明 → JNI 函数签名」这一步自动化,你只需关注 C 侧的业务逻辑实现。命名规则 Java_包名_类名_方法名 和固定前两个参数 (JNIEnv*, jobject) 是 JNI 调用约定的基石,后续所有类型转换、字段访问都建立在这个签名框架之上。