Java 通过 JNI 调用 C++ DLL:关键流程总结
Java 通过 JNI 调用 C DLL关键流程总结1. 项目目标本项目演示了 Windows 下最基本的 JNI 调用链Java 声明 native 方法 → javac 生成 JNI 头文件 → C 按头文件实现导出函数 → CMake 将 C 编译为 DLL → Java 加载 DLL 并调用其中的函数JNI 的核心作用是建立两端共同遵守的二进制接口Java 端的方法声明决定接口javac -h生成 C/C 函数签名DLL 必须导出完全匹配的函数JVM 才能完成绑定。2. 项目中的关键文件文件作用java_call_cpp/pom.xmlMaven 构建配置设置Main-Class并在打包时复制hello.dll到targetjava_call_cpp/src/main/java/com/App.javaJava 程序入口调用Hello.helloFromCPP()java_call_cpp/src/main/java/com/Hello.java声明 native 方法并按 JAR 目录优先加载 DLLjava_call_cpp/src/main/java/com/com_Hello.hjavac -h生成的 JNI 头文件副本cpp_native/src/com_Hello.hC 工程使用的 JNI 头文件cpp_native/src/com_Hello.cppJNI 函数的 C 实现cpp_native/CMakeLists.txt查找 JNI 头文件并构建动态库cpp_native/CMakePresets.jsonMinGW 配置和构建预设3. Java 工程结构java_call_cpp是一个 Maven JAR 工程入口类是com.App。参照java_native_access/java_call_cpp后当前工程结构改为maven-jar-plugin在 JAR 清单中写入Main-Class: com.App因此可以直接使用java -jar。maven-resources-plugin在prepare-package阶段把java_call_cpp/hello.dll复制到target让 DLL 与 JAR 同级。Hello加载 DLL 时优先查找类或 JAR 所在目录下的hello.dll如果该位置不存在则回退到user.dir/hello.dll兼容原来的 classpath 运行方式。打包后的关键结构如下java_call_cpp/ pom.xml hello.dll target/ java_call_cpp-1.0-SNAPSHOT.jar hello.dll4. 完整调用链App.main()new Hello().helloFromCPP()Hello 静态初始化System.load(JAR 同级或 user.dir 下的 hello.dll)JVM 加载 DLL查找 Java_com_Hello_helloFromCPP执行 C printf()实际运行顺序如下JVM 进入com.App.main()。new Hello()首次使用Hello类触发其静态初始化块。静态初始化块优先加载 JAR 同级目录中的hello.dll否则回退到当前工作目录中的hello.dll。Java 调用helloFromCPP()。JVM 在 DLL 中查找与该方法匹配的 JNI 导出符号。JVM 进入 C 实现并输出im from cpp。5. Java 端声明 native 方法Hello.java中的关键代码为packagecom;importjava.net.URISyntaxException;importjava.nio.file.Files;importjava.nio.file.Path;publicclassHello{publicnativevoidhelloFromCPP();static{PathdllPathresolveDllPath();System.out.println(dllPath);System.load(dllPath.toString());}privatestaticPathresolveDllPath(){PathfallbackPathPath.of(System.getProperty(user.dir),hello.dll).toAbsolutePath().normalize();try{PathcodePathPath.of(Hello.class.getProtectionDomain().getCodeSource().getLocation().toURI());PathbaseDircodePath.toFile().isFile()?codePath.getParent():codePath;PathjarSideDllbaseDir.resolve(hello.dll).toAbsolutePath().normalize();returnFiles.exists(jarSideDll)?jarSideDll:fallbackPath;}catch(URISyntaxExceptione){returnfallbackPath;}}}关键点native表示该方法只有 Java 声明具体实现位于本地动态库中。当前方法是实例方法没有业务参数返回值为voidJNI 描述符是()V。项目使用System.load(绝对路径)不是System.loadLibrary(hello)。使用java -jar target/java_call_cpp-1.0-SNAPSHOT.jar时DLL 应位于 JAR 同级目录即target/hello.dll。使用java -cp target/classes com.App时如果target/classes/hello.dll不存在会回退加载当前工作目录下的hello.dll。App.java发起实际调用newHello().helloFromCPP();6. 生成 JNI 头文件在java_call_cpp目录中执行mkdir-ptarget/jni-classes javac-encodingUTF-8\-h./src/main/java/com\-d./target/jni-classes\./src/main/java/com/Hello.javacp-f\./src/main/java/com/com_Hello.h\../cpp_native/src/com_Hello.h-h指定 JNI 头文件的输出目录-d将临时生成的.class文件放入target避免污染源码目录。生成的核心声明为JNIEXPORTvoidJNICALLJava_com_Hello_helloFromCPP(JNIEnv*,jobject);函数名来自 Java 的包名、类名和方法名Java_ com Hello helloFromCPP Java_com_Hello_helloFromCPP这里使用的是 Java 包名com与 Maven 的groupIdcom.example无关。两个由 JVM 自动传入的参数分别是JNIEnv *JNI 环境指针用于字符串转换、对象访问、异常处理等 JNI 操作。jobject调用该非静态 native 方法的当前Hello对象如果 Java 方法是static native这里会改为jclass。生成文件中的extern C用于关闭 C 名字改编JNIEXPORT负责导出符号JNICALL指定 JNI 调用约定。带有DO NOT EDIT THIS FILE标记的头文件不应手工修改。7. C 实现 JNI 函数cpp_native/src/com_Hello.cpp按生成的声明实现函数#includejni.h#includestdio.h#includecom_Hello.hJNIEXPORTvoidJNICALLJava_com_Hello_helloFromCPP(JNIEnv*,jobject){printf(%s\n,im from cpp);}C 函数名、返回值、参数顺序和调用约定必须与生成的头文件完全一致。本例没有 Java 业务参数和返回值因此不涉及jstring、jint等类型转换。8. 使用 CMake 构建 DLLCMakeLists.txt的核心配置是add_library(hello SHARED src/com_Hello.cpp ) find_package(JNI REQUIRED) target_include_directories(hello PRIVATE ${JNI_INCLUDE_DIRS} )add_library(hello SHARED ...)创建动态库目标。find_package(JNI REQUIRED)根据 JDK 环境查找jni.h和 Windows 平台的jni_md.h。JAVA_HOME应指向有效的 JDKJava 与 DLL 的体系结构必须一致例如都为 x64。本例的 DLL 由 JVM 加载C 代码也没有主动创建 JVM因此当前实现只需要 JNI 头文件不需要显式链接jvm.lib。项目提供了 MinGW 预设。正常情况下可在cpp_native目录执行cmake--presetmingw cmake--build--presetmingwbuildMinGW 构建的实际 DLL 名称是cpp_native/build/mingw/libhello.dll如果项目目录曾被移动或重命名旧的CMakeCache.txt会记录失效的绝对路径。此时可使用一个新的构建目录重新配置cmake--presetmingw-B./build/jni cmake--build./build/jni对应产物为cpp_native/build/jni/libhello.dll。9. 将 DLL 交给 Java 工程MinGW/CMake 生成的是libhello.dll但当前 Java 代码加载的文件名是hello.dll所以需要先复制并改名到java_call_cpp目录。使用默认预设构建目录时在cpp_native目录执行cp-f\./build/mingw/libhello.dll\../java_call_cpp/hello.dll如果使用了上面的新构建目录则源文件改为build/jni/libhello.dll。随后执行mvn package时Maven 会再把java_call_cpp/hello.dll复制到java_call_cpp/target/hello.dll使它与可执行 JAR 同级。libhello.dll.a是供其他原生程序链接 DLL 时使用的导入库Java 运行本例不需要它。项目的 Git 忽略规则也不会提交 DLL因此新环境需要重新构建并执行复制步骤。10. 打包并使用 java -jar 测试在java_call_cpp目录执行mvn packagejava-jar./target/java_call_cpp-1.0-SNAPSHOT.jarmvn package会完成 Java 编译、生成target/java_call_cpp-1.0-SNAPSHOT.jar并把hello.dll复制到target目录。也可以保留 classpath 方式做快速测试mvn compilejava-cp./target/classes com.App成功输出为Hello World! C:\source\learning_cpp_vscode\java_native_interface\java_call_cpp\target\hello.dll im from cpp最后一行来自 C DLL说明以下环节均已成功Java 找到并加载 DLL。JVM 找到正确的 JNI 导出符号。Java 调用已经进入 C 实现。11. 修改接口时必须同步的内容只要修改以下任一内容就必须重新执行javac -h、同步头文件、重新构建 DLLJava 包名Java 类名native 方法名方法是否为static参数类型或数量返回值类型native 方法是否重载。正确的依赖方向是Hello.java 中的 native 声明 → javac -h 重新生成 com_Hello.h → C 按新头文件修改实现 → 重新构建并复制 DLL → 重新运行 Java不要通过手工修改生成的头文件来反向改变 Java 接口。12. 常见失败位置现象重点检查CMake 找不到 JNIJAVA_HOME是否指向完整 JDK而不是 JRE重新配置 CMakeCant load ... hello.dlljava -jar时检查target/hello.dll是否存在classpath 运行时检查当前目录是否有hello.dllUnsatisfiedLinkError提示找不到 native 方法Java 声明、生成头文件、C 导出函数和 DLL 是否为同一版本DLL 存在但仍无法加载Java 与 DLL 位数是否一致DLL 依赖的运行库是否可被 Windows 找到CMake 提示源码目录与缓存目录不一致项目目录曾移动使用新的构建目录重新配置修改 Java 接口后仍执行旧逻辑重新生成头文件、重建 DLL并覆盖java_call_cpp/hello.dll