Skip to content

从 JAR 包中加载本地库文件 ​

在跨平台 Java 项目中,往往需要使用 JNI 调用本地(C/C++)代码。本方案介绍如何将 native media 编译后的各平台动态库(如 Windows 的 DLL、Linux 的 .so、macOS 的 .dylib)打包到 JAR 内部,并在运行时通过工具类将这些库文件提取到用户目录下加载。这样既便于统一管理依赖,又能做到跨平台支持。

1. 项目目录结构 ​

在项目中,将本地库文件放置于 src/main/resources/lib 目录下,并按照平台进行归类,示例如下:

├── lib
│   ├── darwin_arm64
│   │   ├── libnative_media.dylib
│   ├── linux_amd64
│   │   ├── libnative_media.so
│   ├── win_amd64
│   │   ├── avcodec-61.dll
│   │   ├── avformat-61.dll
│   │   ├── avutil-59.dll
│   │   ├── libmp3lame.DLL
│   │   ├── libnative_media.dll
│   │   ├── swresample-5.dll

这样打包后,上述文件会被嵌入到 JAR 内部,在运行时根据当前平台自动提取对应的库文件进行加载。

2. 工具类实现 ​

下面的工具类 LibraryUtils 完成以下功能:

  • 判断当前系统平台(Windows、macOS 或 Linux),确定库文件名称及所在的子目录。
  • 检查目标目录是否存在,并创建用于存储提取出的库文件。
  • 从 JAR 内部资源中将对应平台的库文件复制到用户目录下的指定位置。
  • Windows 平台下需要额外加载依赖的 DLL 文件,且加载顺序要求严格。
  • 最后通过 System.load 加载主库。

以下是经过润色后的完整代码示例:

java
package nexus.io.media.utils;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;

import nexus.io.media.core.Core;

public class LibraryUtils {
  public static final String WIN_AMD64 = "win_amd64";
  public static final String DARWIN_ARM64 = "darwin_arm64";
  public static final String LINUX_AMD64 = "linux_amd64";

  public static void load() {
    // Determine the current operating system and platform to identify the library file name and resource directory

    String osName = System.getProperty("os.name").toLowerCase();
    String userHome = System.getProperty("user.home").toLowerCase();
    System.out.println("os name:" + osName + " user.home:" + userHome);
    String archName;
    String libFileName;
    if (osName.contains("win")) {
      libFileName = Core.WIN_NATIVE_LIBRARY_NAME;
      archName = WIN_AMD64;
    } else if (osName.contains("mac")) {
      libFileName = Core.MACOS_NATIVE_LIBRARY_NAME;
      archName = DARWIN_ARM64;
    } else if (osName.contains("nix") || osName.contains("nux") || osName.contains("aix") || osName.contains("linux")) {
      libFileName = Core.UNIX_NATIVE_LIBRARY_NAME;
      archName = LINUX_AMD64;
    } else {
      throw new UnsupportedOperationException("Unsupported OS: " + osName);
    }

    // Create the directory for storing the extracted library file, e.g.: lib/win_amd64/
    String dstDir = userHome + File.separator + "lib" + File.separator + archName;
    File libFile = new File(dstDir, libFileName);
    File parentDir = libFile.getParentFile();
    if (!parentDir.exists()) {
      parentDir.mkdirs();
    }
    // Now extract the resource
    extractResource("/lib/" + archName + "/" + libFileName, libFile);

    // If the OS is Windows, additional dependent DLL files need to be loaded
    if (WIN_AMD64.equals(archName)) {
      String[] dlls = { "avutil-59.dll", "swresample-5.dll", "libmp3lame.DLL", "avcodec-61.dll", "avformat-61.dll" };
      for (String dll : dlls) {
        File dllFile = new File(dstDir, dll);
        extractResource("/lib/" + archName + "/" + dll, dllFile);
        System.load(dllFile.getAbsolutePath());
      }
    }

    // Load the main library file
    String absolutePath = libFile.getAbsolutePath();
    System.out.println("load " + absolutePath);
    System.load(absolutePath);
  }

  /**
   * Copies a resource file from the jar to the specified destination.
   *
   * @param resourcePath The resource path inside the jar, e.g.: /lib/win_amd64/xxx.dll
   * @param destination  The destination file
   */
  private static void extractResource(String resourcePath, File destination) {
    try (InputStream in = LibraryUtils.class.getResourceAsStream(resourcePath)) {
      if (in == null) {
        throw new RuntimeException("Resource does not exist: " + resourcePath);
      }
      Files.copy(in, destination.toPath(), StandardCopyOption.REPLACE_EXISTING);
    } catch (IOException e) {
      throw new RuntimeException("Failed to extract resource: " + resourcePath + " to " + destination.getAbsolutePath(), e);
    }
  }
}

3. 加载流程说明 ​

  1. 确定平台
    根据 os.name 判断当前操作系统,并选择相应的平台目录(如 win_amd64、darwin_arm64、linux_amd64)及库文件名称。对应的库文件名称存放在 Core 接口中,例如:

    • Windows:libnative_media.dll
    • macOS:libnative_media.dylib
    • Linux:libnative_media.so
  2. 创建目录与提取资源
    工具类首先创建一个 lib 目录,并在其中生成平台对应的子目录。若目标库文件不存在,则通过 getResourceAsStream 从 JAR 内部提取该文件,并写入到本地目录中。

  3. 加载依赖(Windows 特有)
    Windows 平台下,由于动态库依赖关系较为复杂,除了主库外,还需要提前加载其他依赖的 DLL 文件,且加载顺序严格(如先加载 avutil-59.dll 等)。

  4. 调用 System.load
    提取并加载所有依赖文件后,最后通过 System.load 加载主库文件,从而使 JNI 方法能够被正确调用。

4. 部署与使用 ​

  • 打包
    将 src/main/resources/lib 目录下的所有库文件打包进 JAR 后,运行时工具类会自动将这些库复制到用户目录下指定位置。

  • 加载调用
    在 Java 项目的入口类或 JNI 相关类中调用 LibraryUtils.load() 方法,即可完成本地库的自动提取和加载。建议在所有 native 方法调用前加载本地库。


打包 ​

java
mvn clean install -DskipTests

打包后的文件 java-native-media-1.0.0.jar 大小是 7.21MB

总结 ​

通过上述方案,您可以将各平台的本地库文件内嵌到 JAR 包中,并在程序启动时自动提取到本地目录,确保所有平台都能正确加载 JNI 依赖。此方法不仅简化了部署流程,也便于跨平台项目的统一管理。如果有进一步的问题或需要扩展其他平台的支持,可在此基础上做相应修改。

MIT Licensed