你有没有过这种经历:在 start.spring.io(Spring Initializr)上选好 Spring Boot 版本、点几下鼠标下载项目压缩包,IDEA 里一打开,还没写任何业务代码,编译就直接抛红:java: 无效的源发行版: 16。
这个报错我前后帮人排查过不下十次。说句实话,它本身不是个难问题,但它特别能折磨人——因为报错指向的是 "Java 16",很多人第一反应是"我机器上装的是 JDK 8/11,那我把 16 卸了/换了不就行了",结果换完还是报错,或者不知道去哪一步换。这篇文章把这个问题从现象到根因完整拆一遍,覆盖 IDEA + Maven / Gradle、命令行、以及打包机上同样的坑。如果你正被invalid source release: 16这类红色报错卡住,照这条链路走一遍,基本能根治。
1. 先把报错翻译成人话:编译器要的是 Java 16,你机器里根本没有
先别着急改配置,要把这行报错读懂。无效的源发行版:16对应的英文是invalid source release: 16,它不是一个"警告"或"提示",而是javac编译器在启动时给的硬错误。意思非常直白:你要求编译器用 Java 16 的语法规则来编译源码,但当前正在执行的 JDK 根本不认识 Java 16 这个版本号。
也就是说,这个问题不是"代码写错了",而是"编译工具链的版本不对齐"。常见情况是:你本机装的是 JDK 8 或 JDK 11,但项目里通过 Maven 插件或者 IDEA 配置,把编译参数指定成了-source 16 -target 16。javac 检查到当前自己是 1.8 或 11,看到目标版本 16 超出自己的能力范围,直接拒绝干活。
用一个生活化的类比:javac 就像一个只装了"英文翻译包"的翻译机,你递给它一份法语文件,要求它翻成法语。它不会瞎猜,不会硬着头皮勉强翻,而是直接罢工告诉你"这个语言我不认识"。
你可以直接在命令行里复现这个机制。装 JDK 8 的机器上:
$ java -version openjdk version "1.8.0_382" Java(TM) SE Runtime Environment (build 1.8.0_382-b05) $ javac -source 16 -target 16 Test.java javac: 错误: 无效的源发行版: 16JDK 11 上结果类似,只是报错文字可能是 "invalid flag: 16",或者 "release version 16 not supported",本质都一样。编译器不会因为你代码里没用到什么新特性就放你一马,-source参数的版本号必须小于等于当前 javac 自己支持的版本,否则一律拒绝。
这里还有一个反直觉的点,很多人以为"Java 16 也是个稳定版本,编译器版本低点无所谓"。实际上 javac 的-source/-target只能向下兼容一两个版本,JDK 8 连 Java 9 都不认识,更别说 16。每代 JDK 编译器的"知识范围"是固定的,它支持的源发行版列表里没有的就是没有,不会自动降级,也不会猜测你要什么。
2. 项目是 Initializr 生成的,为什么它会默认生成 Java 16
搞清楚报错原理后,下一个自然的问题是:我这项目明明是 Spring Initializr 生成的,它为什么给我搞出一个本机没有的 Java 16?
原因在于 start.spring.io 生成项目时,pom.xml 里有一个关键属性:
<properties> <java.version>16</java.version> </properties>而你从官网下载项目时,页面上有一个Java 版本下拉框,它默认值取决于你选的 Spring Boot 版本。很多人创建项目时注意力全在 Spring Boot 版本、依赖选择上,Java 下拉框压根没注意,直接用默认值,于是项目就在本地被声明成了 Java 16。把时间线拉长一点看更清楚:
- Spring Boot 2.4.x 时代,Initializr 默认 Java 是 11。
- Spring Boot 2.5.x 时代,也就是 2021 年那会儿,Spring 官方把默认 Java 版本提到了 16,当时 Java 16 正好是 LTS 之后的短期版本,社区讨论度很高。
- 后来 Spring Boot 3.x 把基线直接抬到 Java 17,所以现在的 start.spring.io 默认基本是 17 或 21。
如果你用的教程、笔记、视频是两三年前的,或者你当时创建项目的时候网页刚好停留在某个旧选项组合上,download 下来的项目很可能就是java.version=16。还有一部分情况是:你用了内网私有化的 Initializr 服务,那个服务版本很老,默认值自然也是旧版本的 16。
Spring Boot 父 POM(spring-boot-starter-parent)会读取这个java.version属性,自动帮你配置maven-compiler-plugin的 source/target:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>16</source> <target>16</target> </configuration> </plugin>也就是说,你在网站上点的那个看似不起眼的 "Java 16" 下拉框,最后会原封不动变成编译器参数。你本机没有 JDK 16,就触发了第一节那个硬错误。
3. 从报错来源往下追:四个配置点怎么互相打架
现在正式进入排查环节。见过太多人一上来就改 pom.xml,改完没用又改 IDEA 设置,改完还是没用,因为根本不知道这个报错是由哪一层设置触发的。把下面几个检查点按顺序过一遍,基本能锁定问题。
3.1 先确认报错是 IDEA 编译器报的,还是 Maven 构建报的
看控制台的报错位置和信息格式:
- 如果报错出现在 IDEA 底部的Build / Compiler 输出,格式是
java: 无效的源发行版: 16,这是 IDEA 自带的编译器在调 javac。 - 如果报错出现在 Maven 面板的构建日志里,格式一般是
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:...:compile ... invalid target release: 16,这是 Maven 调的编译器。
虽然两个来源最终都指向同一个 javac 参数问题,但它们的配置入口不同。先区分来源,才不会在错误的地方瞎折腾。
3.2 命令行确认你现在到底用的是哪个 JDK
这一步是地基,建议先做。打开终端:
java -version javac -version echo $JAVA_HOME三条命令的结果要保持一致。很多机器上java -version显示的是 1.8,但JAVA_HOME指向的可能是已经装好的 JDK 17 路径,或者反过来。IDEA 和 Maven 有时候各自有独立的 JDK 来源,不一定跟终端的 PATH 一致,但我们要先把这个基准摸清楚。
这里有个非常常见的隐藏坑:你已经装了 JDK 17,只是终端用的还是旧 JDK。Windows 上安装 JDK 17 之后,PATH 里如果旧 JDK 的路径排在前面,终端永远调的是旧的。Linux/macOS 上则可能是alternatives或.bashrc里的 export 顺序不对。后面第 5 节单独说。
3.3 IDEA 里的三个设置点,经常互相覆盖
IDEA 对编译级别有多个"页签"级别的设置,它们是有优先级关系的,很多人只改其中一个,所以改完没效果。
首先是最显眼的Project Structure。快捷键Ctrl+Shift+Alt+S打开,看两个地方:
Project -> SDK:当前项目用的是哪个 JDK。Project -> Project language level:这个下拉框定义了语言级别(8、11、16、17),它影响的是 IDEA 内置编译器的-source参数。
这两个地方必须互相匹配,并且和项目的java.version匹配。如果 Project SDK 是 1.8,而 language level 是 16,IDEA 的编译器直接就会报无效的源发行版: 16。
其次是Settings -> Build, Execution, Deployment -> Compiler -> Java Compiler。这里有一个 per-module 的字节码版本设置,它覆盖 Project 级别的 language level。如果之前手贱在这个表里给某个 module 单独设了 target bytecode version,那 Project Structure 里怎么改都压不住它。
第三处是 Maven 项目特有的Settings -> Build, Execution, Deployment -> Build Tools -> Maven -> Runner -> JRE。这一个是我见过最隐蔽的坑。它的作用是指定 Maven 构建进程(那个跑mvn的 JVM)用哪个 JDK。当你点 Maven 面板里的 compile 时,走的是这里。如果这个下拉框选的是Project SDK (1.8)或者某个旧 JDK,那即使你 Project SDK 已经换成了 17,Maven 构建用的还是 1.8,pom.xml 里写的 java.version=16 照样让它报错。
我把四个配置点汇总成一张表,方便对照:
| 配置点 | 位置 | 影响范围 | 常见坑 |
|---|---|---|---|
| Project SDK | Project Structure -> Project | 整个项目的运行和编译基准 | 下拉框里有 17,但实际没装对应 JDK,只是历史残留 |
| Project language level | Project Structure -> Project | IDEA 内置编译器的 source 级别 | 和 Project SDK 版本不匹配 |
| per-module bytecode version | Settings -> Build -> Java Compiler | 覆盖 language level | 改过之后忘了,全局怎么改都不生效 |
| Maven Runner JRE | Settings -> Maven -> Runner | Maven 构建进程的 JDK | 锁死旧 JRE,无视 Project SDK 的变化 |
3.4 Gradle 项目还有一个专属配置
如果你的项目不是 Maven 而是 Gradle,那除了上面 IDEA 层面的设置外,还要注意 Gradle 构建进程本身使用哪个 JDK。Gradle 从 7.3 开始支持完整的 Java 17 编译,但你如果项目里gradle/wrapper/gradle-wrapper.properties指向的是一个老版本 Gradle(比如 6.x),它对 JDK 16 的支持也是不完整的,报错形态更多样。
Gradle 的 JDK 可以通过gradle.properties指定:
org.gradle.java.home=/usr/lib/jvm/java-17-openjdk或者通过环境变量JAVA_HOME决定。Gradle 的坑在于:即使 IDEA 的 Gradle JVM 设置里选对了 JDK,命令行直接跑./gradlew build时用的又是另一套环境。所以 Gradle 项目一定要确认命令行和 IDE 两边用的是同一个 JDK。
4. 落地修复:换 JDK 还是降编译级别,看你用哪个 Spring Boot 版本
排查完四个配置点,接下来才是真正的修复动作。修复方案其实只有两条路:让项目匹配本机已有的 JDK,或者让本机的 JDK 匹配项目。选哪条路,不取决于你的心情,而取决于 Spring Boot 的版本。
在动手之前,先记住一张兼容性对照表:
| Spring Boot 版本 | 最低 Java 要求 | 是否可用 Java 16 | 是否能降级到 Java 8 |
|---|---|---|---|
| 2.3.x / 2.4.x | Java 8 | 可以 | 可以 |
| 2.5.x / 2.6.x | Java 8 | 可以 | 可以 |
| 2.7.x | Java 8 | 可以 | 可以 |
| 3.x | Java 17 | 不能(Spring Framework 6 要求 17+) | 不能 |
如果是 Spring Boot 2.5 或 2.6 这种项目,POM 里声明 Java 16 是正常的,你既可以把 JDK 升级到 16 或 17,也可以把编译级别降回本机的 JDK 8/11。但如果你的项目是 Spring Boot 3.x,那么最低门槛就是 Java 17,Java 16 本身就不满足要求,正确的做法是把 JDK 升到 17 或更高,而不是把编译级别降下去——降到 8 或 11 会让项目直接跑不起来。
4.1 推荐路线:装一个和项目匹配的 JDK
我的建议是,不要折腾降级,直接装 JDK 17。理由有三点:
- Spring Boot 3.x 本身就是 17 起步,以后你升级 Boot 版本也不用再换 JDK。
- JDK 17 是 LTS,社区支持、各种插件兼容性都比 16 好。Java 16 只是个过渡版本,很多老版本 Lombok 在 16 上都有兼容问题,在 17 上反而早就修好了。
- 你只需要在 IDEA 里点几下,不用改任何项目文件。
操作步骤:
- 到 Adoptium 官网或你常用的镜像站下载 OpenJDK 17,Windows 直接 exe 安装,macOS 装 dmg,Linux 解压 tarball。
- IDEA 里
File -> Project Structure -> SDKs -> 加号 -> Add JDK,选择你刚安装的 JDK 路径。 - 在
Project -> SDK下拉框里选中刚添加的 JDK 17,Project language level同步选 17。 - 检查第 3 节那四个配置点,确保 Maven Runner 和 Java Compiler 里没有残留旧 JDK 的设置。
- 回到 IDEA,点 Maven 面板的刷新按钮(Reload All Maven Projects),或者右键 pom.xml -> Maven -> Reload project。
- 重新编译。
4.2 妥协路线:把编译级别降到本机 JDK
如果因为各种原因实在不想装新 JDK(比如公司网速太慢、机器权限受限),也可以把项目编译级别降到本机已有的 JDK。操作如下:
在 pom.xml 里修改:
<properties> <java.version>11</java.version> </properties>把java.version改成你本机 JDK 对应的版本(8 就写 8,11 就写 11)。如果项目的 POM 里还显式配置了maven-compiler-plugin,而且写死了 source/target,也要同步改:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>11</source> <target>11</target> </configuration> </plugin>同时把 IDEA 的 Project language level 同步改成 11。然后重复步骤 5 和 6。
这里必须提醒一句:不要直接去下载一个 JDK 16 装上,就以为万事大吉。如果你的项目是 Spring Boot 2.5 时代生成的,装 JDK 16 确实能编译,但 Java 16 已经停止公开更新,而且很多 IDE 插件的兼容列表里早就没有 Java 16 了。装 16 是给未来埋雷,装 17 才是正道。
4.3 改完 POM 还要改 IDEA 的两处“记忆”
这是最容易功亏一篑的地方。POM 改好了,但是 IDEA 不会立即跟着变。它有自己的项目模型缓存。实际经验是,改完 POM 必须做以下两件事的全部组合,缺一个都可能继续报错:
- 右键 pom.xml -> Maven -> Reload project。
- 如果还不行,
File -> Invalidate Caches...勾选 Clear file system cache and Local History,然后 Restart。 - 再不行,关掉项目,删掉项目根目录下的
.idea文件夹(如果不怕麻烦,.iml文件也删),重新用Open打开项目。
别觉得第四步夸张。.idea里的 modules.xml 和 compiler.xml 可能存着旧的 module language level,IDEA 在处理这种情况时常常比我们想象中更"恋旧"。删掉重开,等它从 POM 重新导入,是终极干净的方案。
5. 改完配置还是一样报错?这几种“配置残留”必须清掉
走到第 4 节,大部分人的问题已经解决了。但还是有相当一部分人,改完配置、刷新完 Maven,一编译还是同样的红字。这通常不是原理没搞懂,而是有几处"配置残留"没有被清掉。单独列一节,因为这些坑真的值得记录。
5.1 终端里 java -version 没变,装的 JDK 白装了
如果你在第 4.1 节装了新 JDK,但打开终端执行java -version还是旧版本,那说明 PATH 里旧 JDK 的优先级更高。这是 Windows 上极其常见的现象:安装新 JDK 只是往C:\Program Files\Java放了一堆文件,但系统 PATH 里仍然指向旧的 JDK 8 路径。解决方法:
- Windows:
系统属性 -> 环境变量 -> Path,找到旧 JDK 的路径(通常是C:\Program Files\Java\jdk1.8.0_xxx\bin),把它删掉或移到新 JDK 路径后面。 - Linux/macOS:检查
~/.bashrc、~/.zshrc或/etc/profile里的export PATH=语句,确认没有把旧 JDK 的 bin 目录排在前面。
5.2 Maven 的全局 settings.xml 里没有玄机,但 Maven 本身用的是系统的 JAVA_HOME
还有一个很容易误解的点:Maven 是个 Java 程序,它自己跑在某个 JVM 上。如果你在 IDEA 里通过 Maven Runner 指定了 JDK 17,那 IDEA 里的 Maven 构建是好的;但你切换到命令行执行mvn clean compile,它用的是系统 PATH 里的 JDK,完全是另一套。反之亦然。
所以,无论你在 IDEA 里怎么设置,命令行构建必须单独检查环境变量:
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk export PATH=$JAVA_HOME/bin:$PATH如果你是 mac 用户,也可以写在.zshrc里,然后source ~/.zshrc。
5.3 IDEA 版本太老,不认识 Java 16/17
很少人意识到:IDEA 自身也有版本上限。如果你的 IDEA 是 2020.3 或更早,它内置的编译器组件对 Java 16 的支持是不完整的,你在 Project Structure 里可能压根找不到 language level 16 的选项,或者选了之后拉不起 JDK 16。
这种情况下的表现很迷惑:明明 Project SDK 已经指向 JDK 17,但编译还会报奇怪的错误。本质上不是项目的问题,是 IDE 版本太老。解决思路很直接——升级 IDEA。如果你公司环境不允许升级 IDE,那只能走第 4.2 节,把项目编译级别降到你 IDE 能支持的范围内。
5.4 Lombok 等注解处理器的版本拖后腿
如果你在项目里用了 Lombok,修完 JDK 版本之后可能遇到第二个报错,比如:
java: 程序包lombok不存在或者更隐蔽的注解处理异常。原因是 Lombok 内部大量使用了 javac 的非公开 API,JDK 版本一换,老版本 Lombok 就不认了。Spring Initializr 生成的默认 Lombok 版本往往偏保守,当你把 JDK 从 8 升到 17 时,最好同时把 Lombok 升级到1.18.30或更高版本。这个细节网上很少写,但实际项目里踩到的概率不低。
5.5 “最干净也最暴力的”清理流程
当所有配置看起来都对、依旧报错的时候,我推荐的终极清理流程是:
- 记下你当前已经修改好的 POM 配置(java.version 的值)。
- 关闭 IDEA 项目。
- 删除项目根目录下的
.idea文件夹。 - 如果你用的是 Maven,删除你的本地仓库里该项目相关的旧 SNAPSHOT 依赖缓存(
~/.m2/repository,不确定的话先保留)。 - 重新用 IDEA 打开项目根目录,让它重新识别 POM。
- 等右下角索引跑完,手动点一次 Maven 面板的刷新,再执行 compile。
这一步做完,所有存储在 IDE 项目文件里的历史配置都会被清空,一切从 POM 重新开始。我处理过的最顽固的一例,就是这里面的workspace.xml缓存的模块编译选项在作怪,改什么都不生效,删除.idea后一次就过了。
6. 脱离 IDE 的视角:命令行和打包机上的同款报错
很多人在本地 IDEA 里修好了,结果在服务器上打包,或者上 CI 流水线,又遇到一模一样的报错。这其实一点都不奇怪——本地的修复是基于 IDE 的设置,而命令行/CI 环境只认 JDK 相关环境变量和配置文件。
6.1 最常见的服务器场景:CentOS/Ubuntu 上 mvn compile 报错
如果你在服务器上执行:
mvn clean package报了invalid source release: 16,第一件事是看mvn -version的输出:
$ mvn -version Apache Maven 3.8.7 Maven home: /opt/maven Java version: 1.8.0_292看到Java version: 1.8就明白了:Maven 跑在 JDK 8 上。这里不管项目要求的是 16 还是 17,反正 8 不认识。处理办法:
sudo yum install -y java-17-openjdk-devel # 或者 apt install openjdk-17-jdk export JAVA_HOME=/usr/lib/jvm/java-17-openjdk export PATH=$JAVA_HOME/bin:$PATH但要注意,export只对当前终端会话有效,关闭终端就没了。要永久生效,建议写进~/.bashrc或者/etc/profile,然后source。如果服务器上还有别的 Java 程序依赖 JDK 8,你就不能全局改默认 JDK,而应该只为 Maven 指定:
JAVA_HOME=/usr/lib/jvm/java-17-openjdk mvn clean package这样只改变这一次命令的 JDK,不影响系统全局设置。这是一种非常干净的做法。
6.2 CI 流水线里的 JDK 矩阵
在 GitHub Actions、GitLab CI 这类环境里,JDK 版本由流水线配置决定,常见错误是在流水线里只配了setup-java的版本,但 Maven 用的还是默认 JDK,或者流水脚本里的JAVA_HOME被其他步骤覆盖了。GitHub Actions 一般这样用:
- name: Set up JDK 17 uses: actions/setup-java@v3 with: distribution: 'temurin' java-version: '17'如果项目是 Spring Boot 3.x,流水线里 Java 版本设成 8 或 11,那它构建出来的结果必然和本地一样红。CI 环境没有 IDEA 那层可视化配置,所有问题都会直接暴露在JAVA_HOME这个环境变量上。排查思路和服务器一致。
6.3 用 Maven Toolchains 做多 JDK 管理:后期值得了解的进阶方案
如果你的日常工作就是同时维护多个用不同 Java 版本编译的项目,那么每次切项目都改JAVA_HOME会很烦。Maven 提供了toolchains.xml,可以在项目层面指定用哪个 JDK,而不用改环境变量。在~/.m2/toolchains.xml里声明:
<toolchains> <toolchain> <type>jdk</type> <provides> <version>17</version> </provides> <configuration> <jdkHome>/usr/lib/jvm/java-17-openjdk</jdkHome> </configuration> </toolchain> </toolchains>然后在项目的pom.xml里声明使用这个 toolchain,再配合maven-compiler-plugin的release参数,就可以做到"一个 Maven,任意切换 JDK"。这是一个后期优化方向,刚被invalid source release折磨的读者不必立刻上手,但知道有这条路,能让你以后少走很多弯路。
第七节就不设了,最后分享两个我从实际项目里沉淀下来的小经验。
第一个经验是:不要只修不验证。改完配置后,别只在 IDEA 里点一次绿色三角就以为完事了。一定要分别验证mvn compile和mvn package两个命令,因为 IDEA 内置编译器和 Maven 构建用的是两套机制,前者过了不能代表后者过。很多项目本地能跑、打包就挂,根子就在这里。
第二个经验是:报错里出现的版本号,是项目想要的环境,不是你拥有的环境。看到16不要只想着"怎么把 16 弄没",要顺着它去确认项目的java.version、IDEA 的 Project SDK、Maven Runner JRE 这三个是否一致。这套排查思路,比记住任何一个单一命令都值钱——因为今天你可能遇到 16,明天升级 Spring Boot 3 后还会遇到 17、21,逻辑完全一样,只是数字不同而已。