ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

物联网平台优化系列(二):本地仓库 + JNI 自动加载,解决非公共依赖难题

物联网平台优化系列(二):本地仓库 + JNI 自动加载,解决非公共依赖难题 技术栈Spring Boot Maven JNI适用场景CI/CD 构建依赖缺失 JNI 本地库跨平台部署1. 问题背景在使用云效、Jenkins 等 CI/CD 平台构建 Java 项目时经常会遇到一类让人头疼的问题项目依赖了某些不在 Maven 中央仓库中的 JAR 包比如硬件厂商提供的 SDK、老旧的本地库封装导致云端编译失败。同时像 rxtx 这类依赖 JNI 本地库的组件还要求在不同操作系统上手动放置对应的 DLL 或 SO 文件给部署带来额外负担。本文以一个真实的 IoT 项目为例介绍两种实用的解决方案让项目在云端 CI 和本地离线环境下都能顺利编译和运行。我们的项目是一个基于 Spring Boot 2.2 的 Java 多模块物联网平台其中有两个不听话的依赖mysdk-jnacom.sun.examples:mysdk-jna:1.0.0是海康威视 SDK 封装所需的 JNA 扩展包不在 Maven 中央仓库中。开发者的本地.m2缓存里通常已经有了所以本地编译正常但云端构建环境是全新的拉不到这个包就直接报[ERROR] build error。rxtxorg.rxtx:rxtx:2.1.7是 Java 串口通信库JAR 包本身能从 Maven 仓库下载但它依赖的 JNI 本地库rxtxSerial.dll/librxtxSerial.so需要手动放置到java.library.path目录下每次换一台机器部署都要重复操作。2. 方案一项目内本地 Maven 仓库核心思路是在项目目录下建一个local-repo文件夹按照标准 Maven 仓库的目录结构存放非公共依赖的 JAR 和 POM然后在pom.xml中声明这个本地仓库。这样无论是云端还是本地Maven 都能从项目目录里解析到这些依赖。2.1 创建目录结构在项目根目录下创建local-repo按照groupId的路径层级组织文件local-repo/ ├── com/sun/examples/mysdk-jna/1.0.0/ │ ├── mysdk-jna-1.0.0.jar │ ├── mysdk-jna-1.0.0.pom │ └── _remote.repositories └── org/rxtx/rxtx/2.1.7/ ├── rxtx-2.1.7.jar ├── rxtx-2.1.7.pom └── _remote.repositoriesJAR 和 POM 文件可以从开发者本地的~/.m2/repository中直接复制。每个目录下还需要一个_remote.repositories文件内容如下#NOTE: This is a Maven Resolver internal implementation file mysdk-jna-1.0.0.jar mysdk-jna-1.0.0.pom表示该构件是本地安装的不关联任何远程仓库防止 Maven 尝试去远程校验。2.2 修改 pom.xml在根pom.xml的/dependencies之后添加仓库配置repositoriesrepositoryidproject-local-repo/idnameProject Local Repository/nameurlfile://${maven.multiModuleProjectDirectory}/local-repo/url/repository/repositories这里用的是${maven.multiModuleProjectDirectory}而非${project.basedir}。后者在子模块中会解析为子模块自身的路径导致子模块找不到仓库。maven.multiModuleProjectDirectory是 Maven 3.3.1 提供的属性始终指向根项目目录完美解决了多模块项目中的路径问题。2.3 提交并验证将local-repo目录和修改后的pom.xml一起提交到代码仓库。执行mvn compile验证所有模块都能正常解析依赖。这个方案的好处是简单直接、零外部依赖。代价是仓库体积会增加通常只增加几 MB但对于内部项目来说完全可以接受。3. 方案二跨平台本地库自动加载rxtx 的 JAR 包只包含 Java 类不包含 JNI 本地库。传统做法是把rxtxSerial.dllWindows或librxtxSerial.soLinux手动复制到C:\Windows\System32或$JAVA_HOME/jre/lib/amd64/下。这种方式在 CI 环境或多台服务器部署时非常不便。我们的做法是把本地库文件打包进 JAR 的资源目录中运行时自动提取并加载。3.1 资源目录规划在src/main/resources/native/下按操作系统和架构组织文件src/main/resources/native/ ├── windows/x86_64/ │ ├── rxtxSerial.dll │ └── rxtxParallel.dll └── linux/x86_64/ ├── librxtxSerial.so └── librxtxParallel.soWindows 的 DLL 从开发机的System32复制Linux 的 SO 文件可以从 rxtx 官方发布的rxtx-2.1-7-bins-r2.zip中提取路径为Linux/x86_64-unknown-linux-gnu/librxtxSerial.so。3.2 NativeLibLoader 工具类核心逻辑分三步检测平台、提取本地库、动态追加搜索路径。publicclassNativeLibLoader{privatestaticvolatilebooleanloadedfalse;publicstaticsynchronizedvoidloadRxtxLibs(){if(loaded)return;StringosdetectOs();// windows / linuxStringarchdetectArch();// x86_64StringnativeDir;String[]nativeLibs;if(windows.equals(os)x86_64.equals(arch)){nativeDirwindows/x86_64;nativeLibsnewString[]{rxtxSerial.dll,rxtxParallel.dll};}elseif(linux.equals(os)x86_64.equals(arch)){nativeDirlinux/x86_64;nativeLibsnewString[]{librxtxSerial.so,librxtxParallel.so};}else{log.warn(当前平台({}/{})未内置本地库请手动放置,os,arch);loadedtrue;return;}try{PathtempDirFiles.createTempDirectory(rxtx-native-);tempDir.toFile().deleteOnExit();intextractedCount0;for(StringlibName:nativeLibs){StringresourcePath/native/nativeDir/libName;try(InputStreamisNativeLibLoader.class.getResourceAsStream(resourcePath)){if(isnull)continue;FiletempFilenewFile(tempDir.toFile(),libName);Files.copy(is,tempFile.toPath(),StandardCopyOption.REPLACE_EXISTING);if(linux.equals(os)){tempFile.setExecutable(true,false);}tempFile.deleteOnExit();extractedCount;}}if(extractedCount0){addLibraryPath(tempDir.toFile());}loadedtrue;}catch(Exceptione){log.error(rxtx 本地库自动加载失败: {},e.getMessage(),e);loadedtrue;}}}平台检测通过System.getProperty(os.name)和System.getProperty(os.arch)实现做了标准化处理比如把amd64和x86_64统一归为x86_64。提取本地库到临时目录时Linux 下需要额外调用setExecutable(true, false)赋予可执行权限否则 JVM 会报UnsatisfiedLinkError。3.3 动态追加 java.library.pathJava 默认只在启动时读取java.library.path运行时修改系统属性不会生效。需要通过反射直接修改 ClassLoader 内部缓存的路径数组privatestaticvoidaddLibraryPath(Filedir)throwsException{// 修改系统属性影响后续新创建的 ClassLoaderStringexistingPathSystem.getProperty(java.library.path,);System.setProperty(java.library.path,existingPathFile.pathSeparatordir.getAbsolutePath());// 反射修改当前 ClassLoader 已缓存的路径数组FieldusrPathsFieldClassLoader.class.getDeclaredField(usr_paths);usrPathsField.setAccessible(true);String[]usrPaths(String[])usrPathsField.get(null);StringdirPathdir.getAbsolutePath();for(Stringpath:usrPaths){if(path.equals(dirPath))return;}String[]newUsrPathsnewString[usrPaths.length1];System.arraycopy(usrPaths,0,newUsrPaths,0,usrPaths.length);newUsrPaths[usrPaths.length]dirPath;usrPathsField.set(null,newUsrPaths);}这段代码同时做了两件事修改系统属性为后续可能创建的 ClassLoader 生效和反射修改usr_paths数组为当前 ClassLoader 立即生效。3.4 集成到 Spring 服务在串口服务的PostConstruct方法中调用加载器确保在任何串口操作之前完成本地库加载ServicepublicclassSerialPortServiceImplimplementsISerialPortService{PostConstructpublicvoidinit(){NativeLibLoader.loadRxtxLibs();}}loadRxtxLibs()内部通过volatile boolean loaded标志位保证线程安全且只执行一次多次调用没有副作用。4. 注意事项1本地仓库与 Git。local-repo目录应该纳入 Git 版本管理。如果 JAR 文件较大超过 100MB建议考虑 Git LFS 或者使用专门的二进制制品管理方案。2JDK 版本兼容性。反射修改usr_paths的方式依赖 JDK 内部实现细节。在 JDK 8 和 JDK 11 上测试正常但 JDK 17 的强封装机制--illegal-accessdeny可能会导致反射失败届时需要添加 JVM 启动参数--add-opens java.base/java.langALL-UNNAMED。如果项目计划升级 JDK 版本建议提前关注。3Linux 并口库的问题。rxtx 2.1.7 官方发布的 Linux x86_64 版本中没有提供librxtxParallel.so。由于串口通信场景下只会加载librxtxSerial.so并口库极少被使用可以不提供。如果需要完整覆盖可以在 32 位 Linux 环境中获取。注意不能混用不同架构的本地库否则 JVM 加载时会报UnsatisfiedLinkError: wrong ELF class。5. 总结方案解决的问题适用阶段项目内本地 Maven 仓库编译期依赖找不到CI/CD 构建 离线开发跨平台本地库自动加载运行期 JNI 库缺失多平台部署两个方案结合使用可以让依赖了非公共组件的 Java 项目在开发和部署流程上更加顺畅。非公共依赖和 JNI 本地库是 Java 项目中常见但又容易被忽视的问题。本文介绍的两种方案都不复杂但对构建稳定性和部署效率的提升是实实在在的。
返回列表