qinghuan
← Java Web / 全部笔记

单元 42 / Java Web

P0 · 必会主线L1 · 基础

Spring Boot:启动、自动配置与 Profiles

从启动入口追到容器创建,按条件解释 Bean 为什么出现或缺失,再用 Profile 管理环境差异。

本篇目录 · 按需要跳转

读完这篇,你能做什么

能解释某个配置为何生效,并通过端口或接口行为验证环境选择。

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

为什么学它

Boot 将启动与配置组合起来,理解条件和环境选择才能定位默认行为。

先读这些内容

知识点清单

  • SpringApplication 启动
  • SpringBootApplication 组合
  • 组件扫描包范围
  • 条件自动配置
  • 用户 Bean 退让
  • debug 条件报告
  • Profile 激活规则
  • 属性覆盖与配置版本

运行前先确认Java17、Spring Boot3.4;源码固定 v3.4.9。属性算法使用显式给定的低到高优先级与单 profile 条件,不实现完整 Config Data 导入规则。

01先弄懂它是什么

Spring Boot 帮助应用按约定装配和启动。main 调用 SpringApplication.run 后,它准备配置环境并建立 Spring 容器。@SpringBootApplication 组合了应用配置、组件扫描和自动配置。启动类常放在业务包上层,让默认扫描覆盖下面的类;这不表示所有依赖 jar 里的任何类都会自动变成 Bean。

自动配置的关键是“满足条件才生效”。例如类路径有某个库、属性符合要求、容器还没有某类 Bean,才可能启用对应配置。starter 主要带来一组依赖,所以引入后也可能仍缺 Bean;自定义 Bean 有时会让默认配置退让,但要看它具体声明的条件,不能假定所有自动配置都按同一规则处理。

Profile 用来组织不同环境下的配置选择。application.yml 可有公共默认值,application-dev.yml 或激活文档提供开发差异,再由更高优先级来源覆盖。Profile 不是秘密保护措施,也不自动阻止开发配置进入错误环境。敏感值应通过适当外部来源管理,配置激活与值来源需要可追踪。

文中这几个词,先认一认

自动配置条件
决定某项默认配置是否启用的检查,例如有没有相关库、属性和已注册 Bean。
有效配置
多份配置按优先级合并后,应用此刻真正读取并使用的值。
条件评估报告
记录哪些自动配置满足条件、哪些没满足,以及具体原因的诊断报告。

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

02看看它怎么工作

启动时,Boot 先准备 Environment,也就是应用读取配置值的统一入口;再创建应用上下文,也就是 Spring 容器,并在 refresh 阶段完成主要初始化。这个顺序有原因:要先知道配置值,才能决定启用哪些配置类、怎样把属性填进对象、哪些条件成立。如果业务代码又各自随意读取配置文件,就可能绕开统一的覆盖规则,让同一个配置名出现几个不同答案。

可以用三个受控启动验证理解覆盖:默认 server.port=8080,dev 文件配置 8081,启动加 --spring.profiles.active=dev 后验证监听8081;再加 --server.port=8090,验证命令行值生效。

每次只改变一个输入并记录实际端口。spring.profiles.active 不应放在 profile 专属文档或被 on-profile 激活的文档中,避免让激活来源自我定义。

日志出现 Started 时,启动流程还可能有后续工作。在本文固定版本中,先输出启动信息并发出 started 通知,再执行 ApplicationRunner 和 CommandLineRunner,之后才发出 ready 通知。Runner 抛出异常,仍可能让启动失败。收到 ready 后,还要实际发起业务请求:延迟发生的数据库访问、权限判断或代理调用,可能到这时才暴露问题。

先跑通一个最小例子

在独立空目录保存这些文件,用最少的 HTTP 接口观察默认配置和 dev Profile。此练习固定 Spring Boot 3.4.9,与本文源码标签一致,用于复现机制;维护实际项目时需另外选择受支持的补丁版本。无需先启动数据库或 Redis。

pom.xml · XML
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.4.9</version>
    <relativePath/>
  </parent>
  <groupId>study.qinghuan</groupId>
  <artifactId>first-boot</artifactId>
  <version>1.0.0</version>
  <properties><java.version>17</java.version></properties>
  <dependencies><dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency></dependencies>
  <build><plugins><plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
  </plugin></plugins></build>
</project>
src/main/java/study/qinghuan/Application.java · Java
package study.qinghuan;

import java.util.Map;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args); // 参数传给启动流程和环境处理。
    }
    @GetMapping("/hello")
    public Map<String, String> hello() {
        return Map.of("message", "Hello Java"); // Web 配置负责将对象序列化成 JSON。
    }
}
src/main/resources/application.properties · Properties
server.address=127.0.0.1
server.port=8080
src/main/resources/application-dev.properties · Properties
server.port=8081
构建并依次启动;每轮 Ctrl+C 结束后再开始下一轮 · PowerShell
mvn -version
mvn package
java -jar target/first-boot-1.0.0.jar
# 第二个终端:curl.exe http://127.0.0.1:8080/hello

# 结束上一轮后,使用 dev 配置:
java -jar target/first-boot-1.0.0.jar --spring.profiles.active=dev
# 第二个终端:curl.exe http://127.0.0.1:8081/hello

# 再次结束后,命令行覆盖 dev 的端口:
java -jar target/first-boot-1.0.0.jar --spring.profiles.active=dev --server.port=8090
# 第二个终端:curl.exe http://127.0.0.1:8090/hello

核对运行结果三轮分别在 8080、8081、8090 返回 {"message":"Hello Java"}。使用各轮匹配的地址访问;端口占用时先定位占用者再选空闲端口。这里的验收是构建、监听与一次真实 HTTP 响应,不能由启动日志单独替代。

正在加载流程图…

固定版本常见启动顺序;就绪事件与业务链路验证仍属于不同证据。
查看流程图源码(Mermaid)
flowchart TD
 A["main 与启动参数"] --> B["准备 Environment"]
 B --> C["创建并准备上下文"]
 C --> D["条件配置与 refresh"]
 D --> E["started 与启动日志"]
 E --> F["运行 Runner"]
 F --> G["ready 通知"]
 G --> H["验证业务请求"]
 D --> I["条件报告定位未匹配"]

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

  1. 读取 parent 或依赖管理,确认 Boot 与 Java 版本一致。
  2. 检查启动类包位置以及显式扫描和导入范围。
  3. 开启 --debug 查看条件报告,定位目标 Bean 对应条件。
  4. 用默认、dev、命令行覆盖三组输入验证端口和来源。
  5. 检查 Runner 结果并发起真实业务请求验证配置影响。

03对照实现代码

SpringApplication.run 的启动主线 (新标签页)↗

Spring Boot v3.4.9 · SpringApplication#run(String...)、#prepareEnvironment

原创顺序模型,非源码节选。省略事件对象、异常处理与上下文类型选择;通过受控轨迹表达 environment、refresh、runner 和 ready 的相对时序。

核心机制代码 · java
import java.util.*;
class SourceModel {
    static List<String> startup(boolean runnerFails) {
        List<String> trace = new ArrayList<>();
        trace.add("environment"); // 条件评估前先有环境。
        trace.add("context");
        trace.add("refresh");
        trace.add("started");
        trace.add("runners");
        if (runnerFails) {
            // Runner 失败时不能宣称 ready。
            trace.add("failed");
            return List.copyOf(trace);
        }
        trace.add("ready"); // 仅表示此教学启动流程已完成。
        return List.copyOf(trace);
    }
}

run 先将启动参数包装并调用 prepareEnvironment,再创建和准备应用上下文。配置来源是启动过程的输入,不是完成后的装饰。

refreshContext 之后还有 afterRefresh、启动信息与 started 通知,随后才调用 runners。判断日志含义应看它位于哪个阶段。

Runner 与主要启动工作处于失败处理范围内,异常会交给 handleRunFailure;不能仅凭前面已有 Started 行忽略后续失败。

ready 通知在上下文运行条件下执行。教学轨迹没有模拟所有事件、AOT 和 Web 类型路径,读者应从自己版本的真实分支继续追踪。

代码先调用谁,再调用谁

  1. main 调用静态 run
  2. 创建 SpringApplication
  3. prepareEnvironment
  4. prepareContext / refreshContext
  5. callRunners
  6. listeners.ready

04动手写一个例子

解析配置覆盖并保留获胜来源

输入按低到高优先级排列的配置层,每层可指定一个激活 profile。忽略未激活层,用后出现值覆盖同名键,并返回值及来源名。空字符串是显式值,不能当成缺失。该受限模型不解析 YAML 或完整 Boot 优先级。

输入 1
base端口8080、dev层8081、cli层8090;激活dev
输出 1
8090,来源cli
输入 2
dev未激活且没有cli层
输出 2
8080,来源base

先做层是否适用的判断,再合并键。未激活层不能参与覆盖,否则即使正确理解优先级也会得到错误环境值。

低到高遍历时直接替换同名键,同时保存来源;这使每次覆盖都有解释。属性值与来源一起保存,避免只打印最终值却找不到配置文件。

空字符串和不存在分别处理。本题 Map 中有键就代表显式设置;Boot 对不同目标类型和来源有更丰富转换行为,不应直接把模型的字符串结果当成绑定成功。

为什么这样写是对的处理到第 k 层后,每个结果键都来自前 k 层中最后一个已激活且显式包含该键的层。

时间 均摊 O(K)额外空间 O(U)

K 为所有层属性总数,遍历适用键并哈希覆盖;U 为最终不同键数。

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

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

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

Main.java · 完整可运行程序 · Java
import java.util.*;
public class Main {
    record Layer(String name, String profile, Map<String,String> values) {}
    record Value(String text, String source) {}
    static Map<String,Value> resolve(List<Layer> layers, Set<String> active) {
        Map<String,Value> result = new HashMap<>();
        for (Layer layer : layers) {
            // 空 profile 表示公共层,其余层需显式激活。
            if (layer.profile() != null && !active.contains(layer.profile())) continue;
            for (var entry : layer.values().entrySet()) {
                // 后层覆盖前层,并同步记录可解释来源。
                result.put(entry.getKey(),new Value(entry.getValue(),layer.name()));
            }
        }
        // 不允许调用者修改已经解析出的配置快照。
        return Map.copyOf(result);
    }
    static void eq(Object expected, Object actual) {
        if (!Objects.equals(expected,actual)) throw new AssertionError(actual);
    }
    public static void main(String[] args) {
        Layer base = new Layer("base",null,Map.of("port","8080","name","notes"));
        Layer dev = new Layer("dev","dev",Map.of("port","8081"));
        Layer cli = new Layer("cli",null,Map.of("port","8090"));
        eq(new Value("8080","base"),resolve(List.of(base,dev),Set.of()).get("port"));
        eq(new Value("8081","dev"),resolve(List.of(base,dev),Set.of("dev")).get("port"));
        eq(new Value("8090","cli"),resolve(List.of(base,dev,cli),Set.of("dev")).get("port"));
        eq(new Value("notes","base"),resolve(List.of(base,dev),Set.of("dev")).get("name"));
        eq(new Value("","empty"),resolve(List.of(base,
            new Layer("empty",null,Map.of("name",""))),Set.of()).get("name"));
        eq(Map.of(),resolve(List.of(),Set.of()));
        System.out.println("PASS 6 configuration precedence tests");
    }
}

这些特殊情况也要试一试

  • 无配置层返回空结果;未激活层完全不参与覆盖。
  • 空字符串保留为显式值,不能偷偷回退到默认层。
  • 多个 profile 同时激活时仍按给定层顺序覆盖,本题不自行推导 Boot 文档加载次序。
进阶选读 · 配置来源链和覆盖风险审计

场景多环境有同名配置,最终值正确却长期被错误高优先级来源遮蔽。

做法为每个配置名记录哪些来源提供了值、后来被谁覆盖,再检查这些值的目标类型和允许使用的环境。展示覆盖过程时隐藏敏感内容;部署前,将结果与已经确认的配置摘要比较,找出意外覆盖。

复杂度保留全部候选 O(K) 时间与空间,排序来源或差异展示另计。

好处与代价来源审计提高解释性,但必须避免把凭据内容暴露;实际加载规则应直接使用 Boot 的属性来源和 origin 能力。

05项目里怎么用

缺 Bean 时,先从条件报告找到对应配置,再检查库是否存在、属性是否满足、容器是否已有其他 Bean。不要遇事只扩大扫描包:自动配置导入与组件扫描是不同过程。自定义组件让默认组件退出后,也要测试它是否仍满足原来使用方的要求。

Profile 用来表达环境差异,应避免随意叠加出难以追踪的组合。记录当前激活哪些 Profile、值来自哪里,并校验端口、容量和超时。若线上临时用启动参数覆盖文件,要记下何时撤销及谁修改,否则下次发布仍可能被旧参数盖住。

把就绪检查与关键业务验证配合。容器启动成功后,至少在隔离环境验证数据库读写、缓存和认证主线;对延迟初始化组件增加有针对性的测试。启动速度优化也需要保留失败可见性,不能仅靠推迟初始化把错误搬到第一位用户请求。

容易踩的坑

  • 引入 starter 后认为目标 Bean 无条件存在。
  • 把启动类放在狭窄包下却假定能扫描所有业务包。
  • 在 profile 专属文档里设置 spring.profiles.active。
  • 看到 Started 后忽略 Runner 失败或延迟请求异常。

几种做法,怎么选

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

方案优势代价与限制适用条件
自动配置默认值减少重复配置并遵循框架约定。需要理解条件与版本变更。标准组件集成。
显式自定义 Bean关键行为可控且便于测试。承担默认配置退让后的全部责任。有明确特殊需求的组件。
多 Profile 配置集中组织环境差异。组合和覆盖来源可能复杂。差异清晰且有部署记录的环境。

06检验是否读懂了

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

先用自己的话解释

01starter 与自动配置有什么区别?

展开参考答案与推导

starter 提供依赖组合,自动配置按条件注册组件,两者配合但职责不同。

02Profile 能保护秘密吗?

展开参考答案与推导

不能,它只影响配置选择;凭据仍需合适外部管理与访问控制。

03配置文件写了键就一定有业务效果吗?

展开参考答案与推导

不一定,需要框架或业务组件实际绑定并消费。

换个情况,还会处理吗

01依赖已存在但没有数据源 Bean,先查什么?

展开参考答案与推导

查条件报告与数据源配置,确认条件、现有 Bean 和类路径是否匹配。

02dev 文件写8081却监听8090怎么解释?

展开参考答案与推导

追踪更高优先级来源,例如命令行覆盖,再验证实际激活集。

03Started后Runner抛异常能算启动完成吗?

展开参考答案与推导

不能,应检查失败处理与进程状态;ready和业务请求还需要各自证据。

写代码试一试

01如何验证未激活层不污染公共值?

输入 / 条件
base=8080,dev=8081,active空
输出 / 预期
8080来自base

提示 先过滤层。

展开参考答案与推导

直接断言值和来源,而不只断言值。

02如何区分空字符串与缺失?

输入 / 条件
高优先级name为空字符串
输出 / 预期
保留空字符串

提示 检查键存在性。

展开参考答案与推导

覆盖时依据是否包含键,不用 isBlank 判定是否回退。

03怎样显示完整覆盖过程?

输入 / 条件
三个层设置相同port
输出 / 预期
base→dev→cli

提示 将单 Value 改为候选列表。

展开参考答案与推导

每个适用层追加值与来源,最终值取最后一个;输出时对敏感键脱敏。

读完之后,独立完成

课后作业:自己做一遍

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

这次练什么

能解释某个配置为何生效,并通过端口或接口行为验证环境选择。

本篇重点加练

重点核验自动配置条件与有效属性,区分声明配置、实际注册和最终行为。

  • 条件自动配置
  • 属性覆盖与配置版本

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

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

基础必做

  1. 作业 01原理问答

    把一个最小 Boot 应用从 main 到可接收请求的过程画成阶段图,并拆解 @SpringBootApplication 涉及的职责;为每阶段寻找可观察证据。

    提交内容
    启动阶段图与证据表。
    怎样算完成
    • 组合注解的职责可区分
    • 启动过程不简化为一个方法名

    练到的知识点SpringApplication 启动 · SpringBootApplication 组合

  2. 作业 02排错实验

    在独立项目将组件移到扫描边界外,并为一个依赖条件构造存在/缺失两组运行配置;记录 Bean 是否注册和对应条件报告。

    提交内容
    配置、自写上下文检查和运行记录。
    怎样算完成
    • 扫描与自动配置条件分开
    • 故障可逐一恢复

    练到的知识点组件扫描包范围 · 条件自动配置

综合应用

  1. 作业 03面试场景

    团队自定义一个基础组件后框架默认组件不再出现。根据条件报告判断是否符合需求,说明如何确认最终被注入的实现而不只读依赖树。

    提交内容
    报告摘录、实例证据和评审结论。
    怎样算完成
    • 结论基于实际条件
    • 用户 Bean 行为纳入测试

    练到的知识点用户 Bean 退让 · debug 条件报告

  2. 作业 04设计评审

    开发和测试环境都定义 server.port 与业务超时,命令行再次覆盖端口。写出本项目版本下的配置来源清单及预期解析政策,并规定启动时如何脱敏报告。

    提交内容
    属性来源表、Profile 启动矩阵和核验步骤。
    怎样算完成
    • 版本及激活方式明确
    • 配置值和来源均可追踪

    练到的知识点Profile 激活规则 · 属性覆盖与配置版本

  3. 作业 05编程实践

    实现两个 Profile 的本地消息发送器:一个记录内存事件,一个输出脱敏控制台消息;业务服务通过同一接口调用,测试未激活和冲突配置的政策。

    提交内容
    最小工程、自写上下文与行为测试、运行证据。
    怎样算完成
    • 按实际注入对象验证行为
    • 没有秘密配置进入输出
    • 异常配置有明确结果

    练到的知识点SpringApplication 启动 · 组件扫描包范围 · 用户 Bean 退让 · Profile 激活规则

  4. 作业 06排错实验

    选择项目中一项按属性决定的自动配置,分别输入属性缺失、合法值、空字符串与拼错键名;先查明该条件的版本合同,再观察条件报告和实际 Bean。

    提交内容
    四组配置、自写上下文检查与运行证据。
    怎样算完成
    • 缺失与显式输入分别验证
    • 错误键名的行为由实际条件证据支持

    练到的知识点条件自动配置 · 属性覆盖与配置版本

  5. 作业 07设计评审

    应用依赖中增加新 starter 后出现同类型基础组件。制定检查条件、候选对象与业务行为的评审流程,允许最后选择保留或禁用但需证据。

    提交内容
    依赖变化表与配置决策记录模板。
    怎样算完成
    • 选择依据来自真实条件
    • 无无依据批量排除配置

    练到的知识点条件自动配置

  6. 作业 08面试场景

    文件配置写 8081、部署环境注入 8082,运维仍访问旧端口。设计能指出最终值及来源的诊断信息,并规定避免暴露的属性。

    提交内容
    脱敏诊断格式与连接验证流程。
    怎样算完成
    • 有效值与实际监听分别核对
    • 配置秘密不进入公开接口

    练到的知识点属性覆盖与配置版本

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

  1. 作业 09设计评审

    完成依赖升级基础后:Boot 小版本升级后自动配置条件发生变化。设计升级前后的差异核验,覆盖条件报告、实际 Bean、有效配置和首个业务请求。

    提交内容
    升级验证矩阵与可回退条件。
    怎样算完成
    • 不只比较启动是否成功
    • 变化能追溯到具体条件或配置

    练到的知识点条件自动配置 · 用户 Bean 退让 · debug 条件报告 · 属性覆盖与配置版本

思维拓展

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

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

    同一应用以命令行批处理方式运行时不需 Web 服务器。审查自动配置和端口配置原有前提,设计两种启动模式的最小验收集。

    提交内容
    模式差异图与验收矩阵。
    怎样算完成
    • 启动形态和功能需求对应
    • 不把某个日志缺失直接当成失败

    练到的知识点条件自动配置 · 属性覆盖与配置版本

完成后,再检验一次

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

继续阅读

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

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