彻底搞懂 IDEA、Maven 和 JDK 版本配置的关系
相信你在跑 Java 项目的时候经常出现因为 JDK 版本设置的问题,出现编写代码、编译、运行不成功的问题。下面就教清楚你原理,以及如何排错和设置。
理解 javac
我们从最基础的开始,就是我们只有 .java 一个单文件。我们不借助任何编辑工具,我们就用文本编辑器编写的。我们的 JDK 版本是 17。
编译 .java 文件靠的是 javac,这个 javac 是 JDK 自带的。来自哪个 JDK?由我们的环境变量决定,我们配置 Java 环境的时候,通常会配置 JAVA_HOME,所以它会从这个里面找到我们的 javac。
我们可以通过下面的命令编译 .java 文件。
javac /path/User.java
javac 命令有两个参数
-source-target
javac 是真正干活的编译器,它跟我们 JAVA_HOME 的 JDK 版本一致,它决定了两件事
- 编译器本身最多认识的语法就到 Java17
- 默认使用 JDK 17 自带的标准类库
-source 是什么?
javac -source 8 User.java
意思是让 JDK 17 的 javac 按照 Java8 的语法规则检查源码。它只管语法,比如 Java 10 才支持的 var
public class User {
public static void main(String[] args) {
var name = "Tom";
}
}

那么这样的话,我们用 Java17 的语法可以正常编译通过,但是 Java8 语法检查就通过不了了。
还记得我们前面强调过一次,它只检查语法?这是什么意思?
比如 List.of 是 Java9 支持的,我们看下面的代码
public class User {
public static void main(String[] args) {
List<Integer> list = List.of(1, 2, 3);
}
}
用下面的命令编译
javac -source 8 User.java

只是警告,它不会报错,所以编译是通过了的!!!因为从语法角度上看,这个是 类名.静态方法(...),Java8 是支持的,所以 -source 8 不会拦他。我们用的仍是 JDK17 类库,所以 javac 能找到这个方法,所以不会报错。
.java文件本身不会标记“我是 Java 8 语法”还是“我是 Java 17 语法”。javac编译时需要按照某一套 Java 语法规则去解析它。
如果你不指定,javac默认按照当前编译器支持的默认版本来解析;如果你指定-source,就强制它按照指定版本的语法规则解析。
javac 版本决定它最多认识到哪一代语法,因为我们用的 JDK17,所以这个之前的语法它都认识,所以我们只能指定 -source 小于等于 17。
-target
-target 用来指定 .java 编译后生成的 .class 文件版本。
对,.class 是有版本号的,不同 Java 版本生成的内部版本号不一样。运行 .class 文件的时候,JVM 会检查这个 class 文件版本,规则是
运行时 JDK 版本 >= class 文件版本
比如 target 8 生成的 class 可以在 JDK 8、11、17、21 上运行,但是 target 17 生成的 class 不能在 JDK 8、11 上运行。如果你用低版本 JDK 运行高版本 .class,就会报 UnsupportedClassVersionError
那当然了,编译版本是 17,运行的 JDK 是8,17 在 8 后面,8 怎么会知道 17 的规则呢?
所以你可以让 JDK17 的 javac 生成它知道的版本,比如 -target 可以小于等于 17,但是不能大于 17。
实际使用时,
-target通常要和-source一起指定,否则可能出现源码语法版本和字节码目标版本不一致的问题。
运行 .class
编译完之后就该运行了,运行时的 JDK 版本必须 >= target 指定的版本 才可以,比如 -target 8 的 .class 文件可以运行在 JDK8 或者 JDK17 上。但是这里有个挺重要的坑的,比如我们用 JDK 17 编译
javac -source 8 -target 8 User.java
并且代码里面写了
List.of(1, 2, 3);
这个时候语法检查没错,编译没错,在 JDK17 上运行也没错,因为 JDK17 的类库里面有 List.of(),它能找到这个方法,但是如果在 JDK8 上运行就不行了,运行到这一行的时候会报错 NoSuchMethodError。
--release
怎么解决前面说的这个问题呢,既然这样我其实不想让它编译通过的,我们可以用 --release 参数--release 8 代表只能使用 Java 8 里存在的 API,这样如果你用 Java9 的API,会直接在编译报错。
依旧要求这个版本必须小于等于 javac 的版本。
但是呢,这个参数其实覆盖范围更全,--release 8 同时限制三件事
- 源码语法按 Java 8 检查;
- 生成 Java 8 版本的
.class文件; - 只能使用 Java 8 的公开标准 API。
但是这个参数在 Java9 及以后的 javac 才能用,在这之前只能配置 -source -target,在 Java9 之后你可以只用 --release 一个参数来,因为它比 -source -target 更安全。
-classpath -sourcepath
当多个 .java 文件之间有依赖,在编译时怎么找到依赖的类呢?
UserController.java
UserService.java
UserMapper.java
User.java
我们当然也可以手写
javac User.java UserMapper.java UserService.java UserController.java
比如在编译 UserMapper.java 的时候,因为它用到了 User.java,所以它会找 User.class,找不到就找 User.java 先编译它,因为如果不先编译 User.java,UserMapper.java 用 User 对象的时候不知道它有什么属性什么方法,不知道调用的对不对,就不知道能不能编译通过。
实际上如果 javac UserController.java,但是前三个文件都没编译的,但是 javac 能找到这三个 .java 文件也能编译通过,并且四个文件都会被编译
javac 找东西从两个路径找,可以通过 -classpath -sourcepath 指定
classpath:找已经编译好的.class文件或者.jar文件sourcepath:找还没编译的.java文件
如果没有指定 classpath,那默认 classpath 就是当前目录 .,如果没有指定 sourcepath,那 javac 就会在 classpath 中找源码文件。如果类声明了 package com.example.entity;,那找字节码文件就去 classpath/com/example/entity 文件夹下找,找 .java 文件就会从 sourcepath/com/example/entity 下找。如果依赖在 jar 包里,就会在 jar 包内部找 com/example/entity/xxx.class。因为 jar 包里面放的一般都是 .class。
理解 Maven
❓ 既然 javac 能编译,为什么还要 Maven?
一整个项目通常有非常多的文件,并且相互依赖,而且有很多第三方 jar 包,编译完如何打包也是一个问题。如果自己手写 javac 很麻烦,Maven 本质上是一个项目构建工具,它能帮助我们下载依赖、管理依赖版本,编译源码、编译测试代码、运行测试、复制资源文件,打包 jar/war,执行插件,管理多模块项目。我们原来手写一堆的命令
javac ... # 编译
java ... # 运行
jar ... # 打包
都让 Maven 来帮我们管理了,它来构建流程(但是不是替代,本质还是用的这几个命令)。
那怎么管理和组织项目的构建流程呢?就是根据 pom.xml 的配置
Maven 做了什么?
Maven 做了两件事
- 约定了项目目录结构
src/main/java 放正式源码 src/main/resources 放正式配置文件 src/test/java 放测试源码 src/test/resources 放测试配置文件 target/classes 放编译后的正式 class target/test-classes 放编译后的测试 class target/*.jar 放打包结果 - 通过
pom.xml描述构建规则groupId:组织名 / 公司名 / 项目组名 artifactId:项目名 / 模块名 version:版本号 packaging:打包方式,默认是 jar properties:一些变量或构建参数 dependencies:项目依赖 build/plugins:构建插件
当我们编译项目,执行 mvn compile 时,Maven 会做这些事
1. 读取 pom.xml
2. 确认项目坐标:groupId、artifactId、version
3. 读取 dependencies,下载依赖 jar
4. 根据约定找到 src/main/java
5. 根据约定找到 src/main/resources
6. 准备 classpath
7. 调用 maven-compiler-plugin
8. maven-compiler-plugin 调用 javac
9. 把 .java 编译成 .class
10. 输出到 target/classes
Maven 怎么管理依赖?
我们在 pom.xml 写的 dependency,Maven 会自动帮我们去远程仓库下载 jar 包,然后放到本地仓库,并且自动加到 classpath,所以我们不需要手动指定 classpath 来添加这些 jar 包了。
javac -cp "lib/lombok.jar"
属性配置
而我们 pom.xml 中的一些属性
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<maven.compiler.release>17</maven.compiler.release>
</properties>
对应前面我们指定 javac 的三个参数,注意奥,我们前面也说了,Java9 及之后推荐只写 release,Java8及之前才写 source+target。
所以我们 mvn -v 命令
显示的 Java version 就是 Maven 实际运行使用的 JDK。它由 JAVA_HOME 和 PATH 来决定。
属性中的 java.version 是什么?
我们在写 SpringBoot 项目的时候,很多时候会写
<properties>
<java.version>17</java.version>
</properties>
这其实只是一个变量定义,跟编译什么的都没关系。而是在 Spring Boot 项目中,spring-boot-starter-parent 已经约定会使用 java.version 这个变量,所以我们经常只需要配置 java.version,就能控制项目使用的 Java 版本。
前面全部是说的只有文本编辑器的情况下,但是我们肯定都用 IDEA 来写代码,也需要一些配置
为什么 pom.xml 配了,IDEA 还要配置?
前面我们已经知道,pom.xml 主要是给 Maven 看的。比如下面这些配置:
<properties>
<maven.compiler.release>17</maven.compiler.release>
</properties>
它的意思是告诉 Maven:这个项目编译时按照 Java 17 来处理。
但是问题来了,IDEA 不只是帮我们执行 Maven。我们平时写代码的时候,IDEA 还要做很多事情,比如
代码提示
语法检查
依赖识别
跳转源码
运行 main 方法
运行单元测试
调试代码
编译项目
导入 Maven 项目
所以 IDEA 自己也必须知道:这个项目到底用哪个 JDK。
也就是说,pom.xml 解决的是 Maven 构建时的问题,而 IDEA 的 JDK 配置解决的是 IDEA 自己怎么理解、检查、运行这个项目的问题。
可以简单理解为:
pom.xml:告诉 Maven 怎么构建项目
IDEA JDK:告诉 IDEA 怎么看懂和运行项目
所以 IDEA 里面通常会涉及几类 JDK 配置。
IDEA 中的 JDK 配置
IDEA 里的 Project SDK

Project SDK 是 IDEA 当前项目默认使用的 JDK。它主要决定 IDEA 手里有哪些 JDK 工具和标准类库。比如 IDEA 能不能识别 String、List、LocalDate、HttpClient 这些 JDK 自带的类,以及默认用哪个 JDK 来运行、调试、编译项目(java / javac)。
举个例子:
import java.util.List;
public class User {
public static void main(String[] args) {
List<String> list = List.of("A", "B", "C");
}
}
这里的 List.of() 是 Java 9 才有的标准类库方法。
如果 Project SDK 配的是 JDK 8,那么 IDEA 使用的就是 JDK 8 的标准类库。
JDK 8 里面没有 List.of(),所以 IDEA 就提示找不到这个方法。

Project SDK:IDEA 这个项目默认拿哪套 JDK 来开发、运行和编译
IDEA 里的 Language Level
Language Level 表示 IDEA 按照哪一代 Java 语法规则检查你的代码。它主要管的是语法,而不是类库。
比如下面这段代码:
public class User {
public static void main(String[] args) {
var name = "Tom";
}
}
这里的 var 是 Java 10 才支持的局部变量类型推断语法。如果 Language Level 配成 Java 8,那么 IDEA 就会按照 Java 8 的语法规则检查代码。Java 8 不支持 var,所以 var 会标红。比如下面我们用 JDK8,但是 Language level 是17。
这里并不会报错
这时候重点不是 JDK 类库里有没有某个方法,而是这套语法规则允不允许你这么写。
Language Level:IDEA 按照哪一代 Java 语法检查代码
但是我们要明白,这里的配置都是对 IDEA 的配置,跟 javac、java 编译、运行的参数都没关系
哎,这里说个让你明白的事,如果我们在 IDEA 的终端执行
javac ...
要么用的 java 版本就是我们 JAVA_HOME 指定的
如果我们是在终端用命令
mvn compile
那就是根据 mvn -v 里面的 Java version,然后用的 pom 文件里面的
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
传递给 javac: source 17 -target 17。
如果我们是点击的 IDEA 的构建、运行、Debug
那么走的是 Project SDK 和 Language Level。但是如果 IDEA 设置了
Build and run using: Maven
Run tests using: Maven
那就会走 Maven 的逻辑
勾选上之后就委托给 Maven 了。
IDEA 里的 Module SDK
如果是普通单模块项目,Project SDK 通常就够了。但是如果是多模块项目
每个模块也可以单独配置自己的 SDK,这就是 Module SDK。
当然也可以设置模块的 Language Level。
如果某个模块的 Module SDK 单独配成了 JDK 8,而整个项目的 Project SDK 是 JDK 17,那么这个模块里写 Java 17 语法仍然可能出问题。
所以排查 JDK 问题时,不能只看 Project SDK,也要看 Module SDK 有没有覆盖项目配置。
一般建议 Module SDK 跟随 Project SDK,除非你真的有特殊需求,比如一个老模块必须使用 JDK 8。
IDEA 里的 Maven JDK
还有一个非常关键的地方:IDEA 里面运行 Maven 时,也有自己的 JDK 配置。
也就是说,你在终端执行
mvn -v
看到的是终端环境下 Maven 使用的 JDK。
但是你点 IDEA 右侧 Maven 面板
用的不是终端里的那个 JDK,而是 IDEA 给 Maven 配置的 JDK。
所以有时候会出现这种情况
终端 mvn compile 成功
IDEA Maven 面板 compile 失败
或者反过来
IDEA Maven 面板 compile 成功
终端 mvn compile 失败
原因就是两边 Maven 使用的 JDK 不一样。
在 IDEA 中,Maven 的 JDK 一般在这里配置
这个 JRE 最好选择项目对应的 JDK,比如 JDK 17。
IDEA 和 Maven 的关系
IDEA 可以帮我们写代码,也可以帮我们执行 Maven。
但是 IDEA 不是 Maven,Maven 也不是 IDEA。
IDEA 可以读取 pom.xml,然后把 Maven 项目的依赖、模块、源码目录、资源目录都导入到 IDEA 里。
比如 Maven 约定:
src/main/java 正式源码
src/main/resources 正式配置文件
src/test/java 测试源码
src/test/resources 测试配置文件
IDEA 读取 pom.xml 后,就知道:
src/main/java 是源码目录
src/main/resources 是资源目录
dependencies 里的 jar 要加入项目依赖
所以我们平时看到的代码提示、依赖跳转、包不报红,很多都是 IDEA 读取 Maven 配置后完成的。(比如 @RequestMapping 注解,是 IDEA 读取 Maven 配置后才知道这个没问题,所以不报红)
但是读取归读取,真正是否能编译成功,还要看 Maven 执行时使用的 JDK、pom.xml 中的编译配置、项目代码本身是否匹配。
总结
一个项目里可能同时存在几个 JDK 概念
Java 项目里最容易乱的地方,就是你以为只有一个 JDK,实际上可能有好几个地方都在配置 JDK。
常见的有
1. 终端里的 JAVA_HOME + PATH
2. IDEA 的 Project SDK
3. IDEA 的 Module SDK
4. IDEA 的 Language Level
5. IDEA 运行 Maven 时使用的 JDK
6. IDEA 运行 main 方法时使用的 JDK
7. Maven 编译插件中配置的 source / target / release
所以出现 JDK 版本问题时,不要只看一个地方。
比如你在终端执行:
java -version
javac -version
mvn -v
只能说明终端环境下的 Java 和 Maven 是什么版本。
但是 IDEA 里面运行项目用的 JDK,可能还要单独看 Run Configuration。
IDEA 里面执行 Maven 用的 JDK,也要看 Maven Runner 的 JRE。
推荐的统一配置方式
实际开发中,最推荐的是让所有地方都统一。
比如项目要求 Java 17,那么就统一成
JAVA_HOME = JDK 17
PATH 中的 java / javac 来自 JDK 17
IDEA Project SDK = JDK 17
IDEA Module SDK = Project SDK
IDEA Language Level = Java 17
IDEA Maven Runner JRE = JDK 17
pom.xml 中 maven.compiler.release = 17
pom.xml 推荐这样写:
<properties>
<java.version>17</java.version>
<maven.compiler.release>${java.version}</maven.compiler.release>
</properties>
如果是 Java 8 项目,可以写:
<properties>
<java.version>8</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
</properties>
如果你使用的是 Java 9 及之后的 JDK,并且想编译成 Java 8 兼容版本,更推荐:
<properties>
<maven.compiler.release>8</maven.compiler.release>
</properties>
因为 --release 8 不仅限制语法和 class 文件版本,还会限制只能使用 Java 8 里存在的标准 API。
最后总结
pom.xml 和 IDEA 的 JDK 配置不是重复配置,而是负责的事情不一样。
pom.xml 是给 Maven 看的,它描述项目怎么构建、依赖是什么、编译目标版本是什么。
IDEA 的 JDK 配置是给 IDEA 自己看的,它决定 IDEA 用哪个 JDK 来理解代码、检查语法、运行程序、调试项目,以及在 IDEA 里执行 Maven 时使用哪个 JDK。
最终可以记住一句话:
pom.xml 规定项目应该怎么编译,IDEA 配置决定 IDEA 用哪个 JDK 来开发和运行,真正干活的仍然是某个 JDK 里的 javac 和 java。
所以排查 Java 项目的 JDK 问题时,要同时看三条线:
终端环境:java -version、javac -version、mvn -v
Maven 配置:pom.xml 中的 source、target、release
IDEA 配置:Project SDK、Module SDK、Language Level、Maven Runner JRE
只要这三条线统一,绝大多数 JDK 版本问题都能解决。
更多推荐




所有评论(0)