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


原始标题:Guide to JNI (Java Native Interface)