一、简介
Java Native Interface (JNI) 是Java提供的一种框架,允许Java代码和其他编程语言(如C、C++)编写的代码进行交互。JNI使得Java程序能够调用本地方法,并且允许本地代码调用Java方法。它是实现Java与本地系统或库交互的重要工具。
1. 基本概念
- JNI的角色:JNI作为Java虚拟机(JVM)和本地代码之间的桥梁,使得Java可以利用已有的本地库或操作系统功能。
- 应用场景 :
- 性能优化:在需要高性能的计算或特定算法时,使用本地代码实现更高效的操作。
- 系统功能访问:访问底层操作系统功能或硬件设备,这些是Java无法直接做到的。
- 复用现有代码:调用已有的本地代码库或集成第三方库。
2. JNI的核心概念
- 本地方法 :用
native
关键字声明的方法,这些方法的实现是用非Java语言编写的。 - 本地方法库 :包含本地方法实现的动态库文件,如
.dll
(Windows)、.so
(Linux/Unix)、.dylib
(macOS)。 - JNI头文件 :通过
javah
工具生成的C/C++头文件,定义了Java类和本地方法之间的接口。
3. JNI的基本步骤
-
声明本地方法 :
在Java类中声明一个或多个本地方法。例如:
javapublic class NativeExample { public native void nativeMethod(); static { System.loadLibrary("nativeexample"); } }
-
生成头文件 :
使用
javac
编译Java代码后,利用javah
工具生成C/C++头文件。或者在Java 8及之后,可以直接使用javac -h
命令:shjavac NativeExample.java javac -h . NativeExample.java
-
实现本地方法 :
在C或C++中实现本地方法,代码示例:
c#include <jni.h> #include "NativeExample.h" // 通过javah生成的头文件 JNIEXPORT void JNICALL Java_NativeExample_nativeMethod(JNIEnv *env, jobject obj) { printf("This is a native method implementation.\n"); }
-
编译和链接 :
编译本地方法实现为动态库。例如:
shgcc -shared -o libnativeexample.so -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux NativeExample.c
-
加载本地库 :
在Java代码中通过
System.loadLibrary
加载动态库,以便调用本地方法。
4. JNI的主要功能
-
方法调用:
- 调用本地方法:从Java调用C/C++编写的本地方法。
- 从本地代码调用Java方法:在本地代码中调用Java方法。
-
数据交换:
- 基本数据类型 :如
int
,float
,double
等可以直接在Java和本地代码之间传递。 - 对象和数组:可以在Java和本地代码之间传递Java对象和数组。
- 基本数据类型 :如
-
异常处理:
- 抛出和处理异常:本地代码可以通过JNI接口抛出异常,也可以检查Java中发生的异常。
-
对象操作:
- 创建和访问Java对象:在本地代码中创建Java对象并访问其字段和方法。
5. JNI的关键函数
-
获取Java环境:
cJNIEnv *env;
-
访问类和方法:
-
FindClass
:查找Java类。cjclass cls = (*env)->FindClass(env, "com/example/NativeExample");
-
GetMethodID
:获取方法ID。cjmethodID mid = (*env)->GetMethodID(env, cls, "methodName", "()V");
-
CallVoidMethod
:调用Java方法。c(*env)->CallVoidMethod(env, obj, mid);
-
-
操作Java字符串和数组:
-
GetStringUTFChars
:将Java字符串转换为C字符串。cconst char *nativeString = (*env)->GetStringUTFChars(env, javaString, NULL);
-
NewObjectArray
:创建Java数组。cjobjectArray array = (*env)->NewObjectArray(env, size, cls, initObject);
-
6. JNI的优缺点
-
优点:
- 性能:可以利用本地代码进行高效处理。
- 功能扩展:可以访问Java无法直接调用的系统级功能。
- 复用代码:可以利用现有的本地库或系统资源。
-
缺点:
- 复杂性:涉及到不同编程语言的交互,编写和调试相对复杂。
- 平台依赖:本地方法的实现通常与平台相关,可能导致跨平台兼容性问题。
- 安全性:本地代码可能带来安全隐患,如果实现不当,可能会影响JVM的稳定性或安全性。
7. 示例
Java类:
java
public class HelloWorld {
public native void sayHello();
static {
System.loadLibrary("hello");
}
}
C实现:
c
#include <jni.h>
#include <stdio.h>
#include "HelloWorld.h"
JNIEXPORT void JNICALL Java_HelloWorld_sayHello(JNIEnv *env, jobject obj) {
printf("Hello from native code!\n");
}
编译和运行:
-
编译Java类:
shjavac HelloWorld.java
-
生成头文件:
shjavah HelloWorld
-
编译本地代码生成动态库:
shgcc -shared -o libhello.so -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux HelloWorld.c
-
运行Java程序:
shjava HelloWorld
JNI是Java与本地代码交互的强大工具,但使用时需要仔细管理内存和处理跨语言调用的复杂性。
二、完整代码示例
下面是一个完整的JNI示例,展示了如何在Java中声明本地方法,如何用C语言实现这些方法,以及如何编译和运行这段代码。这个示例将实现一个简单的本地方法,该方法将打印一条消息到控制台。
1. Java代码
首先,我们编写一个Java类,声明一个本地方法,并在静态块中加载本地库。
HelloWorld.java:
java
public class HelloWorld {
// 声明本地方法
public native void sayHello();
// 加载本地库
static {
System.loadLibrary("hello");
}
public static void main(String[] args) {
HelloWorld helloWorld = new HelloWorld();
helloWorld.sayHello(); // 调用本地方法
}
}
2. 生成JNI头文件
编译Java代码,并使用javah
(或者在Java 8及以后的版本中使用javac -h
)生成JNI头文件。
步骤:
sh
# 编译Java类
javac HelloWorld.java
# 生成JNI头文件(使用javah或javac -h)
javac -h . HelloWorld.java
生成的头文件HelloWorld.h将包含JNI接口的C声明。
3. C实现
根据生成的头文件编写C语言的实现。
HelloWorld.c:
c
#include <jni.h>
#include <stdio.h>
#include "HelloWorld.h" // 由javah生成的头文件
// 实现本地方法
JNIEXPORT void JNICALL Java_HelloWorld_sayHello(JNIEnv *env, jobject obj) {
printf("Hello from native code!\n");
}
4. 编译和链接本地代码
将C代码编译成动态库。不同平台的编译命令有所不同。
Linux/Unix:
sh
# 编译生成共享库(.so文件)
gcc -shared -o libhello.so -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux HelloWorld.c
Windows:
sh
# 编译生成动态库(.dll文件)
gcc -shared -o hello.dll -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" HelloWorld.c
macOS:
sh
# 编译生成共享库(.dylib文件)
gcc -shared -o libhello.dylib -I${JAVA_HOME}/include -I${JAVA_HOME}/include/darwin HelloWorld.c
5. 运行Java程序
确保动态库文件位于Java的库路径中,然后运行Java程序。
步骤:
sh
# 设置库路径
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH # Linux/Unix
set PATH=%PATH%;. # Windows
export DYLD_LIBRARY_PATH=.:$DYLD_LIBRARY_PATH # macOS
# 运行Java程序
java HelloWorld
6. 完整目录结构
假设您的目录结构如下:
/path/to/project/
HelloWorld.java
HelloWorld.h (由javah生成)
HelloWorld.c
libhello.so (生成的动态库)
7. 注意事项
- 路径:确保动态库文件的路径被正确设置,以便Java虚拟机可以找到它。
- 头文件 :
HelloWorld.h
文件由javah
或javac -h
自动生成,包含JNI函数的声明。 - JNI函数命名 :JNI函数的名称必须符合约定,即
Java_<包名>_<类名>_<方法名>
的格式。
8. 完整代码
HelloWorld.java:
java
public class HelloWorld {
public native void sayHello();
static {
System.loadLibrary("hello");
}
public static void main(String[] args) {
HelloWorld helloWorld = new HelloWorld();
helloWorld.sayHello();
}
}
HelloWorld.h(自动生成的头文件):
c
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloWorld */
#ifndef _Included_HelloWorld
#define _Included_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: HelloWorld
* Method: sayHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_HelloWorld_sayHello
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
HelloWorld.c:
c
#include <jni.h>
#include <stdio.h>
#include "HelloWorld.h"
JNIEXPORT void JNICALL Java_HelloWorld_sayHello(JNIEnv *env, jobject obj) {
printf("Hello from native code!\n");
}
编译命令(Linux/Unix 示例):
sh
javac HelloWorld.java
javac -h HelloWorld.java
gcc -shared -o libhello.so -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux HelloWorld.c
java HelloWorld
这样,通过JNI,你可以将Java程序与本地系统功能进行交互,实现高效的计算或系统功能访问。