qinghuan
← Java Web / 全部笔记

单元 36 / Java Web

P0 · 必会主线L1 · 基础

Maven 与运行环境:从“能编译”走到“能请求”

先找出从工具、编译、启动到接口请求的哪一步失败,再核对实际 JDK、依赖与服务配置。

本篇目录 · 按需要跳转

读完这篇,你能做什么

能核对工具版本,分清错误发生在编译、启动还是请求阶段,逐步缩小环境故障范围。

学习理由、准备要求与知识点

为什么学它

把编译、启动和请求分别验证,可以避免将工具链问题误诊成数据库或业务故障。

先读这些内容

知识点清单

  • POM 与 Java 目标
  • Maven 实际 JDK
  • IDE Runner 与 JAVA_HOME
  • Maven Wrapper
  • 编译和测试阶段
  • 启动日志边界
  • 首次请求依赖失败
  • 直连后端与代理排查

运行前先确认Java 17 为目标;Maven CLI 源码定位为 3.9.9。算法只计算显式给定的整数版本约束,不推断框架兼容性,也不代替真实编译、启动或接口验证。

01先弄懂它是什么

“项目启动不了”太笼统,先找到第一步失败的位置。工具能否启动、依赖能否下载、能否编译、测试是否通过、应用能否启动、业务请求是否成功,是不同问题。找不到 javac 和数据库拒绝登录,处理方法完全不同。先记录已经通过的步骤和第一条实际错误,再修改相关配置。

同一个项目里,要求的 JDK、启动 Maven 的 JDK、编译使用的 JDK 和运行应用的 JDK 可能不是同一套。IDE 的 Project SDK 与 Maven Runner 也分别设置。

java -version 只说明终端当前找到的 Java;mvn -version 才能看 Maven 的运行环境。使用 Toolchains 时,部分插件还能选择另一套工具,因此版本不一致要查清原因,不能直接认定全部都错。

目标版本也不是运行时本身。javac --release 17 同时约束支持的 Java 语法、目标字节码与对应平台 API;它不会把第三方依赖自动降级,也不保证注解处理器兼容任意新编译器。某个 jar 需要更高运行时,不能仅修改项目 release 属性解决。框架、插件、处理器与 JDK 应当作为一组兼容约束核对。

文中这几个词,先认一认

证据阶段
一次检查真正证明了哪一步,例如编译通过只说明编译成功,不说明数据库请求也正常。
Toolchain
让支持它的 Maven 插件按配置选择工具的机制,所用 JDK 可以不同于启动 Maven 的那一套。
目标 release
编译时声明代码面向哪个 Java 平台版本,但仍需检查运行 JDK 和第三方库是否兼容。

还有陌生词?到术语小抄里看一个具体例子 ↗

02看看它怎么工作

从项目根目录先读 POM 的 parent、Java 属性、插件和模块,再运行项目自带的 Maven Wrapper 或已固定版本的 Maven。记录 mvn -version、effective-pom 与 dependency:tree 可以回答工具是谁、配置最终是什么、依赖从哪里来。不要把日志中最后出现的插件名直接认定为根因,应继续看首个导致失败的异常和它的 cause。

编译后检查目标模块的产物路径和入口类。多模块项目中,根目录命令成功可能只有 POM 聚合工作,真正可运行类在另一个模块。应用监听端口后先请求直连地址,确认路径和业务响应,再经过反向代理;直连成功而代理失败时,重点检查上游地址、路径改写和转发头,而不是重新调整编译器。

数据库连接池可能在启动时初始化,也可能在首次使用时才建立部分连接。Redis、SQL 映射或外部 HTTP 客户端也可能延迟触发。因此 Started 日志只是启动流程的一项证据。对关键用例记录请求方法、响应状态、必要结果和对应服务端日志,才能限定“这个输入在这个配置下通过了哪些组件”。

正在加载流程图…

每一层通过只缩小故障范围;后续链路需要自己的请求或运行证据。
查看流程图源码(Mermaid)
flowchart TD
 A["读取 POM 与工具链"] --> B["依赖解析"]
 B --> C["编译与测试"]
 C --> D["确认模块产物"]
 D --> E["进程启动与监听"]
 E --> F["直连业务请求"]
 F --> G["代理或客户端路径"]
 B --> H["记录首次失败边界"]
 C --> H
 E --> H
 F --> H
 G --> H

按顺序看,每一步做了什么

  1. 在含目标 POM 的目录核对 Wrapper、Maven 版本与实际 Java 路径。
  2. 查有效 POM 与依赖树,确认目标 release 和处理器版本。
  3. 执行目标模块的编译与测试,检查测试数量及跳过标志。
  4. 确认实际产物和入口类,再启动并记录监听地址。
  5. 先直连接口,再验证代理;将失败与对应日志按关联标识对齐。

03对照实现代码

MavenCli 如何依次准备与执行构建 (新标签页)↗

Apache Maven maven-3.9.9 · MavenCli#doMain(CliRequest)、#initialize

依据真实调用次序创作的独立步骤模型,非原源码节选。不解析 Maven 参数或执行插件;返回轨迹只用于理解失败会截断后续阶段。

核心机制代码 · java
import java.util.*;
class SourceModel {
    static List<String> prepareAndExecute(String failAt) {
        List<String> trace = new ArrayList<>();
        String[] stages = {"initialize", "cli", "properties", "logging",
            "version", "container", "configure", "toolchains", "execute"};
        try {
            for (String stage : stages) {
                // 每个阶段先记录进入,再模拟该阶段失败。
                trace.add(stage);
                if (stage.equals(failAt)) throw new IllegalStateException(stage);
            }
        } catch (IllegalStateException error) {
            // 失败保留所在边界,不继续执行后面的插件工作。
            trace.add("failed:" + error.getMessage());
        } finally {
            // 资源清理属于退出流程,不能当成构建成功证据。
            trace.add("cleanup");
        }
        return List.copyOf(trace);
    }
}

doMain 在 execute 前依次准备参数、属性、日志、容器、配置和工具链。遇到早期错误时,业务编译可能根本尚未开始。

initialize 处理工作目录、多模块目录与 -f 路径等输入。错误目录不仅影响相对文件位置,还会改变正在构建的项目范围。

真实方法对若干错误采用不同退出码并在 finally 清理容器。进程退出与日志清理完成均不代表生命周期任务成功。

教学代码省略大量 CLI 分支,重点是阶段先后与失败截断。定位实际问题仍应回到固定版本源码和日志,而不是要求模型列出的步骤与所有 Maven 版本完全一致。

代码先调用谁,再调用谁

  1. Maven 启动脚本确定 Java
  2. MavenCli.doMain
  3. initialize / cli / properties
  4. configure / toolchains / populateRequest
  5. execute 调用构建核心

04动手写一个例子

选择满足所有显式约束的已安装 JDK

每项需求给出允许的 Java 主版本闭区间,计算交集,再从已安装版本选择最小可行值;没有交集与没有安装候选要返回不同原因。输入是已核验的约束数据,不自动从 POM 猜测兼容范围。

输入 1
要求 [17,21]、[17,17],已安装 11、17、21
输出 1
SELECTED 17,交集 [17,17]
输入 2
要求 [17,17]、[21,21]
输出 2
CONFLICT

每项约束都是一个允许集合,全部同时成立意味着求交集。闭区间交集的下界取所有下界最大值,上界取所有上界最小值;下界大于上界立即说明约束本身冲突。

有交集却没有对应安装版本,是工具准备问题;没有交集是需求组合问题。把两者分开可以避免给不可能兼容的项目再安装更多 JDK。

本题不采用“越新越好”的猜测,只在明确允许的候选内选最小主版本作为确定策略。真实构建还要检查补丁级、安全维护状态、厂商实现以及插件是否使用 Toolchain,这些不由整数区间保证。

为什么这样写是对的处理前 k 项需求后,[low,high] 恰好是这 k 项允许版本集合的交集。

时间 O(N+M)额外空间 O(1) 额外空间

N 项约束各更新一次上下界,M 个安装版本扫描一次选最小值;不排序候选。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

展开收起代码 · Main.java · 完整可运行程序Java · 41 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

Main.java · 完整可运行程序 · Java
import java.util.*;
public class Main {
    record Range(int low, int high) {
        Range {
            if (low < 1 || low > high) throw new IllegalArgumentException("bad range");
        }
    }
    record Choice(String status, Integer version, int low, int high) {}
    static Choice select(List<Range> requirements, List<Integer> installed) {
        int low = 1, high = Integer.MAX_VALUE;
        for (Range r : requirements) {
            // 累积交集:下界只能上升,上界只能下降。
            low = Math.max(low, r.low());
            high = Math.min(high, r.high());
        }
        if (low > high) return new Choice("CONFLICT", null, low, high);
        Integer chosen = null;
        for (int version : installed) {
            if (version < 1) throw new IllegalArgumentException("bad installed version");
            // 扫描可行候选,保留最小值而不依赖输入顺序。
            if (version >= low && version <= high &&
                    (chosen == null || version < chosen)) chosen = version;
        }
        // 区分约束冲突和仅缺少已安装候选。
        return new Choice(chosen == null ? "NOT_INSTALLED" : "SELECTED", chosen, low, high);
    }
    static void eq(Object expected, Object actual) {
        if (!Objects.equals(expected, actual)) throw new AssertionError(actual);
    }
    public static void main(String[] args) {
        eq(new Choice("SELECTED",17,17,17), select(
            List.of(new Range(17,21),new Range(17,17)),List.of(21,11,17)));
        eq("CONFLICT", select(List.of(new Range(17,17),new Range(21,21)),List.of(17,21)).status());
        eq("NOT_INSTALLED", select(List.of(new Range(17,17)),List.of(11,21)).status());
        eq(Integer.valueOf(17), select(List.of(new Range(17,21)),List.of(21,17,17)).version());
        eq("NOT_INSTALLED", select(List.of(),List.of()).status());
        try { new Range(21,17); throw new AssertionError("invalid range"); }
        catch (IllegalArgumentException expected) {}
        System.out.println("PASS 6 toolchain constraint tests");
    }
}

这些特殊情况也要试一试

  • 空需求视作所有正整数主版本允许,但仍需要已安装候选。
  • 重复安装版本不影响结果;恰好等于边界的版本可被选择。
  • 区间冲突与缺少安装候选分别报告,非法上下界直接拒绝。
进阶选读 · 从区间提升为兼容矩阵与最小冲突解释

场景某注解处理器只支持离散版本,多个插件与框架组合又存在排除条件。

做法将每项约束表示为有限候选集合或谓词,逐候选验证;记录拒绝原因,进一步求一个不可同时满足的约束子集帮助决定升级哪项组件。

复杂度枚举 M 个候选与 N 个约束为 O(MN);最小冲突子集的精确搜索可能指数增长。

好处与代价矩阵能表达真实非连续兼容性,但来源与维护成本较高,必须注明版本证据,不能把未知兼容当成允许。

05项目里怎么用

建立环境记录时保留工具版本、项目提交、目标模块、启动参数名称和成功请求的范围。凭据只记录配置来源,不把秘密值贴入日志。切换 JDK 后再次检查 mvn -version 与实际编译命令,避免终端或 IDE 仍持有旧环境。

修复依赖冲突先查引入路径。优先遵守项目父 POM 或 BOM 的版本组合,再针对明确不兼容的库调整。一次升级全部依赖会扩大回归面,也让根因无法复现。构建缓存可能掩盖缺失生成步骤,必要时在明确的项目目录做一次干净构建验证可重复性。

在隔离测试环境中,至少检查一个关键读取和一个受控写入,并测试失败时的结果。存活检查说明进程在,就绪检查说明具备接请求条件;两者都不能覆盖全部 SQL 和权限分支。复现报告还要写明哪些外部调用没验证,让后续排查能接着进行。

容易踩的坑

  • 只看 java -version 就断定 Maven、IDE 和应用都用同一 JDK。
  • 把 --release 当作第三方依赖或注解处理器兼容适配器。
  • 仅看 BUILD SUCCESS,忽略零测试或跳过测试配置。
  • 接口首次数据库失败时随机修改编译插件,破坏已通过的边界。

几种做法,怎么选

表格有四列,窄屏可左右滑动查看代价和适用条件。

方案优势代价与限制适用条件
项目 Maven Wrapper团队易统一 Maven 发行版本。仍需管理 Java 和仓库访问。需要可重复开发与持续集成的项目。
Maven Toolchains允许运行 Maven 与编译工具独立选择。要求相关插件支持并正确配置。多目标 Java 版本构建。
容器化构建环境集中描述工具和系统依赖。镜像维护、缓存和平台差异仍需处理。团队或 CI 的一致构建环境。

06检验是否读懂了

先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。

先用自己的话解释

01编译通过证明数据库账号可用吗?

展开参考答案与推导

不能。数据库认证通常在连接建立时验证,需对应请求或集成测试。

02Maven 运行 JDK 与编译 JDK 能不同吗?

展开参考答案与推导

可以,例如支持 Toolchains 的插件选择了另一套工具;必须有明确配置证据。

03release 17 会把依赖的 Java21字节码改为17吗?

展开参考答案与推导

不会。需要选择兼容依赖或调整经验证的运行目标。

换个情况,还会处理吗

01终端成功而 IDE Maven 失败怎么办?

展开参考答案与推导

比较实际 Maven 版本、Runner JDK、工作目录、激活配置和仓库设置,先找到差异。

02Started 后第一条查询才报认证错误,优先看什么?

展开参考答案与推导

检查该请求使用的数据源、账号、认证参数与服务端日志,确认连接是否延迟创建。

03后端直连成功而代理返回404,怎么办?

展开参考答案与推导

核对代理转发路径、前缀改写与上游地址,保留直连成功证据缩小范围。

写代码试一试

01如何判断交集只有一个允许版本?

输入 / 条件
[11,17] 与 [17,21]
输出 / 预期
[17,17]

提示 闭区间允许相等。

展开参考答案与推导

使用 low > high 判冲突,不能使用大于等于。

02怎样验证安装顺序不影响选择?

输入 / 条件
候选 [21,17] 与 [17,21]
输出 / 预期
均选17

提示 最小值扫描。

展开参考答案与推导

对两个列表运行同一需求并断言相同结果。

03如何输出约束冲突的来源?

输入 / 条件
带名称的框架区间与插件区间
输出 / 预期
包含导致最大下界和最小上界的约束名

提示 更新界限时保存来源。

展开参考答案与推导

为上下界记录对应需求,发生冲突时输出两条证据;多个离散约束需更完整解释算法。

读完之后,独立完成

课后作业:自己做一遍

本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。

这次练什么

能核对工具版本,分清错误发生在编译、启动还是请求阶段,逐步缩小环境故障范围。

本篇重点加练

重点加练实际工具链与构建阶段证据,避免本机残留产物掩盖环境问题。

  • Maven 实际 JDK
  • 编译和测试阶段

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01排错实验

    在当前练习项目收集 POM 的 release 配置、终端 java/javac、Maven 使用的 JDK 和 IDE Runner 设置;只读取配置,画出实际编译链,解释哪些证据可能指向不同 JDK。

    提交内容
    脱敏命令输出、设置记录和编译链图。
    怎样算完成
    • 区分运行 Maven 与编译目标
    • 记录实际路径和版本

    练到的知识点POM 与 Java 目标 · Maven 实际 JDK · IDE Runner 与 JAVA_HOME

  2. 作业 02原理问答

    说明 Maven Wrapper 固定了哪部分构建入口,并将编译、测试、打包分别对应到可观察产物;给出“成功但测试未执行”的核验步骤。

    提交内容
    工具职责表及构建证据清单。
    怎样算完成
    • 不把工具下载等同测试通过
    • 测试数量与报告路径可核验

    练到的知识点Maven Wrapper · 编译和测试阶段

  3. 作业 03原理问答

    同一终端能执行 javac 17,Maven 日志却使用另一 JDK。列出无需卸载软件即可确认执行入口、环境来源和编译工具链的最少检查项。

    提交内容
    检查顺序与每步可区分的假设。
    怎样算完成
    • 检查面向实际进程入口
    • 运行和目标版本分别记录

    练到的知识点Maven 实际 JDK

综合应用

  1. 作业 04面试场景

    日志 10:00 显示 Started,10:01 第一次数据库请求失败,10:02 静态健康页成功。根据这三条事实列出已证实、未证实和下一步检查项。

    提交内容
    证据分层表与最短诊断流程。
    怎样算完成
    • 不以进程启动替代依赖验证
    • 检查项能缩小具体未知范围

    练到的知识点启动日志边界 · 首次请求依赖失败

  2. 作业 05排错实验

    为本地 /api/notes 端点记录直连与代理请求的 URL、方法、状态和后端访问日志;在练习配置中构造一次路径前缀不一致,再定位并恢复。

    提交内容
    原配置备份、请求记录、根因与恢复后验证。
    怎样算完成
    • 只操作独立练习配置
    • 前后请求具有可比性
    • 能区分代理与后端响应

    练到的知识点直连后端与代理排查

  3. 作业 06编程实践

    制作一个最小 Maven Java 工程,加入一个必定能被测试执行器发现的行为测试;从干净的项目输出目录完成编译、测试、启动和一次业务调用记录。

    提交内容
    工程、自写正常与失败测试、测试报告及业务调用证据。
    怎样算完成
    • 每阶段独立留证
    • 失败测试的发现机制有验证
    • 不泄露运行配置秘密

    练到的知识点POM 与 Java 目标 · Maven 实际 JDK · Maven Wrapper · 编译和测试阶段 · 启动日志边界 · 首次请求依赖失败

  4. 作业 07排错实验

    在独立练习工程中记录 Maven 实际 JDK 后编译,把一个源码语法错误加入副本再构建;比较返回码、日志阶段和旧 class 文件,随后恢复。

    提交内容
    环境记录、自写成功判据、失败及恢复证据。
    怎样算完成
    • 源码和产物时间关系可追踪
    • 不把旧产物存在当作本轮成功

    练到的知识点Maven 实际 JDK · 编译和测试阶段

  5. 作业 08设计评审

    团队只归档打包后的 jar,没有测试报告。定义可接受一次学习项目构建所需的证据,包含源码版本、测试发现数量及错误退出场景。

    提交内容
    构建验收表与失败样本。
    怎样算完成
    • 证据能定位到同一次运行
    • 阶段完成判据独立

    练到的知识点编译和测试阶段

进阶挑战 · 掌握前置后选做

  1. 作业 09设计评审

    完成 CI 基础后:同一项目需在两台电脑和 CI 构建。设计环境诊断清单与可重复入口,规定版本冲突时的错误信息,避免依赖本机隐式 PATH。

    提交内容
    环境合同、诊断输出草案与迁移验收方案。
    怎样算完成
    • 工具链来源可追踪
    • 区分网络依赖与编译兼容性

    练到的知识点POM 与 Java 目标 · IDE Runner 与 JAVA_HOME · Maven Wrapper · 直连后端与代理排查

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 10思维拓展 · 条件变化

    项目必须生成可在 JDK 17 运行的产物,但构建机器只提供更高版本 JDK。评估需要确认的兼容边界与验证方式,不能仅比较主类字节码。

    提交内容
    兼容性清单和验证链。
    怎样算完成
    • 运行依赖与编译输出都纳入
    • 结论注明实际验证范围

    练到的知识点Maven 实际 JDK · 编译和测试阶段

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗