相信你在跑 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 同时限制三件事

  1. 源码语法按 Java 8 检查;
  2. 生成 Java 8 版本的 .class 文件;
  3. 只能使用 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.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/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 版本问题都能解决。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐