有些开发工作中,如音视频SDK/服务、AI推理服务等,偶尔会遇到Java调用C/C++开发的动态库,通常使用JNI或JNA技术实现,如何加载动态库是绕不开的坎。
比如说Java SDK开发时,集成SDK的Java服务通常在Linux上部署运行,但很多Java开发人员根本搞不清楚工作目录、库目录、库搜索路径等概念,作为SDK提供方,每次都要指导集成方调用方放置对应架构的动态库文件、设置Java启动参数或环境变量等操作,带来很多的沟通成本。尤其是跨团队、跨部门甚至跨公司时,总是因为库加载出错而反复沟通细节。
Java服务基本都是打成JAR包部署、运行,JAR不仅能存放class文件,也能存放一些配置文件,(WAR和fat-JAR也是JAR包),既然JAR包能存放资源文件,那是不是可以把动态库文件也存放到JAR包里呢?答案是肯定的,有不少类库使用了这种技巧。
原理就是一句话:将JAR包里的动态库文件保存后使用System.load
方法加载。
说起来简单,但是需要考虑一些细节:动态库在jar里的存储位置,保存到哪里,要不要复用保存的动态库文件,怎么清理保存的文件,不同的平台怎么读取不同的动态库文件,多个Java进程同时加载会不会有问题等等。
很遗憾的是,找了很久也没有找到符合的类库。Github上有一个native-utils
库,实现了从JAR包读取动态库的功能,但是缺少多平台支持,清理功能做的不好。于是萌生了写一个考虑上面说的那些细节、生产可用的开源类库的想法。
基本原理是清晰的,首先要考虑的是如何检查当前运行的平台。因为不同平台之间存在差异,比如动态库名称、文件占用机制等。
Java提供了一些属性(Properties),可以用于判断运行平台:os.name
可以判断操作系统;os.arch
可以判断指令架构;java.vendor
可以判断JVM提供商。通过System.getProperty
方法就可以查到对应属性的值,但是这些属性的值并不是标准化的,是实现相关,所以写这部分代码是一个脏活累活。
好在有些网站对这些属性值有部分收集,并且当前主流的JDK就那么几个,费点功夫还是能解析出准确的系统、指令架构信息的。这里有个特殊的地方,安卓的os.name
也是linux,需要通过java.vendor
来判断是否是安卓。
//判断操作系统:
String os = System.getProperty("os.name").toLowerCase().replaceAll("[^a-z0-9]", "");;
if (os.contains("linux")) {
current = LINUX;
if (System.getProperty("java.vendor").toLowerCase().contains("android"))
current = ANDROID;
} else if (os.startsWith("windows")) {
current = WINDOWS;
} else if (os.startsWith("mac") || os.startsWith("osx")) {
current = MACOS;
} else if (os.startsWith("aix")) {
current = AIX;
} //...
//判断CPU架构
String arch = System.getProperty("os.arch").toLowerCase().replaceAll("[^a-z0-9]", "");
if (arch.equals("x86") || arch.startsWith("i") && arch.endsWith("86"))
current = X86;
else if (arch.equals("amd64") || arch.equals("x86_64") || arch.equals("em64t"))
current = X64;
else if (arch.startsWith("armel") || arch.equals("armeabi") || arch.equals("armv5"))
current = ARMEL;
else if (arch.startsWith("armhf") || arch.equals("armeabiv7a") || arch.equals("armv7") || arch.equals("armv8l"))
current = ARMHF;
else if (arch.equals("aarch64") || arch.startsWith("arm64") || arch.equals("armv8") || arch.equals("armv8a"))
current = AARCH64;
else if (arch.equals("loonarch64"))
current = LOONGARCH64;
//...
平台就是系统与指令架构的结合,得知运行的平台后,即可将jar包里存放的对应平台动态库保存出去。
加载采用的方式是:在java.io.temp
属性指向的目录里创建一个唯一的目录存放保存的动态库临时文件,临时文件名是动态库名称加随机数,临时文件不共用也不复用,保存后加载即可。
String fullName = getLibraryFullName(baseName); //得到动态库全名
String jarLibraryFile = getJarLibraryFilePath(path, fullName); //获取动态库的JAR路径
Path tmpLibraryPath = makeTempFile(fullName); //创建临时文件
saveJarFile(jarLibraryFile, tmpLibraryPath.toString()); //保存动态库文件
try {
System.load(tmpLibraryPath.toString()); //加载动态库
} catch (Throwable e) {
throw new UnsatisfiedLinkError(String.format("natives: load '%s' occurs exception: %s", jarLibraryFile, e));
} finally {
deleteTempFile(tmpLibraryPath); //不管加载成功与否都要清理
}
清理方式是:unix-like的系统下动态库加载后直接删除;Windows下加载后的库文件不能删除,但是可以改名。所以Windows下动态库加载后改名为delete-随机数,下次Java进程启动时再删除。
对多平台的支持其实就是对动态库存放在JAR包里的路径作了约定,关键还是要编译对应平台的动态库文件后放到JAR包里。
为了方便把动态库文件放到JAR包里正确的路径下,同时提供工具脚本:
./natives-add -h
Usage: ./natives-add <jar> <path> <os_arch> <library>
<jar> target jar file.
<path> native library save path in jar.
<os_arch> library's platform. etc. windows_x86 windows_x64 linux_x64 linux_arm64 ...
<library> library file to add.
Example:
Bash: ./natives-add example.jar org/example/libs linux_x64 libexample.so
# add libexample.so to example.jar!/org/example/libs/linux_x64/libexample.so
Java: LibraryLoader.loadFromJar("org/example/libs", "example");
// load the libexample.so library on linux_x64 platform.
有了这个类库后,动态库JAR包的开发者只要使用工具脚本将不同平台的动态库都放入JAR包,JAR的使用者可以很轻易的加载动态库,再也不用纠结动态库文件放哪、怎么设置Java启动参数、怎么设置环境变量了!
import com.github.xuges.natives.LibraryLoader;
//在Linux x86-64下读取org/example/libs/linux_x64/libexample.so
//在Windows x86-64下读取org/example/libs/windows_x64/example.dll
//在Android arm64-v8a下读取org/example/libs/android_aarch64/libexample.so
LibraryLoader.loadFromJar("org/example/libs", "example");
需要注意的是,如果动态库有依赖,可以把依赖的动态库一起放在JAR包里,加载的时候先加载依赖动态库,再加载动态库。如果系统环境里存在依赖,则不用关心。建议动态库静态链接其他依赖库。
项目地址:https://githib.com/xuges/natives
此类库目前生产环境使用中,发现BUG会第一时间修复。如果对你的工作有所帮助,请给个赞/Star。如果使用中发现BUG,烦请提个issue,感谢支持。
参考:
https://github.com/adamheinrich/native-utils
https://github.com/trustin/os-maven-plugin
https://lopica.sourceforge.net/os.html