1. 为什么在 Ubuntu 20.04 上用 apt 装 Java 不是“点几下鼠标”那么简单

很多人第一次在 Ubuntu 20.04 上装 Java,心里想的是:“不就是 sudo apt install default-jre 一行命令搞定?”结果敲完回车,系统报错 E: Unable to locate package default-jre ,或者装完一查 java -version ,显示的是 OpenJDK 11,而项目文档明确要求 JDK 17;又或者 javac 命令根本不存在,只装了 JRE 没装 JDK;更常见的是, JAVA_HOME 环境变量压根没设,后续跑 Maven、Gradle 或 Tomcat 全部报错——不是找不到 tools.jar ,就是提示 No Java runtime present

这些不是偶然,而是 Ubuntu 20.04 的 Java 生态设计逻辑决定的。它不像 Windows 那样提供一个“Java 官方安装包.exe”,也不像 macOS 用 Homebrew 默认拉最新版;Ubuntu 的 apt 仓库对 Java 的管理是 分版本、分用途、分维护主体 的:OpenJDK 是上游社区维护的开源实现,Canonical(Ubuntu 官方)只打包其中经过长期测试、满足 LTS(长期支持)策略的版本;而 Oracle JDK 自 2019 年起已停止免费商用,不再进入任何主流 Linux 发行版的默认仓库。所以当你执行 apt list --installed | grep java ,看到的不是“Java”,而是一长串带版本号和后缀的包名: openjdk-11-jre-headless openjdk-17-jdk java-common ca-certificates-java ……它们各自承担不同职责,缺一不可,也绝不能随意混装。

我刚接手一个遗留 Spring Boot 2.5 项目时就踩过这个坑:运维同事说“Java 已装好”,我连上服务器一查, java -version 显示 11.0.11,但 mvn clean package 直接失败,报错 Unsupported class file major version 61 ——这是 JDK 17 编译的字节码,而运行环境只有 JDK 11。后来发现他只装了 default-jre ,连 JDK 都没装,更别说配置 JAVA_HOME 。这种“看似装了,实则废柴”的状态,在 Ubuntu 20.04 的 Java 部署中太常见了。它不是 apt 不好用,而是你没理解 apt 在这个发行版里对 Java 的“分治逻辑”:它把 Java 拆成了 运行时(JRE)、开发套件(JDK)、头文件(headless)、证书支持(ca-certificates-java)、基础元数据(java-common) 五个独立可选组件。你必须按需组合,而不是指望一个“万能包”。

这也是为什么网络热搜里总出现 sudo: apt: command not found apt --fix-broken install ——前者往往是因为误删了 apt 本身(极罕见),更多是用户把 apt apt-get 混淆,或在非 root 用户下漏了 sudo ;后者则几乎全是 Java 包依赖冲突导致的,比如你先装了 openjdk-11-jdk ,又手动下载 .deb 包强行装 openjdk-17-jdk ,两个版本的 java-common 元数据打架,apt 就会拒绝继续操作,直到你运行 sudo apt --fix-broken install 强制修复依赖树。这不是 bug,是 apt 的自我保护机制在起作用。

所以,这篇内容不教你怎么“一键安装”,而是带你 看清 Ubuntu 20.04 的 apt 仓库里 Java 的真实地图 :哪些包是必装的,哪些是可选的,哪些是陷阱,以及每一步操作背后,apt 究竟在帮你做什么、又在防止你做什么。你不需要记住所有命令,但需要建立一个判断框架:当项目要求“JDK 17”时,你知道该搜 openjdk-17-jdk 而不是 java-17-openjdk ;当 CI 流水线报 JAVA_HOME not set 时,你知道该去 /usr/lib/jvm/ 下找路径,而不是瞎猜 /opt/java ;当 sudo apt update 404 Not Found 时,你知道该检查 /etc/apt/sources.list 里是否还残留着已废弃的 old-releases.ubuntu.com 镜像源。这才是真正能让你在 Ubuntu 20.04 上稳定交付 Java 应用的底层能力。

1.1 Ubuntu 20.04 的 Java 仓库结构:三个层级与两个生命周期

要真正用好 apt 装 Java,你得先看懂它的仓库布局。Ubuntu 20.04(Focal Fossa)的官方软件源分为三个主要层级: main universe multiverse 。Java 相关包全部位于 universe 层级,这意味着它们 由社区维护,而非 Canonical 官方直接支持 。这直接决定了两点:第一,更新频率低于 main 中的核心系统组件;第二,版本选择更保守——只收录经过充分测试、满足 LTS 要求的 OpenJDK 版本。

具体到 Java,20.04 的 universe 仓库中,长期支持(LTS)版本只有两个: OpenJDK 11 和 OpenJDK 17 。前者是 Ubuntu 20.04 发布时的默认 JDK,后者是在 2021 年 10 月随 OpenJDK 17 GA(General Availability)发布后,被同步进 focal-updates 仓库的。注意,这里没有 OpenJDK 13、14、15、16——它们属于短期支持(STS)版本,Ubuntu 官方不会打包进任何 LTS 发行版的仓库,因为其生命周期太短(仅 6 个月),无法匹配 Ubuntu 20.04 长达 5 年的桌面支持期和 10 年的服务器支持期。

我们来实际验证一下。打开终端,执行:

apt update && apt list -a openjdk-*

你会看到类似这样的输出(截取关键部分):

Listing... Done
openjdk-11-jdk/focal-updates,now 11.0.22+8-0ubuntu1~20.04.1 amd64 [installed]
openjdk-11-jdk-headless/focal-updates,now 11.0.22+8-0ubuntu1~20.04.1 amd64 [installed,automatic]
openjdk-11-jre/focal-updates,now 11.0.22+8-0ubuntu1~20.04.1 amd64 [installed,automatic]
openjdk-11-jre-headless/focal-updates,now 11.0.22+8-0ubuntu1~20.04.1 amd64 [installed,automatic]
openjdk-17-jdk/focal-updates,now 17.0.10+7-0ubuntu1~20.04.1 amd64
openjdk-17-jdk-headless/focal-updates,now 17.0.10+7-0ubuntu1~20.04.1 amd64
openjdk-17-jre/focal-updates,now 17.0.10+7-0ubuntu1~20.04.1 amd64
openjdk-17-jre-headless/focal-updates,now 17.0.10+7-0ubuntu1~20.04.1 amd64

注意观察包名后缀:

  • -jdk :完整的 Java 开发工具包,包含 javac javadoc jdb 等编译和调试工具, 开发必备
  • -jdk-headless :无图形界面的 JDK,不含 AWT/Swing 等 GUI 类库,体积更小, 服务器部署首选
  • -jre :Java 运行时环境,仅含 java 命令和核心类库, 纯运行应用够用
  • -jre-headless :无 GUI 的 JRE,最精简, 容器化或嵌入式场景常用

java-common 这个包,虽然名字不起眼,却是整个 Java 生态的“地基”。它不提供任何可执行命令,只负责管理 /usr/lib/jvm/ 目录结构、符号链接(如 /usr/bin/java 指向哪个具体版本)、以及 update-alternatives 的注册信息。如果你手动删除了 java-common ,再装任何 JDK, java -version 可能依然报错,因为符号链接断了。这就是为什么 apt --fix-broken install 经常能修好 Java 问题——它本质上是在重建 java-common 所维护的这套元数据关系。

再来看生命周期。OpenJDK 11 在 Ubuntu 20.04 中的生命周期,与其上游一致:Oracle 提供的 OpenJDK 11 LTS 支持到 2026 年 10 月,因此 Ubuntu 会持续为其提供安全更新,直到 2026 年。而 OpenJDK 17 的支持期到 2029 年 9 月,Ubuntu 也会同步跟进。这意味着,如果你今天在 20.04 上装 OpenJDK 17,它在未来 5 年内都会收到 Canonical 提供的安全补丁,无需升级操作系统。这正是 LTS 发行版的核心价值: 稳定,可预测,免于频繁迁移

提示:不要试图用 apt install oracle-java17-installer 。这个包早已从 universe 仓库移除,因为它依赖的 Oracle JDK 二进制文件不再提供免费下载链接,apt 无法自动获取。强行添加第三方 PPA(如 webupd8team/java )不仅违反 Ubuntu 官方安全策略,还会导致系统包管理器混乱,是生产环境的大忌。

1.2 “default-jre” 和 “default-jdk”:一个被严重误解的“快捷方式”

在大量中文教程里,你一定会看到这两行命令:

sudo apt install default-jre
sudo apt install default-jdk

它们看起来是“最简单、最官方”的安装方式。但真相是: default-* 系列包,是 Ubuntu 为新手准备的“概念性入口”,而非生产环境推荐方案 。它们的作用,是提供一个指向当前“默认”版本的符号链接,这个“默认”由 Canonical 的包维护者手动设定,并非技术最优解。

我们来拆解 default-jdk 到底是什么。执行:

apt show default-jdk

输出中关键一行是:

Depends: openjdk-11-jdk (>= 11.0.22+8-0ubuntu1~20.04.1)

看到了吗? default-jdk 本身不包含任何 Java 代码,它只是一个 元包(metapackage) ,唯一作用就是强制依赖 openjdk-11-jdk 。也就是说, sudo apt install default-jdk 的效果,等价于 sudo apt install openjdk-11-jdk 。它只是给你一个“不用记版本号”的心理安慰。

但问题来了:如果你的项目需要 JDK 17,而你装了 default-jdk ,那你就永远卡在 JDK 11。 default-jdk 不会自动升级到 JDK 17,除非 Canonical 的维护者手动修改其 Depends 字段,并发布新版本的 default-jdk 包——而这通常发生在下一个 Ubuntu LTS 版本(如 22.04)发布时。在 20.04 的生命周期内, default-jdk 永远指向 JDK 11。

更隐蔽的坑在于 update-alternatives 的优先级机制。当你同时装了 openjdk-11-jdk openjdk-17-jdk ,系统会通过 update-alternatives --config java 来让你选择默认 java 命令指向哪个版本。但 default-jdk 的存在,会让 update-alternatives 的自动模式(auto mode)产生歧义。因为 default-jdk 注册的替代项(alternative)优先级是 1000,而 openjdk-11-jdk 是 1101, openjdk-17-jdk 是 1701。理论上,数字越大优先级越高, openjdk-17-jdk 应该胜出。但如果你先装 default-jdk (它拉了 JDK 11),再装 openjdk-17-jdk update-alternatives 有时会因为元数据缓存问题,不自动切换,导致 java -version 仍显示 11。

我遇到过最典型的案例:一个团队的 CI 服务器,管理员为了“省事”统一装了 default-jdk ,结果所有构建都用 JDK 11。半年后项目升级到 Spring Boot 3,要求 JDK 17,他们花了两天时间排查,才发现 default-jdk 是罪魁祸首。最后解决方案不是卸载它,而是 显式指定版本安装,并手动配置 JAVA_HOME

所以,我的建议非常明确: 在生产环境、CI/CD 流水线、或任何需要版本确定性的场景,永远使用 openjdk-XX-jdk 的全名安装,彻底抛弃 default-* 系列包 。它带来的那点“便捷”,远不如版本失控带来的风险大。 default-jdk 的唯一合理使用场景,是给完全不懂 Java 的新用户做一次性的、临时的演示环境搭建——比如你在咖啡馆帮朋友快速跑一个 Hello World,用完即删。

注意: default-jre 同理,它只依赖 openjdk-11-jre 。如果你只需要运行一个 .jar 文件,且确认该文件是用 JDK 11 编译的,那它没问题;但一旦涉及编译、调试、或版本敏感的运行时(如某些 JNI 库),就必须用具体版本的 JDK。

2. 从零开始:四步完成 JDK 17 的完整安装与验证

现在,我们抛开所有“快捷方式”,用最清晰、最可控的方式,在 Ubuntu 20.04 上完成 OpenJDK 17 的安装。整个过程分为四个原子步骤: 环境预检 → 包安装 → 环境变量配置 → 全面验证 。每一步都不可跳过,且每一步都有其不可替代的技术目的。

2.1 步骤一:环境预检——为什么 sudo apt update 必须成功?

很多教程把 sudo apt update 当作一句“例行公事”的命令,一笔带过。但在 Java 安装前,这一步是 整个流程的基石和守门员 。它的作用,远不止是“刷新软件包列表”。

apt update 的核心任务,是下载并解析 /var/lib/apt/lists/ 目录下的所有 Packages 文件。这些文件是 Ubuntu 官方镜像站(如 archive.ubuntu.com )生成的索引数据库,里面精确记录了每个包的名称、版本、依赖关系、校验和(SHA256)、以及它在仓库中的物理路径( .deb 文件 URL)。当你执行 apt install openjdk-17-jdk 时,apt 不是凭空下载,而是先查这个索引,确认 openjdk-17-jdk 这个包是否存在、有哪些可用版本、它依赖哪些其他包(如 openjdk-17-jre-headless , ca-certificates-java ),然后才去镜像站拉取对应的 .deb 文件。

所以,如果 apt update 失败,原因只有一个: 你的系统无法访问或解析这些索引文件 。常见原因有三类:

  1. 网络连接问题 :公司防火墙屏蔽了 archive.ubuntu.com ,或 DNS 解析失败。此时 ping archive.ubuntu.com 会超时, curl -I http://archive.ubuntu.com/ubuntu/dists/focal-updates/ 会返回 Connection refused
  2. 源地址失效 :Ubuntu 20.04 发布初期,部分国内镜像源(如 mirrors.ustc.edu.cn )的 focal-updates 分支尚未同步完成,导致 apt update 404 Not Found 。这在 2020 年很常见,现在基本已解决。
  3. sources.list 配置错误 :最致命的是,你可能手动修改过 /etc/apt/sources.list ,把 focal-updates 写成了 focal-security ,或者误加了 old-releases.ubuntu.com (这是为已 EOL 的旧版 Ubuntu 准备的,20.04 还在支持期内,不该用)。

我曾在一个客户现场遇到过一个诡异问题: apt update 总是卡在 Reading package lists... 99% ,持续十分钟不动。排查发现,他们的 sources.list 里有一行:

deb http://archive.ubuntu.com/ubuntu/ focal main restricted universe multiverse

但缺少了 focal-updates focal-security 这两行。 focal 主仓库只包含 20.04 发布时的初始版本(如 OpenJDK 11.0.7),而 openjdk-17-jdk 是后来发布的,必须在 focal-updates 仓库中才能找到。没有这行, apt update 虽然能完成,但 apt list openjdk-17-jdk 会返回 No packages found ,因为索引里根本没有这个包。

因此,预检的正确姿势是:

# 1. 检查网络连通性
ping -c 3 archive.ubuntu.com

# 2. 检查 sources.list 是否完整(关键!)
grep -E "focal-updates|focal-security" /etc/apt/sources.list

# 你应该看到类似这样的两行:
# deb http://archive.ubuntu.com/ubuntu/ focal-updates main restricted universe multiverse
# deb http://archive.ubuntu.com/ubuntu/ focal-security main restricted universe multiverse

# 3. 执行 update 并观察输出
sudo apt update 2>&1 | grep -E "(openjdk|Hit|Ign|Err)"

如果输出中出现 Err:... Failed to fetch ,说明源有问题,必须先修复。如果一切正常,最后一行应该是 Reading package lists... Done ,且中间有 Hit:... focal-updates 的提示,这就意味着 focal-updates 仓库已成功加载, openjdk-17-jdk 就在那里等着你。

提示:不要迷信“国内镜像源一定更快”。有些小众镜像源同步延迟高达 24 小时,可能导致你 apt update 成功,却 apt install 失败(因为索引里有包名,但镜像站物理文件还没同步)。生产环境建议优先使用 archive.ubuntu.com 官方源,或 security.ubuntu.com (专用于安全更新)。

2.2 步骤二:包安装—— openjdk-17-jdk openjdk-17-jdk-headless 的抉择

现在, apt update 已确认成功,我们可以正式安装 JDK 了。命令很简单:

sudo apt install openjdk-17-jdk

但这里有一个关键决策点: 你是否需要 openjdk-17-jdk-headless

答案取决于你的使用场景。 -headless 后缀,直译是“无头”,在 Java 语境中特指 不包含图形用户界面(GUI)子系统的运行时 。它移除了 java.awt javax.swing java.applet 等所有与屏幕绘制、事件处理相关的类库。这带来了两个显著优势:

  • 体积更小 openjdk-17-jdk 完整包约 350MB,而 openjdk-17-jdk-headless 仅约 220MB。对于磁盘空间紧张的服务器或 Docker 镜像,这 130MB 的节省很可观。
  • 启动更快、内存占用更低 :JVM 启动时无需初始化 AWT Toolkit、X11 连接等 GUI 相关模块,冷启动时间平均快 15%-20%,堆外内存(off-heap)占用减少约 8%。

那么,什么情况下你 不能 -headless ?只有两种:

  1. 你的 Java 应用本身需要 GUI :比如一个 Swing 桌面程序,或一个用 JavaFX 做前端的工具。但请注意,Ubuntu 20.04 的 openjdk-17-jdk 默认 不包含 JavaFX ,它已被 Oracle 移出 OpenJDK 标准,需单独下载 gluonhq/javafx SDK。所以即使你装了完整版 JDK,也未必能跑 JavaFX。
  2. 你依赖的某个第三方库,内部调用了 AWT 的 headful 功能 :最典型的是 java.awt.Toolkit.getDefaultToolkit().getScreenSize() 这类方法。如果库作者没做 headless 兼容(即没加 if (!GraphicsEnvironment.isHeadless()) { ... } 判断),在 -headless 环境下会抛 HeadlessException

绝大多数后端服务、Web 应用(Spring Boot, Tomcat)、构建工具(Maven, Gradle)、数据处理(Spark, Flink)都 完全兼容 -headless 。它们从不碰 GUI API,只用 java.lang , java.util , java.net , java.nio 等核心包。

因此,我的生产环境推荐是: 无脑选择 openjdk-17-jdk-headless 。命令如下:

sudo apt install openjdk-17-jdk-headless

它会自动拉取所有必要依赖,包括 openjdk-17-jre-headless ca-certificates-java java-common 。你不需要单独装它们,apt 的依赖解析器会帮你搞定。

安装过程中,apt 会显示类似这样的依赖树:

The following additional packages will be installed:
  ca-certificates-java java-common libasound2 libasound2-data libavahi-client3
  libavahi-common3 libdbus-1-3 libfontconfig1 libfreetype6 libgcc-s1 libglib2.0-0
  libgraphite2-3 libharfbuzz0b libjpeg-turbo8 libjpeg8 liblcms2-2 libnspr4 libnss3
  libpcsclite1 libpng16-16 libstdc++6 libx11-6 libx11-data libxau6 libxcb1 libxdmcp6
  libxext6 libxi6 libxrender1 libxtst6 openjdk-17-jre-headless tzdata-java

注意,这里没有 libx11-xcb1 libxrandr2 等典型的 X11 图形库——这正是 -headless 的体现。如果你看到这些包被列为“将被安装”,那说明你误装了完整版 openjdk-17-jdk

安装完成后,JDK 的文件被放在 /usr/lib/jvm/ 目录下。执行 ls -l /usr/lib/jvm/ ,你会看到:

drwxr-xr-x 7 root root 4096 Oct 12 10:23 java-1.17.0-openjdk-amd64/
lrwxrwxrwx 1 root root   25 Oct 12 10:23 java-17-openjdk-amd64 -> java-1.17.0-openjdk-amd64/

java-1.17.0-openjdk-amd64 是实际安装目录, java-17-openjdk-amd64 是一个符号链接,方便记忆。这个路径,就是下一步配置 JAVA_HOME 的关键。

提示:不要用 which java 的结果来反推 JAVA_HOME which java 返回的是 /usr/bin/java ,这是一个由 update-alternatives 管理的符号链接,它最终指向 /etc/alternatives/java ,再指向 /usr/lib/jvm/java-1.17.0-openjdk-amd64/bin/java 。绕这么多层,不如直接认准 /usr/lib/jvm/java-1.17.0-openjdk-amd64 这个物理路径,绝对可靠。

2.3 步骤三:环境变量配置—— JAVA_HOME PATH 的黄金搭档

安装完 JDK, java javac 命令已经可以全局使用了,但这只是“表面功夫”。真正的挑战在于 JAVA_HOME 环境变量的配置。它是 Java 生态的“心脏起搏器”,几乎所有基于 Java 的工具都依赖它:

  • Maven :用它定位 tools.jar (虽然 JDK 9+ 已移除,但插件仍会读取)和 jre/lib/rt.jar
  • Gradle :用它确定默认的 JVM 启动参数(如 -Xmx )和编译目标版本。
  • Tomcat :用它设置 CATALINA_HOME JRE_HOME ,并加载 tomcat-juli.jar
  • IDEA/VS Code :用它识别项目 SDK 和语言级别。
  • Docker 构建 Dockerfile ENV JAVA_HOME 是构建多阶段镜像的基础。

所以,配置 JAVA_HOME 不是“可选项”,而是“必选项”。而且,它必须与 PATH 协同工作,形成闭环。

标准做法是编辑 /etc/environment (系统级,对所有用户生效)或 ~/.profile (用户级)。我强烈推荐 系统级配置 /etc/environment ,因为:

  • 它在用户登录前就被 shell 读取,确保所有进程(包括 systemd 服务、cron 任务、GUI 应用)都能继承。
  • 它不执行任何 shell 脚本,只做纯变量赋值,避免了 ~/.bashrc 中可能存在的语法错误导致整个 shell 启动失败的风险。

操作步骤:

# 1. 用 nano 编辑(或你喜欢的编辑器)
sudo nano /etc/environment

# 2. 在文件末尾添加两行(注意:不要加 export,/etc/environment 不是 shell 脚本)
JAVA_HOME="/usr/lib/jvm/java-1.17.0-openjdk-amd64"
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:$JAVA_HOME/bin"

# 3. 保存退出,然后让新环境变量立即生效(对当前会话)
source /etc/environment

# 4. 验证
echo $JAVA_HOME
# 输出应为:/usr/lib/jvm/java-1.17.0-openjdk-amd64

echo $PATH | tr ':' '\n' | grep jvm
# 输出应包含:/usr/lib/jvm/java-1.17.0-openjdk-amd64/bin

这里有个极易被忽略的细节: PATH 的赋值。很多教程写成 PATH="$JAVA_HOME/bin:$PATH" ,这看似正确,但会导致一个问题: /usr/bin 等系统路径被挤到后面,万一 JAVA_HOME/bin 下有个叫 ls 的恶意脚本(当然不可能,但原理如此),它就会被优先执行。所以, 最佳实践是把 $JAVA_HOME/bin 加在 PATH 的末尾 ,确保系统命令优先级最高,Java 工具次之。上面的写法 PATH="...:$JAVA_HOME/bin" 就是遵循此原则。

配置完成后,重启一个新终端,执行 env | grep JAVA ,你应该看到:

JAVA_HOME=/usr/lib/jvm/java-1.17.0-openjdk-amd64

如果没看到,说明 /etc/environment 没生效。常见原因是:你用的是 ~/.bashrc ,而 ~/.bashrc 默认不读取 /etc/environment (它只读取 /etc/profile )。此时,你需要在 ~/.bashrc 末尾添加:

# Load system environment variables
if [ -f /etc/environment ]; then
    . /etc/environment
fi

然后 source ~/.bashrc

注意: JAVA_HOME 的值必须是 绝对路径,且不能有尾部斜杠 。写成 /usr/lib/jvm/java-1.17.0-openjdk-amd64/ (带 / )会导致某些工具(如旧版 Ant)解析失败,报 Invalid JAVA_HOME

2.4 步骤四:全面验证——不只是 java -version

安装和配置完成后,别急着庆祝。真正的验证,是模拟一个真实的应用场景,用一套组合拳,确保每个环节都坚如磐石。我把它总结为“四验法”:

验一:基础命令与版本一致性
# 1. 检查 java 命令
java -version
# 输出应为:openjdk version "17.0.10" ...

# 2. 检查 javac 命令(证明 JDK 而非仅 JRE)
javac -version
# 输出应为:javac 17.0.10

# 3. 检查 JAVA_HOME 是否被正确读取
echo $JAVA_HOME
# 输出应为:/usr/lib/jvm/java-1.17.0-openjdk-amd64

# 4. 检查 java 命令的实际路径
readlink -f $(which java)
# 输出应为:/usr/lib/jvm/java-1.17.0-openjdk-amd64/bin/java

这四条命令,必须全部通过。特别是第 4 条 readlink -f ,它能穿透所有符号链接,直达物理文件。如果这里显示的是 /usr/lib/jvm/java-11-openjdk-amd64/bin/java ,说明 update-alternatives 没切过来,或者 PATH 里有别的 java 在前面。

验二: update-alternatives 状态检查
# 查看 java 的替代项配置
sudo update-alternatives --config java

# 查看 javac 的替代项配置
sudo update-alternatives --config javac

你应该看到类似这样的输出:

There are 2 choices for the alternative java (providing /usr/bin/java).

  Selection    Path                                            Priority   Status
------------------------------------------------------------
* 0            /usr/lib/jvm/java-17-openjdk-amd64/jre/bin/java   1701      auto mode
  1            /usr/lib/jvm/java-11-openjdk-amd64/jre/bin/java   1101      manual mode
  2            /usr/lib/jvm/java-17-openjdk-amd64/jre/bin/java   1701      manual mode

Press <enter> to keep the current choice[*], or type selection number:

注意 Priority 列: 1701 > 1101 ,所以 auto mode 会自动选 17。 * 号表示当前生效项。如果 * 在 11 那行,说明你需要手动输入 0 2 来切换。

验三:编译与运行一个真实 Java 类

这是最硬核的验证。创建一个最小可运行单元:

# 1. 创建测试目录
mkdir -p ~/java-test && cd ~/java-test

# 2. 创建 HelloWorld.java
cat > HelloWorld.java << 'EOF'
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello from OpenJDK 17 on Ubuntu 20.04!");
        System.out.println("JAVA_HOME: " + System.getenv("JAVA_HOME"));
        System.out.println("Java Version: " + System.getProperty("java.version"));
    }
}
EOF

# 3. 编译
javac HelloWorld.java

# 4. 运行
java HelloWorld

预期输出:

Hello from OpenJDK 17 on Ubuntu 20.04!
JAVA_HOME: /usr/lib/jvm/java-1.17.0-openjdk-amd64
Java Version: 17.0.10

这个测试同时验证了:

  • javac 能正常工作(JDK 安装正确)
  • java 能加载并执行 class 文件(JRE 运行时正确)
  • JAVA_HOME 环境变量在 JVM 进程内可被 System.getenv() 读取(环境变量透传成功)
验四:Maven/Gradle 兼容性(可选但强烈推荐)

如果你的项目要用 Maven,再加一验:

# 安装 Maven(如果还没装)
sudo apt install maven

# 创建一个最小 pom.xml
cat > pom.xml << 'EOF'
<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>
  <groupId>test</groupId>
  <artifactId>hello-world</artifactId>
  <version>1.0</version>
  <properties>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
  </properties>
</project>
EOF

# 运行 mvn compile(它会触发下载依赖,但至少能走到编译阶段)
mvn compile -q 2>/dev/null && echo "✅ Maven compilation successful" || echo "❌ Maven failed"

如果输出 ✅ Maven compilation successful ,恭喜,你的 JDK 17 已经可以投入生产使用了。

3. 常见故障全景图:从 apt --fix-broken install JAVA_HOME not set

在 Ubuntu 20.04 上装 Java,90% 的问题都集中在几个经典故障点。它们不是随机发生的,而是有清晰的因果链。下面,我以一个真实的排错日志为线索,带你走一遍完整的“故障定位-分析-修复”全流程。这个日志,来自一个 CI 服务器的构建失败报告:

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project my-app: Fatal error compiling: invalid target release: 17 -> [Help 1]
...
[INFO] -------------------------------------------------------
[INFO

更多推荐