彻底搞懂 IDEA、Maven 和 JDK 版本配置的关系
相信你在跑 Java 项目的时候经常出现因为 JDK 版本设置的问题出现编写代码、编译、运行不成功的问题。下面就教清楚你原理以及如何排错和设置。理解 javac我们从最基础的开始就是我们只有.java一个单文件。我们不借助任何编辑工具我们就用文本编辑器编写的。我们的 JDK 版本是 17。编译.java文件靠的是javac这个javac是 JDK 自带的。来自哪个 JDK由我们的环境变量决定我们配置 Java 环境的时候通常会配置JAVA_HOME所以它会从这个里面找到我们的javac。我们可以通过下面的命令编译.java文件。javac /path/User.javajavac命令有两个参数-source-targetjavac是真正干活的编译器它跟我们JAVA_HOME的 JDK 版本一致它决定了两件事编译器本身最多认识的语法就到 Java17默认使用 JDK 17 自带的标准类库-source是什么javac-source8User.java意思是让 JDK 17 的javac按照 Java8 的语法规则检查源码。它只管语法比如 Java 10 才支持的varpublicclassUser{publicstaticvoidmain(String[]args){varnameTom;}}那么这样的话我们用 Java17 的语法可以正常编译通过但是 Java8 语法检查就通过不了了。还记得我们前面强调过一次它只检查语法这是什么意思比如List.of是 Java9 支持的我们看下面的代码publicclassUser{publicstaticvoidmain(String[]args){ListIntegerlistList.of(1,2,3);}}用下面的命令编译javac-source8User.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 是817 在 8 后面8 怎么会知道 17 的规则呢所以你可以让 JDK17 的 javac 生成它知道的版本比如-target可以小于等于 17但是不能大于 17。实际使用时-target通常要和-source一起指定否则可能出现源码语法版本和字节码目标版本不一致的问题。运行.class编译完之后就该运行了运行时的 JDK 版本必须 target 指定的版本才可以比如-target 8的 .class 文件可以运行在 JDK8 或者 JDK17 上。但是这里有个挺重要的坑的比如我们用 JDK 17 编译javac-source8-target8User.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我们当然也可以手写javacUser.javaUserMapper.javaUserService.javaUserController.java比如在编译UserMapper.java的时候因为它用到了User.java所以它会找User.class找不到就找User.java先编译它因为如果不先编译User.javaUserMapper.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/classesMaven 怎么管理依赖我们在pom.xml写的dependencyMaven 会自动帮我们去远程仓库下载 jar 包然后放到本地仓库并且自动加到 classpath所以我们不需要手动指定 classpath 来添加这些 jar 包了。javac-cplib/lombok.jar属性配置而我们pom.xml中的一些属性propertiesmaven.compiler.source17/maven.compiler.sourcemaven.compiler.target17/maven.compiler.targetmaven.compiler.release17/maven.compiler.release/properties对应前面我们指定 javac 的三个参数注意奥我们前面也说了Java9 及之后推荐只写 releaseJava8及之前才写 sourcetarget。所以我们mvn -v命令显示的 Java version 就是 Maven 实际运行使用的 JDK。它由 JAVA_HOME 和 PATH 来决定。属性中的 java.version 是什么我们在写 SpringBoot 项目的时候很多时候会写propertiesjava.version17/java.version/properties这其实只是一个变量定义跟编译什么的都没关系。而是在 Spring Boot 项目中spring-boot-starter-parent 已经约定会使用 java.version 这个变量所以我们经常只需要配置 java.version就能控制项目使用的 Java 版本。前面全部是说的只有文本编辑器的情况下但是我们肯定都用 IDEA 来写代码也需要一些配置为什么pom.xml配了IDEA 还要配置前面我们已经知道pom.xml主要是给 Maven 看的。比如下面这些配置propertiesmaven.compiler.release17/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 SDKProject SDK 是 IDEA 当前项目默认使用的 JDK。它主要决定 IDEA 手里有哪些 JDK 工具和标准类库。比如 IDEA 能不能识别 String、List、LocalDate、HttpClient 这些 JDK 自带的类以及默认用哪个 JDK 来运行、调试、编译项目(java / javac)。举个例子importjava.util.List;publicclassUser{publicstaticvoidmain(String[]args){ListStringlistList.of(A,B,C);}}这里的List.of()是 Java 9 才有的标准类库方法。如果 Project SDK 配的是 JDK 8那么 IDEA 使用的就是 JDK 8 的标准类库。JDK 8 里面没有List.of()所以 IDEA 就提示找不到这个方法。Project SDKIDEA 这个项目默认拿哪套 JDK 来开发、运行和编译IDEA 里的 Language LevelLanguage Level 表示 IDEA 按照哪一代 Java 语法规则检查你的代码。它主要管的是语法而不是类库。比如下面这段代码publicclassUser{publicstaticvoidmain(String[]args){varnameTom;}}这里的 var 是 Java 10 才支持的局部变量类型推断语法。如果 Language Level 配成 Java 8那么 IDEA 就会按照 Java 8 的语法规则检查代码。Java 8 不支持 var所以 var 会标红。比如下面我们用 JDK8但是 Language level 是17。这里并不会报错这时候重点不是 JDK 类库里有没有某个方法而是这套语法规则允不允许你这么写。Language LevelIDEA 按照哪一代 Java 语法检查代码但是我们要明白这里的配置都是对 IDEA 的配置跟 javac、java 编译、运行的参数都没关系哎这里说个让你明白的事如果我们在 IDEA 的终端执行javac...要么用的 java 版本就是我们 JAVA_HOME 指定的如果我们是在终端用命令mvn compile那就是根据mvn -v里面的 Java version然后用的pom文件里面的maven.compiler.source17/maven.compiler.sourcemaven.compiler.target17/maven.compiler.target传递给javac: source 17 -target 17。如果我们是点击的 IDEA 的构建、运行、Debug那么走的是 Project SDK 和 Language Level。但是如果 IDEA 设置了Build and run using: MavenRun 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 不是 MavenMaven 也不是 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-versionjavac-versionmvn-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 17pom.xml推荐这样写propertiesjava.version17/java.versionmaven.compiler.release${java.version}/maven.compiler.release/properties如果是 Java 8 项目可以写propertiesjava.version8/java.versionmaven.compiler.source${java.version}/maven.compiler.sourcemaven.compiler.target${java.version}/maven.compiler.target/properties如果你使用的是 Java 9 及之后的 JDK并且想编译成 Java 8 兼容版本更推荐propertiesmaven.compiler.release8/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 版本问题都能解决。