当 JNI 遇到“一次编写,随处运行”时:驾驭 Java 的多架构现实

发布: (2025年12月26日 GMT+8 22:59)
13 min read
原文: Dev.to

Source: Dev.to

Source:

介绍

Java 最为人称道的承诺之一是 “一次编写,随处运行” (WORA)
其理念简单而强大:只需编写一次代码,编译后即可在任何平台——Windows、Linux、macOS、x86、ARM——上运行,无需修改。正是这项承诺让 Java 成为企业应用、Android 开发以及无数其他场景的首选语言。

对于纯 Java 代码,这一承诺能够完美实现。但也有例外。

当你进入 JNI(Java Native Interface) 的世界——即 Java 与用 C、C++ 或 Rust 编写的本地代码之间的桥梁时,跨平台的简洁性就会消失。我们突然必须考虑平台特定的二进制文件,并且会遇到晦涩的 UnsatisfiedLinkError 错误信息。

在本文中,我们将探讨:

  • Java 通常是如何实现平台无关的
  • 在使用 JNI 时,这种独立性为何以及何时会失效
  • 现代库如何巧妙地解决多架构挑战
  • 生产环境中多架构库的内部实现概览

“一次编写,到处运行”理念

Java的WORA承诺不仅是营销口号——它是1990年代中期革命性的软件开发的根本架构决策。

它解决的问题

在Java之前,跨平台软件需要痛苦的妥协:

  • 为每个平台(Windows、macOS、Unix)维护独立代码库
  • 抽象层如POSIX,但仍需为每个平台单独编译
  • 解释型语言如Perl或Python,速度较慢且生态系统有限

Java的巧妙解决方案

Java引入了一个中间层:字节码。Java不是直接编译成机器码,而是编译成一种通用、平台无关的指令集,能够在任何装有JVM的机器上运行。

Source Code (.java) → Bytecode (.class) → JVM (x86/ARM/…) → Machine Code
   [Developer]          [Compiler]          [Runtime]          [CPU]

当你使用javac编译Java代码时,它会生成包含字节码的.class文件——这些是通用指令,不针对x86、ARM或任何特定CPU架构。这些字节码指令(例如aload_0getstaticinvokevirtual)由JVM理解,而不是直接由硬件理解。

魔法发生在运行时:每个平台都提供自己的JVM实现(为x86‑linuxaarch64‑darwin等编译),它读取这种通用字节码并通过即时编译(Just‑In‑Time, JIT)翻译成本地机器码。相同的.jar.class文件的压缩文件夹)可以在Linux x86_64、macOS ARM64或Windows x86_64上运行,且无需任何修改。

这种架构提供了真正的可移植性,因为相同的.jar可以在任何地方运行。

异常情况:当架构变得重要时

现在进入有趣的部分:这种美妙的抽象何时会失效?

引入 JNI:通往本地代码的桥梁

JNI(Java Native Interface)允许 Java 代码调用用 C、C++、Rust 或任何能够编译为本机机器码的语言编写的函数。

我们为什么要这么做?

  • 性能 – 对于关键操作(例如加密),本地代码通常更快。
  • 硬件访问 – Java 无法执行的底层硬件操作。
  • 遗留集成 – 重用已有的 C/C++ 库。
  • 系统 API – 访问 Java 未公开的操作系统特定功能。

注意: 使用 JNI 并不总是有利。JNI 调用会产生开销,因此频繁地对轻量级操作进行调用可能会增加延迟。仅在本地实现能够提供显著速度优势时才使用 JNI。

问题:本地库是平台和架构特定的

编译本地代码时,必须为每个目标平台(Windows、macOS、Linux,……)以及每个目标架构(x86‑64、ARM64、armv7,……)生成相应的二进制文件。

我们可以使用 SQLite JDBC 驱动进行一个简单的测试来了解这一点:

# 从 Maven 下载 SQLite JDBC jar
mvn dependency:get -Dartifact=org.xerial:sqlite-jdbc:3.44.1.0

# 解压其内容
jar xf ~/.m2/repository/org/xerial/sqlite-jdbc/3.44.1.0/sqlite-jdbc-3.44.1.0.jar

# 检查本地库
file native/Mac/x86_64/libsqlitejdbc.dylib
# → Mach-O 64-bit dynamically linked shared library x86_64

file native/Mac/aarch64/libsqlitejdbc.dylib
# → Mach-O 64-bit dynamically linked shared library arm64

file native/Windows/x86_64/sqlitejdbc.dll
# → PE32+ executable (DLL) (console) x86-64 ...

file native/Windows/x86/sqlitejdbc.dll
# → PE32 executable (DLL) (console) Intel 80386 ...

file native/Windows/armv7/sqlitejdbc.dll
# → PE32 executable (DLL) (console) ARMv7 ...

每个文件都是为特定的操作系统 + CPU 组合编译的;单个 JAR 无法包含“一刀切”的本地库。

如何通过库解决多架构问题

现代 Java 库采用的模式,使单个 Maven/Gradle 构件能够在多平台上工作。

“全部打包”模式

  • 它是什么:所有 支持平台的本机二进制文件打包进一个 JAR。
  • 它是如何工作的: 在运行时,库检测当前的操作系统/CPU,提取匹配的本机库到临时位置,并通过 System.load()System.loadLibrary() 加载。
  • 优点: 对终端用户来说依赖管理简单(只需一个构件)。
  • 缺点: JAR 包体积更大;提取开销;如果某些本机二进制文件的许可证受限,可能会出现许可证问题。

“平台/架构特定 JAR”模式

  • 它是什么: 为每个平台/架构发布单独的 JAR(例如 my-lib-linux-x86_64.jarmy-lib-windows-arm64.jar)。
  • 它是如何工作的: 主“core” JAR 只包含纯 Java 代码,并声明对平台特定 JAR 的 可选 依赖。构建工具(Maven、Gradle、sbt)会根据目标 classifier 解析出正确的变体。
  • 优点: 构件更小;无需运行时提取;每个平台的许可证更清晰。
  • 缺点: 依赖声明稍微复杂;用户必须确保拉取到正确的 classifier。

何时使用每种模式

情境推荐模式
平台数量少二进制体积可忽略Bundle Everything
平台数量多二进制体积较大(例如 OpenCV、TensorFlow)Platform‑Specific JARs
严格的许可证,且每个二进制文件的许可证不同Platform‑Specific JARs
需要快速启动(无需提取)Platform‑Specific JARs
简化分发(终端用户只需单一构件)Bundle Everything

Conclusion

Java的“Write Once, Run Anywhere”承诺对纯Java代码来说是成立的,这要归功于JVM的字节码抽象。一旦使用JNI进入本地领域,就会重新引入平台特定的顾虑。现代库通过将所有本地二进制文件打包成单个构件,或发布不同平台特定的构件来减轻这种痛苦。了解这些模式可以让你为项目选择合适的方法,并保持良好的开发者体验。

进一步阅读

  • The Java™ Native Interface: Programmer’s Guide and Specification – Sun Microsystems
  • JEP 391: macOS/AArch64 Port – OpenJDK
  • Class Loading and Native Libraries – Oracle Documentation

参考文献

  1. Oracle. Java Platform, Standard Edition Documentation. https://docs.oracle.com/javase/8/docs/
  2. Xerial. SQLite JDBC Driver. https://github.com/xerial/sqlite-jdbc
  3. OpenJDK. JEP 391: macOS/AArch64 Port. https://openjdk.org/jeps/391

UI) ARMv7 Thumb,适用于 MS Windows

% file native/Windows/aarch64/sqlitejdbc.dll
native/Windows/aarch64/sqlitejdbc.dll: PE32+ executable (DLL) (GUI) Aarch64, for MS Windows

x86 .so 文件在 ARM 上无法运行,反之亦然。Linux 的 .so 文件在 Windows 上也无法运行。当你尝试时,会看到著名的

java.lang.UnsatisfiedLinkError

“一次编写,随处运行”的承诺被打破——但仅在涉及本机代码时。

库如何解决多架构问题(扩展)

“打包所有内容”模式(再访)

最常见的解决方案:在单个 JAR 中包含所有平台的本机库。我们在前面的章节中已经看到 sqlite-jdbc 的做法。

运行时 Java 代码:

  1. 从系统属性检测操作系统和架构
  2. 从 JAR 中提取对应的本机库到临时位置。
  3. 使用 System.load() 加载库
  4. 在 JVM 退出时清理临时文件。

JAR 文件会更大(因为包含了多个本机库),但开发者体验是无缝的。

“平台/架构特定 JAR”模式(再访)

另一种做法:为每个平台发布单独的 JAR,让构建工具在构建时选择正确的那个。

例如,netty‑tcnative 提供了:

netty-tcnative-2.0.74.Final-javadoc.jar
netty-tcnative-2.0.74.Final-linux-x86_64-fedora.jar
netty-tcnative-2.0.74.Final-linux-x86_64.jar
netty-tcnative-2.0.74.Final-osx-aarch_64.jar
netty-tcnative-2.0.74.Final-osx-x86_64.jar
netty-tcnative-2.0.74.Final-sources.jar
netty-tcnative-2.0.74.Final.jar

一个小问题:如果在 pom.xml 中硬编码 classifier,构建就会变成平台特定的。

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-tcnative</artifactId>
    <version>2.0.74.Final</version>
    <classifier>linux-x86_64</classifier>
</dependency>

现在这个 pom.xml 只能在 Linux x86_64 上工作。macOS 开发者无法构建,且我们也无法部署到 ARM 服务器。

解决方案:Maven 的 os-maven-plugin

os‑maven‑plugin 能自动检测构建平台并选择正确的 JAR。

<plugin>
    <groupId>kr.motd.maven</groupId>
    <artifactId>os-maven-plugin</artifactId>
    <version>1.7.1</version>
</plugin>

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-tcnative</artifactId>
    <version>2.0.74.Final</version>
    <classifier>${os.detected.classifier}</classifier>
</dependency>

插件会根据构建环境设置 ${os.detected.classifier},因此同一个 pom.xml 能在每位开发者的机器以及所有 CI/CD 流水线中通用。

何时使用每种模式(扩展)

模式何时选择
打包所有内容- 向未知终端用户分发桌面应用
- 需要一个在所有环境都能运行的单一通用构件
- 将同一 JAR 部署到多种架构
- 简单性比 JAR 大小更重要
平台特定 JAR- 为已知平台构建服务器应用
- 使用容器化(每个容器镜像本身已是平台特定的)
- JAR 大小重要(例如 AWS Lambda 冷启动时间)
- 每种架构都有一致的 CI/CD 流水线

结论(扩展)

Java的“写一次,随处运行”承诺并非神话;只是在涉及本地代码时带有一个实际的注脚。通过上述模式,现代 Java 库即使依赖于特定架构的本地代码,也能保持跨平台兼容性。

进一步阅读(扩展)

  • Project Panama – 外部函数与内存 API,旨在用更安全、更简洁的 API 替代 JNI。
  • GraalVM Native Image – 将 Java 编译为本地二进制文件,彻底改变范式。
  • 改进的 JIT 编译 – 缩小 Java 与本地代码之间的性能差距。

如果您喜欢这篇文章,可能也会对我的另一篇关于 Simplifying the complicated world of Libraries 的帖子感兴趣。

参考文献(扩展)

Back to Blog

相关文章

阅读更多 »

在 Rust 中嵌入 JVM

Java ↔ Rust 互操作使用 jni Crate 作者:Ivan Yurchenko https://media2.dev.to/dynamic/image/width=50,height=50,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fd...

我的 TicketDesk 系统

简介:在我的编程入门模块中,我用 Java 开发了一个 TicketDesk 系统,能够:- 跟踪工单 - 跟踪登录信息 - 提供基于角色的身份验证……