主题
界面
主题
在 《IOC 与依赖注入》 里,我们手动装配了一个应用——用 @Configuration + @Bean 一个个写出来,容器里总共 9 个 Bean。
本节要做同一件事,但换一种方式:引两个 starter,写一个注解——容器里有 160 个 Bean,其中包括内嵌的 Tomcat、Spring MVC 的前端控制器、JSON 序列化器、参数校验器。
这一个注解替我们做了什么——就是本节的内容。
完成本节后,你应该能够:
@SpringBootApplication 包含的三个注解及各自作用;@ConditionalOnMissingBean 为什么是"自动配置可被覆盖"的关键;--debug 查看自动配置报告,判断"某个自动配置为什么没生效";@Configuration + @Bean 装配吗?(《Spring:IOC 与依赖注入》)@Component、@Autowired 的作用吗?(《Spring:IOC 与依赖注入》 的 5.3 节)手写配置的问题不是"难",而是"规模"。
一个真实的 Web 应用需要什么?至少要:内嵌 Tomcat、Spring MVC 的 DispatcherServlet、请求映射器、参数解析器、JSON 序列化器、异常解析器、参数校验器、静态资源处理……几十上百个组件,还要保证它们的装配顺序正确。
在 Spring Boot 之前,这些要么写在 XML 里(几百行),要么写在 @Configuration 里(同样几百行),而且每换一个技术栈就要重写一遍。
Spring Boot 的答案:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>@SpringBootApplication
public class WebApplication {
public static void main(String[] args) {
SpringApplication.run(WebApplication.class, args);
}
}两处改动,换来了 160 个 Bean。
@SpringBootApplication 是三合一 @SpringBootApplication
// 等价于下面三个:
// @SpringBootConfiguration → 声明这是一个配置类(本质是 @Configuration)
// @EnableAutoConfiguration → 开启自动配置(本节的机制)
// @ComponentScan → 扫描当前包及子包下的 @Component/@Service/@RestController
public class WebApplication {三者分工:
| 注解 | 作用 | 对应到 《Spring:IOC 与依赖注入》 |
|---|---|---|
@SpringBootConfiguration | 允许在这里定义 @Bean | 就是 《Spring:IOC 与依赖注入》 的 AppConfig |
@EnableAutoConfiguration | 自动装配 | 本节新增的部分 |
@ComponentScan | 扫描业务组件 | 《Spring:IOC 与依赖注入》 用的是显式 @Bean |
@ComponentScan 的一个关键约定:它扫描"启动类所在包及其子包"。
cn.kb.learn.web
├── WebApplication.java ← 启动类放在最外层
├── controller/ ← 会被扫到 ✓
├── service/ ← 会被扫到 ✓
├── dto/ ← 会被扫到 ✓
└── exception/ ← 会被扫到 ✓如果启动类放错位置(如 cn.kb.learn.web.app),那么 cn.kb.learn.web.controller 就扫不到——症状是"Bean 找不到"或"接口 404"。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>这一个依赖带来了什么:
| 传递依赖 | 作用 |
|---|---|
spring-boot-starter | 核心(含 spring-boot-autoconfigure) |
spring-boot-starter-tomcat | 内嵌 Tomcat |
spring-web / spring-webmvc | Spring MVC |
jackson-databind | JSON 序列化 |
starter 的本质是"一组协调过的依赖集合"——它解决的不是"能不能找到依赖",而是"版本之间是否兼容"。
命名约定:
| starter | 场景 |
|---|---|
spring-boot-starter-web | Web 应用(含内嵌 Tomcat) |
spring-boot-starter-validation | 参数校验 |
spring-boot-starter-data-jpa / -mybatis | 数据访问 |
spring-boot-starter-test | 测试(JUnit + AssertJ + Mockito + MockMvc) |
spring-boot-starter-data-redis | Redis |
核心问题:Spring Boot 怎么知道"引了 web starter 就该配 Tomcat"?
三步机制:
1. @EnableAutoConfiguration 去读一份"候选配置类清单"
(Spring Boot 3.x:META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,
Spring Boot 2.x:META-INF/spring.factories)
2. 逐个评估这些候选配置类的条件注解
(@ConditionalOnClass / @ConditionalOnMissingBean / @ConditionalOnProperty ...)
3. 条件成立的才生效,注册成 Bean三类最常见的条件注解:
| 注解 | 含义 | 典型用法 |
|---|---|---|
@ConditionalOnClass | 类路径上有某个类才生效 | "有 Tomcat 的类 → 才配内嵌容器" |
@ConditionalOnMissingBean | 容器里还没有这个 Bean 才生效 | "你没自己配,我才给你配默认的" |
@ConditionalOnProperty | 配置项满足条件才生效 | spring.servlet.multipart.enabled=true |
@ConditionalOnMissingBean 是"自动配置可覆盖"的关键:
自动配置:if (容器里没有 ObjectMapper) { 注册一个默认的 }
你的代码:@Bean ObjectMapper objectMapper() { ... } ← 你配了,默认的就不生效这解释了一个重要现象:为什么"自定义一个 Bean"就能覆盖默认行为——不需要关掉自动配置,只要自己提供一个同类型的 Bean 即可 ✓
这也解释了另一个现象:为什么删掉一个 starter,相关功能就"自然消失"——条件不成立,配置类不加载。
启动一次,看容器里有什么(StartupReporter 的实现见第 7 节):
===== 启动完成 =====
应用名:spring-boot-web
端口:18080
BeanDefinition 总数:160
自动配置带来的关键能力(按类型统计):
内嵌 Servlet 容器 → 1 个
DispatcherServlet(前端控制器) → 1 个
HandlerMapping(请求映射) → 8 个
ObjectMapper(JSON 序列化) → 1 个
Validator(参数校验) → 1 个
当前生效的 profile:[]
==================四个值得注意的点:
| 输出 | 含义 |
|---|---|
| 160 vs 《Spring:IOC 与依赖注入》 的 9 | 自动配置的规模——这 151 个 Bean 是你没写的部分 |
| 内嵌 Servlet 容器 1 个 | 引了 web starter,Tomcat 被自动配上 |
| HandlerMapping 8 个 | 不是一个——Spring MVC 注册了多个(请求映射、静态资源、欢迎页……) |
profile:[] | 没有激活任何 profile,用的是默认配置 |
Tomcat 也确实起来了(启动日志):
Tomcat initialized with port 18080 (http)
Starting Servlet engine: [Apache Tomcat/10.1.34]
Tomcat started on port 18080 (http) with context path '/'
Started WebApplication in 4.608 seconds注意 4.608 seconds:这是整个应用启动的时间——包含 160 个 Bean 的创建。启动慢是因为创建的东西多(《Spring:IOC 与依赖注入》 的 5.5 节讲过"单例在启动时创建")。
四种配置来源(从低到高,后面的覆盖前面的):
1. 代码里的默认值(@Bean 中的硬编码、默认属性)
2. application.yml / application.properties(打包在 jar 里)
3. 外部配置文件(jar 同目录的 config/ 目录、命令行 --spring.config.location)
4. 环境变量与命令行参数(--server.port=9090)本项目的配置(src/main/resources/application.yml):
server:
port: 18080 # 换端口,避免与本机其他服务冲突
spring:
application:
name: spring-boot-web # 应用名(日志、监控里会用到)
jackson:
default-property-inclusion: non_null # 序列化时忽略 null 字段
logging:
level:
cn.kb.learn.web: info三种覆盖方式(同一个配置项):
| 方式 | 命令/写法 |
|---|---|
| 配置文件 | server.port: 18080 |
| 命令行参数 | java -jar app.jar --server.port=9090 |
| 环境变量 | SERVER_PORT=9090(大写下划线,Spring 会自动映射) |
优先级的意义:jar 打包一次,可以在不同环境用不同配置运行——这就是"同一份构建产物"能部署到测试与生产的原因(T2-017 CI 分层执行与报告归档会展开)。
# application.yml(公共配置)
spring:
application:
name: spring-boot-web
---
# 开发环境
spring:
config:
activate:
on-profile: dev
logging:
level:
cn.kb.learn.web: debug
---
# 生产环境
spring:
config:
activate:
on-profile: prod
logging:
level:
cn.kb.learn.web: info激活方式:
java -jar app.jar --spring.profiles.active=prod注意 profile 的边界:
| 该放 profile | 不该放 profile |
|---|---|
| 日志级别、连接串、开关 | 业务逻辑分支(if (profile == "prod")) |
| 与部署环境相关的配置 | 密码(用环境变量或密钥管理) |
"按 profile 走不同业务逻辑"是反模式——它会让"测试环境通过、生产环境失败"。业务逻辑只应该有一份。
src/main/java/cn/kb/learn/web/
├── WebApplication.java ← 启动类放在最外层
├── controller/ ← 接收请求、参数绑定、返回响应
│ └── OrderController.java
├── service/ ← 业务逻辑(事务边界通常在这一层)
│ └── OrderService.java
├── repository/ 或 mapper/ ← 数据访问
├── dto/ ← 对外契约(请求/响应对象)
│ ├── CreateOrderRequest.java
│ ├── OrderResponse.java
│ └── PageResponse.java
├── exception/ ← 自定义异常与全局处理
│ ├── BusinessException.java
│ ├── ResourceNotFoundException.java
│ ├── ErrorResponse.java
│ └── GlobalExceptionHandler.java
└── config/ ← 自定义配置类(如拦截器、线程池)
src/main/resources/
├── application.yml
└── (mapper/*.xml 之类的资源)三条分层原则:
| 原则 | 说明 |
|---|---|
| Controller 不写业务逻辑 | 它只做"参数绑定 → 调 Service → 包装响应" |
| DTO 与实体分离 | 不把数据库实体直接返回给前端(S3-REST 的 8.5 节) |
| 异常在边界统一处理 | Controller 里不写 try-catch,交给 @RestControllerAdvice(《参数校验与全局异常处理》) |
当"某个功能没生效"时,第一步是打开自动配置报告:
java -jar app.jar --debug报告分两部分:
============================
CONDITIONS EVALUATION REPORT
============================
Positive matches: ← 生效的自动配置
-----------------
TomcatWebServerFactoryAutoConfiguration matched:
- @ConditionalOnClass found required classes 'jakarta.servlet.Servlet', ...
Negative matches: ← 未生效的自动配置(含原因)
-----------------
RedisAutoConfiguration did not match:
- @ConditionalOnClass did not find required class 'redis.clients.jedis.Jedis'读法:
| 问题 | 看哪里 |
|---|---|
| "我引了依赖,为什么功能没生效?" | Negative matches 里找对应配置,看 did not match 的原因 |
| "为什么我的自定义 Bean 没被用?" | 找 @ConditionalOnMissingBean 相关的配置,检查是否与你的 Bean 类型冲突 |
| "到底加载了多少自动配置?" | Positive matches 的数量 |
另一个更轻量的方式:启动时加 --debug 只在需要时用(它输出很多);日常可以在配置里指定:
logging:
level:
org.springframework.boot.autoconfigure: debug@SpringBootApplication
public class WebApplication {
public static void main(String[] args) {
SpringApplication.run(WebApplication.class, args);
}
}就这两行,Tomcat 起来了、Spring MVC 配好了 ✓
见 5.4 节与第 7 节的 StartupReporter。
@Configuration
public class MyWebConfig {
@Bean
public TomcatServletWebServerFactory tomcatFactory() { /* 几十行 */ }
@Bean
public DispatcherServlet dispatcherServlet() { /* ... */ }
@Bean
public ObjectMapper objectMapper() { /* ... */ }
// ... 还有几十个
}问题:你在重新实现 Spring Boot 已经做好的事——而且大概率做得更差(版本兼容、装配顺序、边界情况)。
正确做法:
| 需求 | 做法 |
|---|---|
| 用默认行为 | 什么都不做 |
| 改某个细节 | 只定义那一个 Bean(@ConditionalOnMissingBean 会让默认的失效) |
| 改配置项 | 写进 application.yml |
| 关掉某个自动配置 | @SpringBootApplication(exclude = XxxAutoConfiguration.class) 或配置项 |
判断标准:"我要覆盖的是"配置"还是"实现"? 配置写 application.yml,实现提供自己的 Bean——都不需要写一大坨 @Configuration。
文件结构(labs/spring-boot-web/):
pom.xml
src/main/resources/application.yml
src/main/java/cn/kb/learn/web/
├── WebApplication.java
├── StartupReporter.java ← 观察自动配置的成果
├── controller/OrderController.java
├── service/OrderService.java
├── dto/{CreateOrderRequest,OrderResponse,PageResponse}.java
└── exception/{BusinessException,ResourceNotFoundException,ErrorResponse,GlobalExceptionHandler}.javaPOM 关键部分(不继承 spring-boot-starter-parent,改用 BOM):
<properties>
<spring-boot.version>3.4.1</spring-boot.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>StartupReporter(启动后打印自动配置的成果):
@Component
public class StartupReporter implements ApplicationRunner {
private final ApplicationContext context;
private final Environment environment;
public StartupReporter(ApplicationContext context, Environment environment) {
this.context = context;
this.environment = environment;
}
@Override
public void run(ApplicationArguments args) {
System.out.println("BeanDefinition 总数:" + context.getBeanDefinitionCount());
System.out.println("自动配置带来的关键能力(按类型统计):");
report("内嵌 Servlet 容器", ServletWebServerFactory.class);
report("DispatcherServlet(前端控制器)", DispatcherServlet.class);
report("HandlerMapping(请求映射)", HandlerMapping.class);
report("ObjectMapper(JSON 序列化)", ObjectMapper.class);
report("Validator(参数校验)", Validator.class);
}
private void report(String label, Class<?> type) {
System.out.println(" " + label + " → " + context.getBeanNamesForType(type).length + " 个");
}
}运行命令:
cd labs
./mvnw -pl spring-boot-web package -DskipTests
java -jar spring-boot-web/target/spring-boot-web.jar预期输出(节选):
. ____ _ __ _ _
/\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
2026-09-21T14:21:28.625+08:00 INFO --- [spring-boot-web] [main] cn.kb.learn.web.WebApplication
: No active profile set, falling back to 1 default profile: "default"
2026-09-21T14:21:31.632+08:00 INFO --- [spring-boot-web] [main] o.s.b.w.embedded.tomcat.TomcatWebServer
: Tomcat started on port 18080 (http) with context path '/'
2026-09-21T14:21:31.878+08:00 INFO --- [spring-boot-web] [main] cn.kb.learn.web.WebApplication
: Started WebApplication in 4.608 seconds (process running for 5.655)
===== 启动完成 =====
应用名:spring-boot-web
端口:18080
BeanDefinition 总数:160
自动配置带来的关键能力(按类型统计):
内嵌 Servlet 容器 → 1 个
DispatcherServlet(前端控制器) → 1 个
HandlerMapping(请求映射) → 8 个
ObjectMapper(JSON 序列化) → 1 个
Validator(参数校验) → 1 个
当前生效的 profile:[]
==================这份输出就是本节的全部论证:
| 数字 | 含义 |
|---|---|
| 160 | 自动配置注册的 Bean 总数(《Spring:IOC 与依赖注入》 手写时只有 9 个) |
| 4.608 秒 | 创建这 160 个 Bean 的代价(启动慢、运行快) |
Tomcat started on port 18080 | 内嵌容器确实起来了——不需要单独装 Tomcat |
HandlerMapping → 8 个 | Spring MVC 的复杂度被自动配置封装了 |
SpringApplication.run(...) 处按 F7 进入:观察 refreshContext → invokeBeanFactoryPostProcessors 的流程——自动配置就是在这里被评估的;StartupReporter.run 处断点,然后求值 context.getBeanDefinitionNames().length——看那 160 个名字里有多少是你认识的;getBeanNamesForType(ObjectMapper.class) 处断点:观察返回的 Bean 名(jacksonObjectMapper),它是自动配置注册的;spring-boot-autoconfigure jar 里的 AutoConfiguration.imports:那是一个文本文件,列着所有候选自动配置类的全限定名——这就是"清单"的真身;--debug 启动,在输出里搜 TomcatWebServerFactoryAutoConfiguration 与 RedisAutoConfiguration——对比 Positive 与 Negative 两部分的措辞。cn.kb.learn.web.app.WebApplication ← 启动类在子包里
cn.kb.learn.web.controller.OrderController ← 扫不到 ✗症状:接口 404,或者启动时报"找不到 Bean"。
修复:启动类放在最外层的包(cn.kb.learn.web),或用 @ComponentScan("cn.kb.learn.web") 显式指定。
spring-boot-starter-parent 时漏掉 -parameters 这是本项目实际踩到的坑(在 《Spring MVC 请求映射与参数绑定》 会详细展开):
java.lang.IllegalArgumentException: Name for argument of type [java.lang.String] not specified,
and parameter name information not available via reflection.
Ensure the compiler uses the '-parameters' flag.原因:spring-boot-starter-parent 会自动为你开启 -parameters 编译标志;不继承它就得手动配:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<parameters>true</parameters>
</configuration>
</plugin>修复方式二:给每个 @PathVariable / @RequestParam 显式写名字(@PathVariable("orderNo"))——但加编译标志更彻底。
症状:"我加了 Redis starter,为什么 RedisTemplate 注入不进去?"
排查:
--debug 看 Negative matches——里面会写明原因(如"缺少某个类");@SpringBootApplication(exclude = ...));@ConditionalOnProperty 的配置项没开)。if (environment.acceptsProfiles(Profiles.of("prod"))) {
doProductionLogic(); // ✗ 反模式
} else {
doTestLogic();
}后果:"测试环境通过、生产环境失败"——因为分支让两套环境的代码路径不同。
修复:业务逻辑只应该有一份;环境差异只放在配置(连接串、地址、开关)里 ✓
application.yml spring:
datasource:
password: 123456 # ✗ 提交进 Git 了修复:用环境变量或密钥管理服务:
spring:
datasource:
password: ${DB_PASSWORD} # 从环境变量读这一条在 《配置管理》 会展开。
目标:亲手观察自动配置的机制。
步骤:
spring-boot-starter-web 从 pom 里注释掉,重新打包启动——观察报错(没有内嵌容器,应用直接退出或找不到 Web 相关类);spring-boot-starter-validation(本项目已有),观察 Validator 从"0 个"变成"1 个"——验证"引 starter → 自动配置生效";@Bean ObjectMapper(定制日期格式),然后用 context.getBeanNamesForType(ObjectMapper.class) 观察——默认的那个被替换了(@ConditionalOnMissingBean 生效);--debug 启动,把输出重定向到文件,统计 Positive matches 与 Negative matches 各有多少条;Redis:找到 RedisAutoConfiguration did not match,读一下它给出的原因。验收方式:能说出第 2 步的报错、第 4 步"覆盖"的机制。
扩展任务:把 application.yml 拆成 application.yml(公共)+ application-dev.yml + application-prod.yml,用 --spring.profiles.active=dev 启动,验证不同 profile 下日志级别与端口不同。
基础题
@SpringBootApplication 等价于 @Configuration + @EnableAutoConfiguration + @ComponentScan。(对 / 错)代码阅读题
package com.example.demo.controller;
import org.springframework.web.bind.annotation.*;
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
}package com.example.demo.app;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}GET /hello 会返回什么?编程题
ApplicationRunner 在启动后打印: BeanDefinition 总数;DispatcherServlet、HandlerMapping、ObjectMapper、Validator 各几个;server.port 的值(从 Environment 读,证明"配置外置生效");--spring.profiles.active=prod"下启动,对比输出;Environment 读配置比直接读 application.yml 更正确?综合任务
CreateTodoRequest 与 TodoResponse 要分开(而不是共用一个 Todo 类);application.yml、哪些该放环境变量、哪些该按 profile 区分;提示 1(方向):第 4 题的关键是启动类所在包与 Controller 所在包的关系。
提示 2(关键点):第 6 题的"Environment vs 直接读 yml"——前者包含所有来源(配置文件、命令行参数、环境变量)合并后的结果。
提示 3(接近答案):第 7 题的"请求 DTO 与响应 DTO 分开"的理由:入参与出参的字段集不同(入参有密码、出参有 id 与创建时间)、校验规则只该加在入参上、以及"改一个不影响另一个"。
@SpringBootApplication 包含的是 @SpringBootConfiguration(@Configuration 的特化)+ @EnableAutoConfiguration + @ComponentScan,还带了若干 @AliasFor 方便你直接配置 exclude 等属性。同时注意:它还包含 @EnableAutoConfiguration 的 excludeFilters——排除了 TypeExcludeFilter 与 AutoConfigurationExcludeFilter(后者保证"自动配置类不会被组件扫描重复注册")。
@ConditionalOnMissingBean。@ConditionalOnMissingBean:@Bean
@ConditionalOnMissingBean
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) { ... }它的判定时机:在你的 @Bean 被注册之后(用户配置优先级更高)✓ 所以:
容器里已有你的 ObjectMapper → 条件不成立 → 默认的不注册
容器里没有 → 条件成立 → 注册默认的这就是"约定优于配置,但配置可覆盖"的机制实现——也是为什么"改一个 Bean"比"关掉整个自动配置"更精确。
第 4 题:不能正常工作,GET /hello 会返回 404。
原因:DemoApplication 在 com.example.demo.app 包下,而 @ComponentScan 只扫描这个包及其子包——com.example.demo.controller 不在其中 ✗
结果:HelloController 根本没被注册成 Bean,请求自然也匹配不到 ✓
第 5 题:两种修复方式。
方式一:把启动类移到最外层包(推荐)
package com.example.demo; // ← 提到 com.example.demo
@SpringBootApplication
public class DemoApplication { ... }此时 com.example.demo.controller 在它的子包下 ✓ 会被扫到。
方式二:显式指定扫描范围
package com.example.demo.app;
@SpringBootApplication(scanBasePackages = "com.example.demo")
public class DemoApplication { ... }哪种更好:方式一——它是 Spring Boot 的约定("启动类放在根包"),约定比配置更不容易出错(新同事加了一个 com.example.demo.service 包,方式一自动生效,方式二需要有人记得改配置)。
顺带一个排查技巧:遇到"接口 404 但代码看着没问题",第一件事是确认 Controller 的包在不在启动类的扫描范围内。
package cn.kb.learn.web;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.validation.Validator;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.HandlerMapping;
import java.util.Arrays;
@Component
public class AutoConfigInspector implements ApplicationRunner {
private final ApplicationContext context;
private final Environment environment;
public AutoConfigInspector(ApplicationContext context, Environment environment) {
this.context = context;
this.environment = environment;
}
@Override
public void run(ApplicationArguments args) {
System.out.println();
System.out.println("===== 自动配置观察 =====");
System.out.println("BeanDefinition 总数:" + context.getBeanDefinitionCount());
System.out.println("激活的 profile:" + Arrays.toString(environment.getActiveProfiles()));
report("内嵌 Servlet 容器", ServletWebServerFactory.class);
report("DispatcherServlet", DispatcherServlet.class);
report("HandlerMapping", HandlerMapping.class);
report("ObjectMapper", ObjectMapper.class);
report("Validator", Validator.class);
// 关键:端口的值可能来自配置文件、命令行参数或环境变量,Environment 会把它们合并
System.out.println("server.port = " + environment.getProperty("server.port"));
System.out.println("配置文件来源:" + Arrays.toString(
context.getEnvironment().getPropertySources().stream()
.map(source -> source.getName())
.filter(name -> name.contains("application"))
.toArray()));
System.out.println("=========================");
}
private void report(String label, Class<?> type) {
String[] names = context.getBeanNamesForType(type);
System.out.println(" " + label + " → " + names.length + " 个"
+ (names.length > 0 && names.length <= 3 ? ":" + Arrays.toString(names) : ""));
}
}两种启动方式的对比:
java -jar target/spring-boot-web.jar
java -jar target/spring-boot-web.jar --spring.profiles.active=prod --server.port=9090预期差异:
| 输出项 | 默认 | 加参数后 |
|---|---|---|
| profile | [] | [prod] |
server.port | 18080 | 9090(命令行覆盖了配置文件) |
| Bean 数量 | 160 | 可能不同(profile 专属配置生效) |
为什么用 Environment 而不是直接读 application.yml:
| 直接读 yml | 用 Environment |
|---|---|
| 只拿到文件里写的值 | 拿到所有来源合并后的最终值 |
| 不知道命令行覆盖 | 反映命令行、环境变量、profile 的全部影响 |
| 在测试里难以替换 | 可以在测试里用 @TestPropertySource 注入 |
一句话:Environment 是 Spring 的"配置视图",它已经帮你做完了优先级合并——你读到的就是"应用真正在用的值" ✓
这个原则可以推广:不要绕过框架去读原始数据源,用它提供的抽象(就像不该绕过 ORM 直接拼 SQL 一样)。
项目结构:
src/main/java/com/example/todo/
├── TodoApplication.java 【启动类】最外层包
├── controller/
│ └── TodoController.java 【Web 层】参数绑定 → 调 Service → 返回响应
├── service/
│ ├── TodoService.java 【业务层】业务规则、事务边界
│ └── impl/TodoServiceImpl.java (接口与实现分离,便于测试)
├── repository/
│ └── TodoRepository.java 【数据层】数据访问
├── dto/
│ ├── CreateTodoRequest.java 【入参】含校验注解
│ ├── UpdateTodoRequest.java 【入参】
│ └── TodoResponse.java 【出参】只含该暴露的字段
├── exception/
│ ├── BusinessException.java
│ ├── ResourceNotFoundException.java
│ ├── ErrorResponse.java
│ └── GlobalExceptionHandler.java 【边界】统一异常处理
└── config/
└── (自定义配置类)
src/main/resources/
├── application.yml 公共配置
├── application-dev.yml 开发环境(日志详细、内存库)
└── application-prod.yml 生产环境(日志收敛、连接池)各层职责:
| 类 | 层 | 职责 |
|---|---|---|
TodoController | Web | 绑定参数、触发校验、调 Service、组装 ResponseEntity(不写业务逻辑) |
TodoService | Service | 业务规则(如"截止日期不能早于今天")、事务边界 |
TodoRepository | 数据 | 持久化 |
CreateTodoRequest | DTO | 入参契约 + 校验规则 |
TodoResponse | DTO | 出参契约 |
GlobalExceptionHandler | 边界 | 异常 → 状态码 + 统一响应体 |
为什么 CreateTodoRequest 与 TodoResponse 要分开:
| 理由 | 说明 |
|---|---|
| 字段集不同 | 入参没有 id、createdAt、完成状态(由服务端决定);出参不该有"内部字段" |
| 校验规则只该在入参上 | 给 TodoResponse 加 @NotBlank 毫无意义,反而会让人困惑 |
| 演进方向不同 | 前端表单加一个字段 → 改 Request;接口加一个返回字段 → 改 Response——互不影响 |
| 安全 | 用同一个类时,很容易"顺手"把敏感字段返回出去(S3-REST 的 8.5 节) |
配置的归类:
| 配置项 | 放哪里 | 理由 |
|---|---|---|
server.port | application.yml(默认可覆盖) | 与环境无关的默认值 |
| 日志级别 | application-dev.yml / -prod.yml | 按环境区分 |
| 连接串(host/port) | 环境变量 / 外部配置 | 每套环境不同,且不想打包进 jar |
| 数据库密码 | 环境变量 / 密钥管理 | 绝不能进 Git |
| 业务开关(如"是否启用邮件通知") | application.yml | 是配置,不是环境差异 |
最可能被自动配置覆盖的三个 Bean:
| Bean | 自动配置给的默认 | 覆盖方式 | 什么情况下要覆盖 |
|---|---|---|---|
ObjectMapper | Jackson 默认(日期为时间戳、包含 null) | 定义自己的 @Bean 或用 application.yml 的 spring.jackson.* | 要统一日期格式、忽略 null |
TaskExecutor | 简单的线程池 | 定义自己的 ThreadPoolTaskExecutor | 要控制线程数、队列容量、拒绝策略 |
Validator | 默认的 Hibernate Validator | 定义 LocalValidatorFactoryBean(如自定义 MessageInterpolator) | 要自定义错误消息来源 |
覆盖的一般方式(三选一):
@ConditionalOnMissingBean 让它自动失效);spring.jackson.date-format);@SpringBootApplication(exclude = XxxAutoConfiguration.class))——最后手段,因为它会让整个自动配置都不生效。推荐顺序:① 改配置 → ② 覆盖 Bean → ③ 排除自动配置。
@SpringBootApplication 包含的三个注解及分工;@ConditionalOnMissingBean 是"可覆盖"的关键;--debug 看自动配置报告,并能从 Negative matches 读出"为什么没生效";spring-boot-starter-parent 时要手动开启 -parameters;《Spring MVC 请求映射与参数绑定》:一个 HTTP 请求是怎么变成 Java 方法调用的。