Java调用C++动态库实战:JNI原理、环境配置与跨平台编译指南
1. 项目概述:为什么Java需要调用C++动态库?
在Java开发中,我们常常会遇到一些性能瓶颈,或者需要复用一些成熟、高效的C/C++遗留代码库。比如,处理复杂的图像算法、进行高精度的科学计算、调用操作系统底层API,或者对接某些硬件设备的驱动。在这些场景下,纯Java实现要么效率不够,要么根本无从下手。这时,Java调用本地代码(Native Code)的能力就显得至关重要,而通过动态链接库(Dynamic Link Library,在Windows上是.dll,在Linux/Unix上是.so,在macOS上是.dylib)来调用C++代码,是最主流、最灵活的方式。
这个过程的核心是JNI(Java Native Interface)。你可以把它想象成Java世界和C/C++世界之间的一座“桥梁”和“翻译官”。Java代码通过JNI声明一个本地方法,编译时会生成一个包含此方法声明的C/C++头文件;然后,我们用C++按照这个头文件的“接口规范”实现具体的功能,并编译成动态库;最后,Java程序在运行时加载这个动态库,就能调用其中的C++函数了。听起来流程清晰,但实际操作中,从环境配置、编译选项到内存管理,每一步都有不少“坑”。
我最近在做一个音视频处理项目,核心的编解码算法是用C++写的,性能经过了极致优化。为了在Java Web服务中集成这个算法,我完整走了一遍Java调用C++动态库的流程,把环境搭建、代码编写、编译调试和部署中遇到的所有问题都梳理了一遍。这篇文章,我就以一个实际的“计算器”示例(提供加减乘除功能),带你从零开始,手把手完成整个调用过程,并附上可运行的源码。无论你是需要集成特定C++库的Java工程师,还是对JNI机制感兴趣的学习者,这篇详尽的指南都能让你避开我踩过的那些坑。
2. 环境准备与工具链选型
工欲善其事,必先利其器。在开始编码之前,搭建一个正确、高效的环境是成功的一半。不同操作系统下的工具链差异很大,我们需要分别准备。
2.1 Java开发环境配置
Java端相对简单,确保你安装了JDK(Java Development Kit),而不仅仅是JRE(Java Runtime Environment)。我们需要javac编译器和javah工具(在较新版本的JDK中,javah的功能已集成到javac中)。
- JDK版本:推荐使用JDK 8或11这些长期支持版本,它们拥有最广泛的兼容性。我使用的是JDK 11。你可以通过终端命令
java -version和javac -version来验证。 - IDE选择:IntelliJ IDEA或Eclipse都可以。IDE能帮我们管理项目结构,但核心的编译命令我们更多会依赖命令行,以确保过程清晰可控。
2.2 C++编译环境配置
这是关键且容易出错的一步,我们需要一个能够生成与JVM兼容的动态库的C++编译器。
Windows平台:
- 首选MSVC:安装Visual Studio(社区版即可)。在安装时,务必勾选“使用C++的桌面开发”工作负载。这将安装MSVC编译器(
cl.exe)和必要的Windows SDK。完成后,你需要使用“Developer Command Prompt for VS”或“x64 Native Tools Command Prompt”来打开命令行,这样环境变量才会被正确设置,能够直接找到cl.exe。 - 为什么不用MinGW?MinGW(GCC for Windows)也可以,但编译出的动态库可能在某些复杂的JNI场景下与MSVC编译的JVM存在微妙的ABI(应用二进制接口)兼容性问题。为了最大程度的稳定和兼容,在Windows上配合Oracle JDK(其HotSpot JVM本身是用MSVC编译的),强烈建议使用MSVC。
- 首选MSVC:安装Visual Studio(社区版即可)。在安装时,务必勾选“使用C++的桌面开发”工作负载。这将安装MSVC编译器(
Linux/macOS平台:
- GCC/Clang:系统通常自带。在Ubuntu/Debian上,可以通过
sudo apt-get install build-essential安装GCC全套工具。在macOS上,安装Xcode Command Line Tools即可获得Clang。
- GCC/Clang:系统通常自带。在Ubuntu/Debian上,可以通过
2.3 辅助工具
- 文本编辑器/IDE:用于编写C++代码。Visual Studio Code(配合C++插件)、Visual Studio、CLion都是优秀的选择。VSCode轻量灵活,适合本项目。
- 依赖管理:如果C++项目本身依赖第三方库(如OpenCV、FFmpeg),你需要提前准备好这些库的开发文件(头文件和链接库),并在编译时正确指定包含路径和链接路径。
注意:环境变量的配置,特别是
PATH、INCLUDE、LIB(Windows)或CPATH、LIBRARY_PATH(Linux/macOS),是编译能否成功的关键。在Windows的VS命令提示符下,这些都已自动配置好。在Linux/macOS下,如果自定义了安装路径,可能需要手动配置。
3. 核心步骤拆解:从Java声明到C++实现
整个流程可以概括为“四步走”:Java声明、生成头文件、C++实现、编译动态库。下面我们用一个简单的NativeCalculator类来演示。
3.1 第一步:编写Java Native方法声明
首先,我们创建一个Java类,使用native关键字声明我们需要用C++实现的方法。
// 文件:NativeCalculator.java public class NativeCalculator { // 加载动态库。库名“NativeCalculator”对应后续编译出的 NativeCalculator.dll(Windows)或 libNativeCalculator.so(Linux/macOS) static { System.loadLibrary("NativeCalculator"); } // 声明四个本地方法 public native int add(int a, int b); public native int subtract(int a, int b); public native int multiply(int a, int b); public native double divide(double a, double b); // 主函数,用于测试 public static void main(String[] args) { NativeCalculator calc = new NativeCalculator(); System.out.println("5 + 3 = " + calc.add(5, 3)); System.out.println("5 - 3 = " + calc.subtract(5, 3)); System.out.println("5 * 3 = " + calc.multiply(5, 3)); System.out.println("5.0 / 3.0 = " + calc.divide(5.0, 3.0)); } }关键点解析:
System.loadLibrary(“NativeCalculator”):这行代码告诉JVM在运行时加载名为“NativeCalculator”的动态库。查找路径遵循系统库路径规则(如java.library.path系统属性指定的路径)。native关键字:表明该方法的具体实现不在当前的Java代码中,而是在一个本地库中。- 方法签名:
(II)I、(DD)D等,这些是JNI类型签名,用于在Java和C++之间精确匹配数据类型。I代表int,D代表double。
3.2 第二步:生成JNI头文件
JNI头文件是一个C/C++头文件,它定义了Java类中声明的本地方法所对应的C函数原型。这个文件是连接Java和C++的“契约”。
在项目根目录(NativeCalculator.java所在目录)打开命令行,执行:
javac -h . NativeCalculator.java这个命令做了两件事:
javac NativeCalculator.java:编译Java源文件,生成NativeCalculator.class。-h .:在当前目录(.)下生成JNI头文件。
执行后,你会看到新生成了一个名为NativeCalculator.h的文件。内容大致如下:
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class NativeCalculator */ #ifndef _Included_NativeCalculator #define _Included_NativeCalculator #ifdef __cplusplus extern "C" { #endif /* * Class: NativeCalculator * Method: add * Signature: (II)I */ JNIEXPORT jint JNICALL Java_NativeCalculator_add (JNIEnv *, jobject, jint, jint); /* * Class: NativeCalculator * Method: subtract * Signature: (II)I */ JNIEXPORT jint JNICALL Java_NativeCalculator_subtract (JNIEnv *, jobject, jint, jint); /* * Class: NativeCalculator * Method: multiply * Signature: (II)I */ JNIEXPORT jint JNICALL Java_NativeCalculator_multiply (JNIEnv *, jobject, jint, jint); /* * Class: NativeCalculator * Method: divide * Signature: (DD)D */ JNIEXPORT jdouble JNICALL Java_NativeCalculator_divide (JNIEnv *, jobject, jdouble, jdouble); #ifdef __cplusplus } #endif #endif头文件解读:
#include <jni.h>:引入了JNI的核心类型和函数定义,这是必须的。extern “C”:用C语言链接规范来声明函数,确保C++编译器不会对函数名进行修饰(Name Mangling),这样JVM才能根据固定的函数名找到它们。JNIEXPORT和JNICALL:这是两个宏,用于指定函数的调用约定和导出属性,确保函数能被JVM正确调用。- 函数名:
Java_NativeCalculator_add。JNI函数名有严格的格式:Java_<完整类名>_<方法名>。类名中的点(.)被替换为下划线(_)。 - 参数:每个JNI函数至少有两个固定参数。
JNIEnv* env:指向JNI环境的指针,提供了所有JNI函数。通过它,C++代码可以访问Java对象、调用Java方法、操作Java数组等。jobject obj或jclass clazz:如果本地方法是实例方法(非static),第二个参数是jobject,代表调用该方法的Java对象实例(本例中的calc)。如果是静态方法,则是jclass,代表调用该方法的Java类。- 后续参数:对应Java方法的参数,但使用的是JNI类型(如
jint,jdouble)。
3.3 第三步:编写C++实现文件
现在,我们根据生成的头文件,创建一个C++源文件来实现这些函数。
// 文件:NativeCalculator.cpp #include "NativeCalculator.h" #include <iostream> // 实现加法函数 JNIEXPORT jint JNICALL Java_NativeCalculator_add(JNIEnv *env, jobject obj, jint a, jint b) { std::cout << “[C++] Adding ” << a << ” and ” << b << std::endl; return a + b; } // 实现减法函数 JNIEXPORT jint JNICALL Java_NativeCalculator_subtract(JNIEnv *env, jobject obj, jint a, jint b) { std::cout << “[C++] Subtracting ” << b << ” from ” << a << std::endl; return a - b; } // 实现乘法函数 JNIEXPORT jint JNICALL Java_NativeCalculator_multiply(JNIEnv *env, jobject obj, jint a, jint b) { std::cout << “[C++] Multiplying ” << a << ” and ” << b << std::endl; return a * b; } // 实现除法函数 JNIEXPORT jdouble JNICALL Java_NativeCalculator_divide(JNIEnv *env, jobject obj, jdouble a, jdouble b) { if (b == 0.0) { // 在C++中处理除零错误。更佳实践是使用JNI抛出Java异常。 std::cerr << “[C++] Error: Division by zero!” << std::endl; return 0.0; // 简单返回0,实际项目应抛异常 } std::cout << “[C++] Dividing ” << a << ” by ” << b << std::endl; return a / b; }实现要点:
#include “NativeCalculator.h”:包含我们生成的头文件,确保函数签名一致。- 函数签名必须严格匹配:函数名、返回类型、参数列表必须与头文件中的声明完全一致,一个字符都不能错。
- JNI类型:我们使用
jint、jdouble,它们分别对应Java的int和double。在大多数平台上,jint就是int,jdouble就是double,但使用JNI类型保证了可移植性。 - 简单的日志:在C++函数中加入
std::cout输出,可以在控制台看到C++代码确实被执行了,这对于调试非常有帮助。 - 错误处理:在
divide函数中,我们简单检查了除数是否为零。更好的做法是使用JNIEnv指针抛出一个Java异常(例如ArithmeticException),这样错误可以沿着Java调用栈传播,被Java端的try-catch捕获。
3.4 第四步:编译C++代码为动态库
这是将C++源代码变成Java可加载的二进制库的最后一步。编译命令因平台和编译器而异。
Windows (使用MSVC cl.exe): 在“x64 Native Tools Command Prompt for VS”中,导航到源码目录,执行:
cl /EHsc /LD /I"%JAVA_HOME%\include" /I"%JAVA_HOME%\include\win32" NativeCalculator.cpp /FeNativeCalculator.dll/EHsc:启用C++异常处理。/LD:告诉编译器生成一个动态链接库(DLL)。/I:指定头文件包含目录。JAVA_HOME是你的JDK安装路径。需要包含jni.h(在include目录)和平台相关的jni_md.h(在include\win32目录)。/Fe:指定输出的DLL文件名。这里生成NativeCalculator.dll。
Linux/macOS (使用GCC/Clang): 在终端中,导航到源码目录,执行:
# Linux g++ -shared -fPIC -I”$JAVA_HOME/include” -I”$JAVA_HOME/include/linux” NativeCalculator.cpp -o libNativeCalculator.so # macOS g++ -shared -fPIC -I”$JAVA_HOME/include” -I”$JAVA_HOME/include/darwin” NativeCalculator.cpp -o libNativeCalculator.dylib-shared:生成共享库(动态库)。-fPIC:生成位置无关代码(Position Independent Code),这是共享库所必需的。-I:指定头文件包含目录。Linux下平台目录是linux,macOS下是darwin。-o:指定输出文件名。Linux约定以lib开头,.so结尾;macOS以.dylib结尾。
编译成功后,你会在当前目录下得到对应的动态库文件(.dll,.so, 或.dylib)。
4. 运行测试与库文件路径问题
动态库编译好后,就可以运行我们的Java程序了。
4.1 运行Java程序
确保NativeCalculator.class和动态库文件都在当前目录,或者动态库位于Java的库搜索路径下。然后在命令行运行:
java NativeCalculator如果一切顺利,你将看到类似以下输出:
[C++] Adding 5 and 3 5 + 3 = 8 [C++] Subtracting 3 from 5 5 - 3 = 2 [C++] Multiplying 5 and 3 5 * 3 = 15 [C++] Dividing 5.0 by 3.0 5.0 / 3.0 = 1.6666666666666667这表明Java程序成功加载了动态库,并调用了C++实现的函数。
4.2 动态库加载路径详解
System.loadLibrary(“NativeCalculator”)这行代码,JVM会去哪里找这个库呢?这是新手最容易出错的地方。
- 默认库搜索路径:JVM会搜索
java.library.path系统属性指定的路径。你可以在Java程序中用System.getProperty(“java.library.path”)打印出来看看,通常包含系统库目录、当前工作目录等。 - 指定绝对路径:如果你不想把库文件放在默认路径,可以使用
System.load(“/full/path/to/your/library.dll”)来加载。这提供了最大的灵活性。 - 开发与部署时的策略:
- 开发阶段:最简单的方法是把动态库文件放在项目的根目录(与
.class文件一起),或者通过启动JVM时指定-Djava.library.path=/path/to/lib参数。 - 生产部署:通常会将动态库打包在应用的特定目录(如
lib/native/),然后在程序启动时,通过代码将该目录添加到java.library.path中,或者使用System.load()加载。
- 开发阶段:最简单的方法是把动态库文件放在项目的根目录(与
实操心得:在IDE(如IntelliJ IDEA)中运行时,
java.library.path可能与命令行不同。你需要在IDE的“运行配置”(Run Configuration)中,手动添加VM选项:-Djava.library.path=/path/to/your/dll。这是一个非常常见的“坑”,明明命令行能运行,在IDE里就报UnsatisfiedLinkError。
5. 进阶:处理复杂数据类型与内存管理
简单的整型、浮点型参数传递很直接。但实际项目中,我们经常需要处理字符串、数组、对象等复杂数据类型。这时就需要深入使用JNIEnv提供的函数。
5.1 字符串传递与转换
Java中的String在JNI中是jstring类型,它是一个引用,不能直接当C风格的字符串(char*)使用。必须通过JNI函数进行转换。
示例:在C++中拼接字符串并返回
// Java端 public native String greet(String name);// C++实现 JNIEXPORT jstring JNICALL Java_MyClass_greet(JNIEnv *env, jobject obj, jstring jname) { // 1. 将jstring转换为C风格的字符串(UTF-8编码) const char *cName = env->GetStringUTFChars(jname, NULL); if (cName == NULL) { return NULL; // 内存不足异常已抛出 } // 2. 使用C字符串进行操作 std::string greeting = “Hello, ” + std::string(cName) + “!”; // 3. 释放从Java获取的字符串资源(必须!) env->ReleaseStringUTFChars(jname, cName); // 4. 将C++字符串转换回jstring并返回 return env->NewStringUTF(greeting.c_str()); }关键点:
GetStringUTFChars/ReleaseStringUTFChars必须成对出现,防止内存泄漏。- 使用
GetStringUTFChars后,cName指向的内存是JVM分配的,用完后必须释放。 NewStringUTF会创建一个新的JavaString对象。
5.2 数组操作
Java数组在JNI中是jarray或其子类(如jintArray,jdoubleArray)。同样不能直接访问其元素。
示例:在C++中计算Java整型数组的和
JNIEXPORT jint JNICALL Java_MyClass_sumArray(JNIEnv *env, jobject obj, jintArray jarray) { // 1. 获取数组长度 jsize length = env->GetArrayLength(jarray); // 2. 获取数组元素的指针。模式:0表示获取,JNI_COMMIT表示写回,JNI_ABORT表示释放不写回。 jint *cArray = env->GetIntArrayElements(jarray, NULL); if (cArray == NULL) { return 0; } // 3. 像操作普通C数组一样操作 jint sum = 0; for (jsize i = 0; i < length; i++) { sum += cArray[i]; } // 4. 释放数组元素。第三个参数是模式: // 0: 将内容复制回Java数组,并释放C数组。 // JNI_COMMIT: 复制回但不释放(用于分段处理大数组)。 // JNI_ABORT: 不复制回,直接释放C数组。 env->ReleaseIntArrayElements(jarray, cArray, 0); return sum; }关键点:
GetIntArrayElements可能会返回一个指向原始Java数组的指针,也可能返回一个拷贝。这取决于JVM的实现和isCopy参数。无论如何,使用后必须调用对应的Release函数。- 对于
Get<Type>ArrayElements/Release<Type>ArrayElements这类函数,必须严格遵守“获取-释放”的配对,否则会导致内存泄漏或数据不一致。
5.3 内存管理与本地引用
JNI层创建的Java对象(如通过NewStringUTF、NewObject等)都是“本地引用”(Local Reference)。它们会在本地方法返回后,由JVM自动垃圾回收。但是,如果在本地方法中创建了大量本地引用(例如在循环中创建字符串),可能会超出JVM的本地引用表默认容量,导致FatalError。
解决方案:
- 及时删除:对于不再需要的大对象或循环内的临时对象,可以使用
env->DeleteLocalRef(ref)手动删除本地引用。 - 使用全局引用:如果需要在多个本地方法调用间,或跨线程保存一个Java对象引用,需要创建“全局引用”(Global Reference):
env->NewGlobalRef(localRef)。使用完毕后,必须手动调用env->DeleteGlobalRef(globalRef)来释放,否则会造成内存泄漏。
6. 常见问题排查与调试技巧实录
即使按照步骤操作,也难免会遇到各种错误。下面是我在实践中总结的常见问题及解决方法。
6.1UnsatisfiedLinkError大全
这是最常见的错误,表示JVM找不到或无法加载本地方法。
| 错误信息 | 可能原因 | 解决方案 |
|---|---|---|
java.lang.UnsatisfiedLinkError: no XXX in java.library.path | 动态库文件不在java.library.path中。 | 1. 将库文件放到java.library.path包含的目录。2. 启动时用 -Djava.library.path=指定路径。3. 使用 System.load(“绝对路径”)。 |
java.lang.UnsatisfiedLinkError: XXX.dll: Can’t find dependent libraries(Windows) | 动态库依赖的其他DLL(如MSVCRxxx.dll)找不到。 | 1. 使用Dependency Walker工具查看依赖。2. 确保依赖的VC++运行库已安装(可通过安装Visual C++ Redistributable解决)。 3. 将依赖DLL放到系统PATH或库所在目录。 |
java.lang.UnsatisfiedLinkError: XXX.so: undefined symbol: _ZTVN10__cxxabiv117__class_type_infoE(Linux) | C++库编译时缺少C++标准库支持,或链接了不兼容的C++ ABI。 | 1. 确保编译命令中链接了stdc++库(GCC下通常自动链接)。2. 检查编译器和JVM使用的C++运行时是否一致(如GCC版本)。 3. 尝试在编译命令中添加 -lstdc++。 |
java.lang.UnsatisfiedLinkError: Native method not found | JNI函数名、签名或参数类型与Java声明不匹配。 | 1. 使用javac -h重新生成头文件,仔细核对C++实现中的函数名和参数列表。2. 使用 javap -s -p NativeCalculator.class查看类中方法的完整签名,与C++函数签名对比。 |
6.2 调试JNI程序
调试JNI程序需要同时调试Java和C++代码,有一定复杂度。
- 日志输出法:最简单有效。在C++代码中使用
printf、std::cout或fprintf(stderr, …)输出日志。在Windows上,输出到控制台;在Linux/macOS上,输出到stderr。确保Java程序的标准输出/错误流没有被重定向。 - 使用IDE调试:
- IntelliJ IDEA + CLion/VSCode:可以用IDEA运行Java程序,用CLion或VSCode附加(Attach)到JVM进程,调试C++代码。需要确保编译C++库时包含调试信息(GCC/Clang加
-g,MSVC加/Zi)。 - Eclipse CDT:配置混合调试(Mixed Debugging)环境。
- IntelliJ IDEA + CLion/VSCode:可以用IDEA运行Java程序,用CLion或VSCode附加(Attach)到JVM进程,调试C++代码。需要确保编译C++库时包含调试信息(GCC/Clang加
- 处理JNI异常:C++代码中如果调用JNI函数失败(如
GetStringUTFChars返回NULL),JVM可能会设置一个异常。在继续调用其他JNI函数前,最好用env->ExceptionCheck()或env->ExceptionOccurred()检查是否有未处理的异常,并及时清理(env->ExceptionClear()),否则后续JNI调用可能行为异常。
6.3 跨平台编译的注意事项
如果你的项目需要在多个操作系统上运行,编译动态库是个挑战。
- 源码级跨平台:C++源码尽量使用标准C++,避免平台相关API。如果必须使用,用预编译宏隔离:
#ifdef _WIN32 // Windows-specific code #include <windows.h> #elif __linux__ // Linux-specific code #include <unistd.h> #elif __APPLE__ // macOS-specific code #include <TargetConditionals.h> #endif - 构建工具:不要手动写编译命令。使用CMake是工业标准的选择。你可以编写一个
CMakeLists.txt文件,自动检测平台、Java路径、编译器,并生成相应的构建脚本(如Makefile或Visual Studio项目)。 - 库命名:Java的
System.loadLibrary()会自动处理平台前缀和后缀(如lib和.so)。但为了清晰,你的构建脚本应为不同平台输出正确的文件名。
7. 项目源码结构与构建自动化
一个清晰的项目结构和一个自动化的构建过程,能极大提升开发效率,也是项目迈向规范化的标志。
7.1 推荐的目录结构
java-jni-demo/ ├── README.md ├── build/ # 编译输出目录(可忽略) ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── example/ │ │ │ └── NativeCalculator.java │ │ └── cpp/ │ │ ├── CMakeLists.txt # CMake构建脚本 │ │ ├── NativeCalculator.h (由javac -h生成) │ │ └── NativeCalculator.cpp │ └── test/ # 测试代码 └── scripts/ # 辅助脚本 ├── build_win.bat └── build_linux.sh7.2 使用CMake自动化构建
在src/main/cpp/目录下创建CMakeLists.txt:
cmake_minimum_required(VERSION 3.10) project(NativeCalculator) # 查找Java,获取JNI头文件路径 find_package(Java REQUIRED) find_package(JNI REQUIRED) # 打印找到的路径,便于调试 message(STATUS “JNI_INCLUDE_DIRS: ${JNI_INCLUDE_DIRS}”) message(STATUS “JNI_LIBRARIES: ${JNI_LIBRARIES}”) # 添加头文件搜索路径 include_directories(${JNI_INCLUDE_DIRS}) # 生成动态库 add_library(NativeCalculator SHARED NativeCalculator.cpp) # 设置输出库的名称(不含平台后缀) set_target_properties(NativeCalculator PROPERTIES OUTPUT_NAME “NativeCalculator”) # 根据平台设置不同的后缀(可选,CMake会默认处理) if(WIN32) set_target_properties(NativeCalculator PROPERTIES SUFFIX “.dll”) elseif(APPLE) set_target_properties(NativeCalculator PROPERTIES SUFFIX “.dylib”) else() set_target_properties(NativeCalculator PROPERTIES SUFFIX “.so”) endif()然后,你可以使用以下命令进行跨平台构建:
# 在cpp目录下 mkdir build && cd build cmake .. -DCMAKE_BUILD_TYPE=Release cmake --build . --config Release构建完成后,动态库文件会生成在build目录(或子目录,如Release/)中。
7.3 集成到Maven/Gradle(高级)
对于大型Java项目,你可能希望将JNI库的编译集成到Maven或Gradle构建流程中。这通常通过maven-native-plugin或自定义Gradle任务来实现,核心思想是在compile或package阶段,调用CMake或原生编译器来构建动态库,并将生成的库文件打包到最终的JAR包中,或者复制到resources目录以便运行时提取。
这个过程配置较为复杂,但它实现了“一键构建”,是专业项目必备的环节。其核心思路是在构建生命周期的特定阶段(如generate-sources之后),执行一个外部命令(CMake/make或直接调用编译器)来编译本地代码,然后将产出物(动态库)复制到类路径(如target/classes)下的特定目录(如native/${os.arch}/${os.name})。这样,在运行时,你的Java程序就可以根据当前操作系统和架构,从类路径中定位并加载正确的动态库。