什么是JNI
Java原生接口(Java Native Interface,简称JNI)是一套强大的编程框架,它允许运行在Java虚拟机(JVM)中的Java代码与用其他编程语言(如C、C++和汇编)编写的原生代码进行交互。JNI是Java平台的重要组成部分,为开发者在需要突破纯Java环境限制时提供了必要的桥梁。
JNI 的核心价值与应用场景
在深入技术细节之前,理解为何需要JNI至关重要。其核心价值在于赋予Java程序更广泛的能力,主要体现在以下几个方面:
- 性能提升: 对于计算密集型任务,如图像处理、音视频编解码、复杂的数学运算等,使用C/C++等原生代码可以获得比纯Java实现更高的执行效率。
- 复用现有代码库: 无需用Java重写已经存在的、久经考验的C/C++库,通过JNI可以直接在Java应用中调用这些原生库,节省开发时间和成本。
- 访问特定于平台的功能: Java以其“一次编写,到处运行”的跨平台性著称,但也因此牺牲了对底层操作系统和硬件特定功能的直接访问能力。JNI允许Java程序调用原生API,从而实现与特定平台的深度集成。
- 与底层硬件交互: 在开发需要直接控制硬件设备的应用程序时,例如读卡器、打印机等,通常需要通过原生代码来实现,JNI则负责连接Java应用层与这些原生驱动。
JNI 工作流程概览
JNI的交互是双向的,既可以从Java调用原生方法,也可以从原生代码中回调Java方法。一个典型的从Java调用原生代码的开发流程如下:
- Java层:声明原生方法
在Java类中使用native关键字声明一个或多个方法,这些方法没有方法体。同时,通过System.loadLibrary("库名")静态代码块来加载即将实现这些原生方法的动态链接库(在Windows上是.dll文件,在Linux/macOS上是.so文件)。 - 编译Java代码并生成头文件
使用javac命令编译包含原生方法的Java类。然后,利用JDK提供的javah工具(或在较新版本的JDK中使用javac -h)根据编译后的.class文件生成一个C/C++头文件(.h文件)。这个头文件定义了与Java中native方法相对应的C/C++函数原型。 - 原生层:实现原生方法
创建一个C或C++源文件,包含上一步生成的头文件,并根据其定义的函数原型编写具体的实现。这些原生函数会接收一个JNIEnv*指针和一个指向调用该原生方法的Java对象的jobject(或jclass对于静态方法)作为参数。 - 编译原生代码生成动态链接库
使用C/C++编译器(如GCC、Clang或Visual C++)将原生代码编译成一个动态链接库。编译时需要链接JNI相关的头文件和库,这些通常位于JDK的安装目录中。 - 运行Java程序
执行Java程序。当调用到native方法时,JVM会通过之前加载的动态链接库,找到并执行对应的原生函数。
动手实现一个JNI
环境配置(VScode为例)
-
添加JDK的include路径到VS Code配置
-
在VS Code中打开
c_cpp_properties.json文件:- 按
Ctrl+Shift+P打开命令面板 - 输入 "C/C++: Edit Configurations (UI)" 或直接编辑
.vscode/c_cpp_properties.json
- 按
{
"configurations": [
{
"name": "Win32",
"includePath": [
"${workspaceFolder}/**",
"C:/Program Files/Java/jdk-21/include", // 修改为你的JDK路径
"C:/Program Files/Java/jdk-21/include/win32" // Windows平台需要这个
],
//如果你配置了JAVA_HOME,上面代码可以替换成
"includePath": [
"${workspaceFolder}/**",
"${env:JAVA_HOME}/include",
"${env:JAVA_HOME}/include/win32" // Linux用linux, macOS用darwin
]
"defines": [],
"compilerPath": "C:/mingw64/bin/g++.exe"
}
],
"version": 4
}
如果不生效
可以试试这样
开始实操
- 准备一个空的工作文件夹,创建一个java文件
public class MyJNIDemo {
static {
// Load native library
System.loadLibrary("myjnidemo");
}
// Native method declaration
public native String sayHello(String name, int age);
public static void main(String[] args) {
MyJNIDemo myJNIDemo = new MyJNIDemo();
String result = myJNIDemo.sayHello("World", 25);
System.out.println(result);
}
}
运行命令,编译 Java 代码并生成头文件 javac -h . MyJNIDemo.java会得到两个同名文件MyJNIDemo.class``MyJNIDemo.h (注意注释不能出现中文,否则编译不会通过)
- 准备C++工具文件(文件命名和Load native library这里设置的名字保持一致)
#include<jni.h>
#include<string>
#include"MyJNIDemo.h"
JNIEXPORT jstring JNICALL Java_MyJNIDemo_sayHello(JNIEnv *env, jobject obj, jstring name, jint age){
const char *nameStr = env->GetStringUTFChars(name, nullptr);
std::string cppName(nameStr);
env->ReleaseStringUTFChars(name, nameStr);
std::string greeting = "Hello, " + cppName + "! You are " + std::to_string(age) + " years old.";
return env->NewStringUTF(greeting.c_str());
}
同样的不要出现中文注释
运行命令g++ -I"%JAVA_HOME%\\include" -I"%JAVA_HOME%\\include\\win32" -shared -o myjnidemo.dll myjnidemo.cpp 将CPP文件编译成dll文件。(Linux/mac是.so)
3.最后运行java MyJNIDemo
补充
这行代码是JNI中原生(C/C++)代码的核心部分,它是一个遵循JNI规范的函数实现,用于响应来自Java代码的native方法调用。
C++
JNIEXPORT jstring JNICALL Java_MyJNIDemo_sayHello(JNIEnv *env, jobject obj, jstring name, jint age);
我们把它拆解成几个关键部分来理解:
1. JNIEXPORT 和 JNICALL
这两者是定义在 jni.h 头文件中的宏。它们处理的是编译器和操作系统的底层细节,以确保Java虚拟机(JVM)能够正确地找到并调用这个函数。
JNIEXPORT: 这个宏负责函数可见性。它将该函数标记为从动态链接库(在Windows上是.dll,在Linux/macOS上是.so)中“导出”的函数。这样,当JVM加载这个库时,它就能在库中找到这个函数。在不同的平台和编译器上,它会被展开为不同的关键字,例如在Windows上是__declspec(dllexport)。JNICALL: 这个宏负责调用约定(Calling Convention)。调用约定定义了函数参数如何传递(例如通过栈还是寄存器)以及调用结束后谁负责清理栈。确保原生函数和JVM使用相同的调用约定是至关重要的,否则会导致程序崩溃。在 x86 架构的 Windows 系统上,这通常对应于_stdcall。
简单记法:你几乎总需要把这两个宏按顺序写在函数声明的开头,它们是JNI函数的“标准前缀”。
2. 返回类型:jstring
这是函数的返回类型。jstring 是JNI定义的数据类型,它在原生代码中代表了Java世界的 java.lang.String 对象。
-
关键点:
jstring不是 C/C++ 中的char*或std::string。它是一个指向JVM内部Java对象的不透明引用。你不能直接对它进行字符串操作。 -
如何使用:你必须通过
JNIEnv指针提供的专用函数来处理它。- 要从
jstring获取C风格的字符串,使用env->GetStringUTFChars(...)。 - 要在C++代码中创建一个新的
jstring以返回给Java,使用env->NewStringUTF(...)。
- 要从
3. 函数名:Java_MyJNIDemo_sayHello
这是JNI中最重要的命名约定,JVM就是通过这个精确的名字来找到与Java native方法对应的C/C++函数。这个命名规则被称为“静态注册”。
它的构成规则如下:
-
Java_: 固定前缀。 -
MyJNIDemo: Java类的完整限定名(Fully Qualified Class Name),其中包名中的点.被替换为下划线_。- 如果你的类是
MyJNIDemo且没有包,就是MyJNIDemo。 - 如果你的类在包
com.example中,即package com.example; class MyJNIDemo {...},那么这部分就是com_example_MyJNIDemo。
- 如果你的类是
-
_: 分隔符。 -
sayHello: Java代码中声明的native方法的名称。- 如果Java方法名中本身包含下划线
_,则会被转义为_1。
- 如果Java方法名中本身包含下划线
所以,这个函数名 Java_MyJNIDemo_sayHello 明确地告诉我们,它对应的是 MyJNIDemo 类中的一个名为 sayHello 的 native 方法。
4. 函数参数列表
参数列表总是以两个固定的参数开始,后面跟着与Java native方法相对应的参数。
固定参数
-
JNIEnv *env: 这是JNI环境的指针,是最重要的一个参数。- 它是一个指向线程本地(thread-local)结构的指针,包含了几乎所有JNI函数。
- 你可以把它看作是原生代码与JVM进行交互的“桥梁”或“总接口”。所有对Java对象的操作,如创建对象、调用Java方法、访问字段、处理字符串和数组等,都必须通过
env指针来完成。 - 注意:
JNIEnv指针是线程独有的,不能在多个线程之间共享。
-
jobject obj: 指向调用这个native方法的Java对象实例的引用。- 它相当于Java中的
this关键字。 - 如果你需要在这个原生方法中访问调用它的那个Java对象的非静态字段,或者回调它的非静态方法,你就会用到这个
obj。 - 特例:如果Java中的
native方法被声明为static,那么这里的第二个参数会变成jclass clazz,它代表的是类本身(一个java.lang.Class对象),而不是一个实例。
- 它相当于Java中的
映射参数
这两个固定参数之后,是与Java native方法声明的参数一一对应的部分。
jstring name: 对应Java方法中的String name参数。jint age: 对应Java方法中的int age参数。
JNI为Java的每一种基本类型和引用类型都定义了相应的原生类型(如jint, jfloat, jboolean, jobjectArray等)。
总结
将以上所有部分组合起来,这行声明完整地描述了一个JNI函数的契约: