- JNA 简介:它是什么,为什么用它。
- 核心概念:
Library接口、Native类、Structure类。 - 完整实践步骤:从零开始,创建一个 DLL 并用 JNA 调用它。
- 处理复杂数据类型:结构体、指针、回调函数等。
- 常用技巧和注意事项。
JNA 简介
什么是 JNA?
JNA (Java Native Access) 是一个开源的 Java 框架,由 Sun Microsystems (现 Oracle) 开发,它允许 Java 代码直接调用本地共享库(如 Windows 的 .dll、Linux 的 .so、macOS 的 .dylib)中的函数,而无需编写任何本地代码(如 C/C++)。

为什么使用 JNA?
- 简单易用:相比 JNI,JNA 极大地简化了过程,你只需要定义一个 Java 接口,JNA 会自动处理底层的 JNI 调用、参数类型转换和内存管理。
- 高性能:虽然比不上直接调用本地代码,但其性能对于大多数应用场景已经足够。
- 跨平台:JNA 本身是跨平台的,你只需要为不同平台提供对应的本地库即可。
JNA 的工作原理
JNA 的核心思想是动态代理,你定义一个 Java 接口,继承自 Library,JNA 在运行时会为这个接口创建一个动态代理实例,当你在 Java 代码中调用接口方法时,这个代理实例会负责将 Java 数据类型转换为 C 数据类型,然后通过 JNI 机制调用本地库中的对应函数,最后将返回值从 C 类型转换回 Java 类型。
核心概念
a. Library 接口
这是 JNA 的核心,你需要创建一个 Java 接口,继承自 com.sun.jna.Library,在这个接口中,声明你想要调用的 DLL 中的函数(作为 Java 方法)。
关键点:

- 函数名必须与 DLL 中的导出函数名完全一致(不区分大小写,但最好保持一致)。
- JNA 会自动处理基本数据类型的映射(如
int,double,String)。
b. Native 类
这是 JNA 的入口点,它负责加载本地库并创建 Library 接口的实例。
常用方法:
Native.load(String libraryName, Class<T> libraryInterface): 加载库并返回接口实例。libraryName: 库的名字,不带.dll或.so后缀。libraryInterface: 你定义的Library子接口。
Native.register(String libraryName, Class<T> libraryInterface): 与load类似,但会尝试在类路径中查找库文件。
c. Structure 类
当需要传递或接收 C 语言中的结构体时,就需要使用 Structure,你需要创建一个继承自 com.sun.jna.Structure 的 Java 类,并用 @FieldOrder 注解来声明结构体中字段的顺序和类型。
完整实践步骤:从零开始
我们将创建一个简单的 C++ DLL,它提供一个函数,用于计算两个整数的和,然后用 Java 和 JNA 来调用它。

步骤 1:创建 C/C++ DLL
-
环境准备:你需要一个 C++ 编译器,MinGW (GCC) 或 Visual Studio。
-
编写 C++ 代码 (
MyMathUtils.cpp)// MyMathUtils.cpp #include <windows.h> // 使用 extern "C" 来避免 C++ 的名称修饰(Name Mangling) // 这样导出的函数名就是原样的 "add_numbers" extern "C" __declspec(dllexport) int add_numbers(int a, int b) { return a + b; }__declspec(dllexport):告诉编译器这个函数需要被导出,以便其他程序(如我们的 Java 程序)可以调用。extern "C":非常重要!它告诉编译器使用 C 语言的链接规则,防止 C++ 编译器对函数名进行“修饰”(add_numbers可能会变成_add_numbers@8),导致 Java 找不到函数。
-
编译 DLL 使用 MinGW 的
g++命令进行编译:g++ -shared -o MyMathUtils.dll MyMathUtils.cpp -Wl,--kill-at
-shared: 生成共享库(DLL)。-o MyMathUtils.dll: 指定输出的 DLL 文件名。-Wl,--kill-at: 用于移除导出函数名后的 后缀,确保导出名是add_numbers。
编译成功后,你会得到
MyMathUtils.dll文件,将它放到你的 Java 项目的根目录下(或者java.library.path指定的路径中)。
步骤 2:编写 Java 代码
-
添加 JNA 依赖 如果你使用 Maven,在
pom.xml中添加:<dependency> <groupId>net.java.dev.jna</groupId> <artifactId>jna-platform</artifactId> <version>5.12.1</version> <!-- 使用最新版本 --> </dependency>如果使用 Gradle:
implementation 'net.java.dev.jna:jna-platform:5.12.1' // 使用最新版本
-
定义 Library 接口 (
MyMathUtilsLibrary.java)import com.sun.jna.Library; public interface MyMathUtilsLibrary extends Library { // 声明一个方法,对应 DLL 中的 add_numbers 函数 int add_numbers(int a, int b); } -
编写主程序调用 (
JnaDemo.java)import com.sun.jna.Native; public class JnaDemo { public static void main(String[] args) { // 1. 加载 DLL 文件 // JNA 会在 java.library.path 中查找 MyMathUtils.dll // 你也可以指定完整路径,"C:/path/to/MyMathUtils.dll" MyMathUtilsLibrary mathUtils = Native.load("MyMathUtils", MyMathUtilsLibrary.class); // 2. 调用 DLL 中的函数 int num1 = 10; int num2 = 20; int sum = mathUtils.add_numbers(num1, num2); // 3. 打印结果 System.out.println("The sum of " + num1 + " and " + num2 + " is: " + sum); } }
步骤 3:运行 Java 程序
确保 MyMathUtils.dll 和你的 Java 类文件在正确的位置,运行 JnaDemo 的 main 方法。
预期输出:
The sum of 10 and 20 is: 30
恭喜!你已经成功使用 JNA 调用了 DLL 函数。
处理复杂数据类型
现实世界中,DLL 函数的参数往往比 int 和 String 复杂。
a. 结构体
假设我们的 DLL 中有一个函数,接收一个包含 x 和 y 坐标的结构体,并返回其距离。
C++ 代码 (PointUtils.cpp)
#include <windows.h>
#include <math.h>
typedef struct {
double x;
double y;
} Point;
extern "C" __declspec(dllexport) double calculate_distance(Point p1, Point p2) {
double dx = p1.x - p2.x;
double dy = p1.y - p2.y;
return sqrt(dx * dx + dy * dy);
}
编译成 PointUtils.dll。
Java 代码
你需要创建一个 Structure 子类来映射 Point 结构体。
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Structure;
import com.sun.jna.Structure.FieldOrder;
// 1. 定义结构体
@FieldOrder({"x", "y"})
public static class Point extends Structure {
public double x;
public double y;
public Point() {}
public Point(double x, double y) {
this.x = x;
this.y = y;
}
}
// 2. 定义 Library 接口
public interface PointUtilsLibrary extends Library {
double calculate_distance(Point p1, Point p2);
}
// 3. 主程序
public class JnaStructureDemo {
public static void main(String[] args) {
PointUtilsLibrary pointUtils = Native.load("PointUtils", PointUtilsLibrary.class);
Point p1 = new Point(3.0, 4.0);
Point p2 = new Point(7.0, 1.0);
double distance = pointUtils.calculate_distance(p1, p2);
System.out.println("The distance between the points is: " + distance); // 预期输出: 5.0
}
}
b. 指针 和 Pointer 类型
当 DLL 函数需要接收一个指向缓冲区的指针时(用于写入结果),JNA 提供了 Pointer 类型。
示例:一个函数,接收一个整数指针,并将结果写入其中。 C++ 代码
extern "C" __declspec(dllexport) void get_square(int number, int* result_ptr) {
*result_ptr = number * number;
}
Java 调用
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
public interface SquareLibrary extends Library {
void get_square(int number, Pointer result_ptr);
}
public class JnaPointerDemo {
public static void main(String[] args) {
SquareLibrary squareLib = Native.load("SquareLibrary", SquareLibrary.class);
// 创建一个指向足够大内存的 Pointer
Pointer resultPointer = new Pointer(Native.malloc(4)); // 分配 4 字节(int 大小)
squareLib.get_square(10, resultPointer);
// 从 Pointer 中读取 int 值
int squareValue = resultPointer.getInt(0);
System.out.println("The square of 10 is: " + squareValue); // 预期输出: 100
// 释放内存
Native.free(resultPointer.getPointer());
}
}
c. 回调函数
回调函数(DLL 调用 Java 方法)稍微复杂一些,JNA 通过 Callback 接口实现。
示例:一个 DLL 函数,遍历数组,并对每个元素调用一个回调函数。 C++ 代码
#include <windows.h>
// 定义回调函数指针类型
typedef void (*ArrayCallback)(int element);
extern "C" __declspec(dllexport) void process_array(int* array, int size, ArrayCallback callback) {
for (int i = 0; i < size; ++i) {
callback(array[i]);
}
}
Java 调用
import com.sun.jna.Callback;
import com.sun.jna.Library;
import com.sun.jna.Native;
// 1. 定义回调接口
public interface ArrayCallback extends Callback {
void apply(int element);
}
// 2. 定义 Library 接口
public interface ArrayProcessorLibrary extends Library {
void process_array(int[] array, int size, ArrayCallback callback);
}
// 3. 主程序
public class JnaCallbackDemo {
public static void main(String[] args) {
ArrayProcessorLibrary processor = Native.load("ArrayProcessor", ArrayProcessorLibrary.class);
int[] numbers = {1, 2, 3, 4, 5};
// 创建回调实例
ArrayCallback callback = new ArrayCallback() {
@Override
public void apply(int element) {
System.out.println("Processing element: " + element * 2); // 对每个元素进行操作
}
};
System.out.println("Starting array processing...");
processor.process_array(numbers, numbers.length, callback);
System.out.println("Array processing finished.");
}
}
预期输出:
Starting array processing...
Processing element: 2
Processing element: 4
Processing element: 6
Processing element: 8
Processing element: 10
Array processing finished.
常用技巧和注意事项
-
java.library.path:- JNA 默认会从
java.library.path系统属性中查找 DLL,这个路径通常包括:- JVM 启动目录 (
-Djava.library.path=.)。 System32目录。- JDK/JRE 的
bin目录。
- JVM 启动目录 (
- 如果你的 DLL 不在标准路径中,可以通过
-Djava.library.path参数指定:java -Djava.library.path="C:/my/dll/path" -jar myapp.jar
- JNA 默认会从
-
数据类型映射表: 了解 JNA 如何映射 C/Java 类型非常重要。 | C 类型 | JNA 映射类型 | 备注 | | :--- | :--- | :--- | |
char|byte| | |short|short| | |int|int| | |long|com.sun.jna.NativeLong| 在 32/64 位系统上大小可能不同,NativeLong会自动适配。 | |long long|long| | |float|float| | |double|double| | |char*|String| JNA 会自动处理内存分配和释放。 | |void*|Pointer| 通用指针类型。 | |HANDLE|Pointer或WinNT.HANDLE| Windows 句柄通常用Pointer表示。 | -
内存管理:
- 对于 JNA 自动分配的内存(如
String参数),你不需要手动释放。 - 对于通过
Native.malloc()或Pointer接收到的指针,你必须手动调用Native.free()来释放内存,否则会导致内存泄漏。
- 对于 JNA 自动分配的内存(如
-
错误处理:
- 如果找不到 DLL 或函数,JNA 会抛出
UnsatisfiedLinkError,建议使用try-catch包裹Native.load调用,以便友好地处理错误。 - DLL 内部的错误(如除以零)可能会导致 JVM 崩溃,编写健壮的本地代码至关重要。
- 如果找不到 DLL 或函数,JNA 会抛出
-
查看 JNA 日志: JNA 提供了详细的日志功能,对于调试非常有用,你可以通过设置系统属性来开启:
java -Djna.debug_load=true -Djna.debug_caching=true ...
这会打印出库加载、函数查找等详细信息。
-
使用 JNA Platform:
jna-platform依赖包不仅包含了jna,还包含了许多针对特定平台(如 Windows)的接口定义(如Kernel32,User32等),当你需要调用 Windows API 时,直接继承这些接口即可,非常方便。
