从大四第一次接触 Spring 到后来被各种 XML 配置劝退,再到 Spring Boot 一把梭,我太懂那种“配置地狱”带来的挫败感了。今天这篇 Java 学习日记 Day51,就专注两件事:一是把 Spring Boot 的自动配置原理讲透,二是带着你亲手跑通一个 HelloWorld。这不是照搬官方文档的翻译,而是我自己在实际项目中踩过坑、翻过源码之后整理出来的学习笔记,适合刚学完 Spring MVC、正在尝试 Spring Boot 的新手,也适合用 Spring Boot 写过一点小 Demo、但始终没搞懂“为什么我们啥都不用配置”的开发者。
1. 为什么 Spring Boot 能“开箱即用”
1.1 传统 Spring 开发的繁琐之处
在说 Spring Boot 之前,先聊聊以前的 Spring 项目是什么画风。做一个 Web 项目,你得先准备一个web.xml,里面要配置 DispatcherServlet,还要引入 Spring 的监听器 ContextLoaderListener。数据源、SqlSessionFactory、事务管理器,每一层都得写一堆 Bean 配置。如果用到 Redis、消息队列、邮件服务,还要上网搜一坨第三方依赖和对应的配置片段,复制粘贴完还要小心版本冲突。
我当时第一次搭一个带 MyBatis 的小项目,光applicationContext.xml就写了三百多行。这还算少的,同事的配置文件里光<bean>标签就有几十个。每次换团队、换电脑、升级依赖,最怕的就是启动报错,因为错误信息往往指向某个 Bean 没有找到,而原因可能是 XML 里漏了一个<property>,也可能是 jar 包版本不对。这种体验让很多人觉得 Spring 强大但不友好。
1.2 Spring Boot 的三板斧:起步依赖、自动配置、命令行启动
Spring Boot 的解决思路其实很质朴:把常用的配置逻辑“内建”进框架,把项目运行方式从“部署到外部容器”改成“一个 main 方法直接跑”。这三板斧缺一不可:
- 起步依赖(Starter):把一组常用依赖打包到一起。比如引入
spring-boot-starter-web,就相当于引入了 Spring MVC、Jackson、Tomcat 等一堆 Web 开发必需的库,不用自己一个个找版本号和 Maven 坐标。 - 自动配置(AutoConfiguration):根据你引入的依赖和当前环境,自动帮你创建需要的 Bean。引入了 Redis 的 starter,就自动配置连接工厂和模板;引入了
spring-boot-starter-web,就自动配置 DispatcherServlet、消息转换器、错误页等。 - 内置容器与可执行 Jar:Spring Boot 默认内置 Tomcat,项目打包成可执行 Jar 后直接
java -jar启动,不再需要额外装 Tomcat 或 Jetty。
这三板斧里面,最神奇的就是自动配置。很多初学者第一次写 Spring Boot 项目时都会有同一个疑问:我只写了一个 Controller,为什么启动后就能通过 HTTP 访问它?谁把 DispatcherServlet 注册进 Servlet 容器的?谁帮我配置的 Jackson 对象转 JSON?
2. HelloWorld 实战:五步跑通第一个 Spring Boot 项目
2.1 环境准备与项目初始化
动手之前,先把环境对齐。我本机用的是 JDK 8,Maven 3.6+,开发工具用的是 IntelliJ IDEA Community Edition。Spring Boot 版本我这里选的是 2.7 系列,因为这个版本对 JDK 8 的兼容性很稳,等会儿涉及源码解析也相对清晰。如果你是刚上手,不建议一上来就用 3.x 系列配合 JDK 21,学习阶段没必要给自己增加版本兼容的负担。
项目初始化最快的方式是在 Spring Initializr 页面生成一个压缩包导入,或者直接在 IDEA 里新建 Spring Initializr 项目。我这里为了演示底层逻辑,选择手动创建 Maven 项目,因为手动创建能让你真正理解目录结构和依赖来源。
新建一个普通的 Maven 项目,pom.xml里最关键的内容如下:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>等一下,这个spring-boot-starter-parent有什么用?它是 Spring Boot 提供的一个 Maven 父工程,里面用<dependencyManagement>统一管理了一堆常见依赖的版本号。也就是说,我们在子工程里只需要写依赖的坐标,不用写版本号,Spring Boot 会帮我们挑一个兼容的版本。这样做的好处显而易见:避免版本冲突,保证你引入的 redis、mybatis、druid 等 starter 之间的兼容性。
2.2 启动类与最小的 Web 接口
Maven 项目创建好后,在src/main/java下建一个包,比如com.demo,包名尽量别用默认的根包。在这个包下创建启动类:
package com.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }这个类就是 Spring Boot 的入口。@SpringBootApplication是一个组合注解,里面绑定了@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan三个核心注解。等会我们会专门拆解它。
现在创建一个 Controller,测试一下接口:
package com.demo.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { @GetMapping("/hello") public String hello() { return "Hello Spring Boot!"; } }直接运行DemoApplication的main方法,控制台会打印一个标志性的 Spring Boot ASCII LOGO,然后出现 Tomcat started on port(s): 8080。这时候打开浏览器访问http://localhost:8080/hello,就能看到Hello Spring Boot!。
很多人跑到这里就觉得完事了,但我会拦你一下:先别急着复制代码,停下来想想几个问题。为什么我们的类没有继承任何东西、没有实现任何接口,就能被识别成 Controller?为什么我并没有创建 Tomcat 对象,Tomcat 却启动了?为什么返回一个 String 就自动变成了 JSON 字符串响应?这些现象背后,全是自动配置在起作用。
2.3 端口与字符集的小配置
如果觉得默认的 8080 端口不顺眼,可以在resources目录下新建application.properties:
server.port=9090 server.servlet.encoding.charset=UTF-8 server.servlet.encoding.enabled=true server.servlet.encoding.force=true改完重新运行,控制台端口会变成 9090。这种简单的配置方式,也是 Spring Boot 自动配置的一部分:框架提供了ServerProperties这个属性绑定类,把配置文件中server.*的内容自动映射到对应的字段,再把这些字段的值传给内嵌容器配置。
这里的启发是:自动配置不是死板地帮你创建 Bean,它还提供了一套可覆盖的默认配置。你如果不改,就用约定好的默认值;如果你改了,就以你的配置为准。
3. 自动配置的底层原理:从注解到导入器
3.1 拆解 @SpringBootApplication
在 IDEA 里按住 Ctrl 点击@SpringBootApplication注解,会看到它的定义。我把它简化为三个核心注解:
@SpringBootConfiguration:标明当前类是配置类。本质上它包含@Configuration,因此这个类也可以被当成配置类来使用。@EnableAutoConfiguration:开启自动配置功能,这是最核心的部分。@ComponentScan:默认扫描当前类所在包及其子包下所有标有@Component、@Service、@Controller、@Repository的组件。
这里有个隐藏的坑:@ComponentScan默认扫描基包是启动类所在的包。如果 Controller 放在了com.demo.controller,而启动类放在com.demo.start或某个子包之外,扫描不到就会 404,或者干脆容器里没有这个 Controller。我见过很多新人把启动类直接放在java根目录下,然后这个包不包含任何代码,所有业务自己建了一个com.xxx包,结果访问接口时找不到 Bean。所以最简单也最安全的做法就是:让启动类位于所有代码所在包的顶层。
3.2 @EnableAutoConfiguration 是怎么“开后门”的
@EnableAutoConfiguration的关键机制是导入一个决策器。你可以把它想象成开着门的所有装饰公司都来报名,但这个门不是全部放进来,而是由一个包工头挨个审核,审核通过的才能进场干活。
我们看注解定义:
@Import(AutoConfigurationImportSelector.class) public @interface EnableAutoConfiguration { ... }AutoConfigurationImportSelector实现了ImportSelector接口。Spring 启动时,在处理配置类过程中会调用它的selectImports方法,让它决定要额外导入哪些配置类。这个方法的返回值是一个字符串数组,每个字符串都是一个配置类的全限定名。
关键是这个数组怎么来的。它会去读取META-INF/spring/spring.factories文件(Spring Boot 2.7 及以前)或者META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件(Spring Boot 3.x 及以后)。这些文件由 Spring Boot 的各个自动配置模块提供,里面列出了所有候选的自动配置类。
我刚学的时候特别容易混淆一件事:以为spring.factories里所有的自动配置类都会被加载。其实不是。spring.factories里的类是“候选名单”,最终要不要加载,取决于一个叫过滤器的组件。
3.3 条件注解:自动配置的“准入证”
Spring Boot 的自动配置为什么不会一锅乱炖?靠的是 Spring 的@ConditionalXxx条件注解。这些注解和名字一样,就是给自动配置类加了一道校验:条件满足才加载,条件不满足就跳过。
常见的条件注解有:
@ConditionalOnClass:满足“当前 classpath 上有指定类”的条件才生效。@ConditionalOnMissingBean:当前容器中没有指定类型的 Bean 时才生效。@ConditionalOnProperty:配置文件中存在指定配置项、且值匹配时才生效。@ConditionalOnWebApplication:当前是一个 Web 应用时才生效。
举一个让我恍然大悟的例子。Spring Boot 的HttpEncodingAutoConfiguration类头上长这样(简化版):
@Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(ServerProperties.class) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) @ConditionalOnClass(CharacterEncodingFilter.class) @ConditionalOnProperty(prefix = "server.servlet.encoding", value = "enabled", matchIfMissing = true) public class HttpEncodingAutoConfiguration { ... }当你引入spring-boot-starter-web之后,classpath 里自然有CharacterEncodingFilter,此时@ConditionalOnClass(CharacterEncodingFilter.class)满足。项目是 Web 项目,@ConditionalOnWebApplication满足。配置文件里虽然没有设置server.servlet.encoding.enabled,但matchIfMissing = true表示“没写也算你满足条件”。三道关卡全过,这个配置类就被加载,然后它就会创建一个CharacterEncodingFilter的 Bean,用于设置请求和响应的字符集。
所以自动配置的本质是一堆“带着准入条件的配置类”。它们不会在你没引入相关依赖时强行生效,也不会在没有 Web 环境时给你创建 Servlet 组件。条件注解就是 Spring Boot 应对复杂场景的过滤器。
3.4 从 Spring Boot 2.7 到 3.x 的变化
如果你手头用的 Spring Boot 3.x,会发现自动配置的候选配置类不再写在spring.factories里,而是写在META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中。这个变化主要是为了减少对 Spring 旧机制的依赖,也方便工具进行静态分析。原则上不影响我们理解整体流程:读取候选名单、通过条件筛选、注册配置类,这条主线没有变。
我实际用 3.x 时的体会是,官网文档结构更加清晰,但一些老代码里的排除自动配置方式也跟着变了。比如 2.7 里常用的@SpringBootApplication(exclude = RedisAutoConfiguration.class)依然可用,排除的原理一样。如果你在 3.x 里手动写spring.factories去加载自己的自动配置,可能会发现失效,原因就是 3.x 的加载器改了路径。
4. 一个自动配置案例的完整还原
4.1 跟着源码看 DispatcherServlet 是怎么被创建的
理论讲了半天,很多朋友还是觉得虚。那我们现在盯住一个最典型的场景:我什么都没做,Spring MVC 的DispatcherServlet是怎么被 Spring Boot 创建出来的?
打开 Spring Boot 的自动配置包里,找到WebMvcAutoConfiguration。这个类的声明大概像我下面这样(已省略大量细节):
@Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class }) @ConditionalOnMissingBean(WebMvcConfigurationSupport.class) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10) @AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class }) public class WebMvcAutoConfiguration { ... }你看,它带着三四个条件:必须是 Servlet 类型的 Web 应用;classpath 里必须有 Servlet、DispatcherServlet 等类;容器里不能有WebMvcConfigurationSupport这个 Bean。这三个条件在普通的 Web 项目中都满足,因此WebMvcAutoConfiguration生效。
它生效后会做什么?它会往容器里注册视图解析器、类型转换器、静态资源映射处理器,还有最重要的DispatcherServlet相关的DispatcherServletRegistrationBean。这个 RegistrationBean 会把 DispatcherServlet 注册到内嵌的 Tomcat 容器上。
这一连串动作的逻辑我并不建议死记硬背。你只需要记住一条路径:引入 web starter → classpath 出现 Servlet 相关类 → 自动配置类满足条件 → 创建 Spring MVC 核心组件。
4.2 为什么只写了一行返回字符串,结果却变成了 JSON
这是很多新手会好奇的:@RestController返回一个 String,按理说 Controller 直接返回字符串给响应不就行了,为什么还要转 JSON?实际上这里的细节很微妙。
@RestController相当于@Controller+@ResponseBody,它的返回值会经过 HttpMessageConverter 转成响应体。Spring Boot 的JacksonAutoConfiguration会在容器里创建MappingJackson2HttpMessageConverter,正是因为容器里存在这个转换器,当返回值是对象时,它会用 Jackson 转成 JSON;当返回值是 String 时,会直接当作文本。这个自动配置生效的条件,是 classpath 里存在 Jackson 的类。而spring-boot-starter-web默认带了 Jackson,所以你的接口自动就支持 JSON 了。
有次我在一个项目里把 Jackson 从依赖里误删了,接口直接报No converter found for return value of type之类的错误。当时没反应过来是自动配置的功劳,后来排查半天才发现是缺了 Jackson。从此我就明白了:自动配置是“看菜下饭”,菜里有 Jackson,它才做 Jackson 的菜;菜里没有,它就跳过这道菜。
4.3 想自己写一个自动配置模块吗
如果以后你想做一个公共组件给别人用,比如封装一个自己的短信发送 starter,那就需要学会自己写自动配置。思路并不复杂:
- 新建一个 Maven 工程,打包成 jar。
- 在
src/main/resources/META-INF下创建spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件。 - 文件里写入你的自动配置类全限定名,比如
com.demo.autoconfigure.SmsAutoConfiguration。 - 在自动配置类里使用
@ConditionalOnClass、@ConditionalOnProperty等注解控制生效条件。 - 其它项目引入这个 jar 后,Spring Boot 启动时会自动读取这个文件并尝试装配。
注意如果你是 Spring Boot 2.7 及以前,文件路径和格式不同,是写在spring.factories里的,key 为org.springframework.boot.autoconfigure.EnableAutoConfiguration。这是很多巧妙的中间件扩展点的经典姿势,理解了它,以后看任何框架型 starter 的源码都不会一头雾水。
5. 自动配置的调试与常见问题
5.1 怎样知道哪些自动配置最终生效了
碰到“为什么这个功能没生效”的问题,先别瞎猜。Spring Boot 给了两个非常有用的调试开关。
第一个是在application.properties里开 debug:
debug=true启动时控制台会打印自动配置报告,其中有两张表:Positive matches列出所有被自动配置类匹配上的条件,Negative matches列出所有没有匹配上的条件以及原因。比如你看Negative matches里写着RabbitAutoConfiguration没有匹配,因为@ConditionalOnClassRabbitTemplate没满足——这说明 classpath 里没有 RabbitMQ 的依赖。
第二个是启动时加--debug参数,效果类似。我排查自动配置问题的时候,几乎都会先看一眼报告,定位具体是哪个条件失败,比盲目百度高效得多。
5.2 启动报错 “Failed to configure a DataSource”
这大概是 Spring Boot 入门阶段最常见的一个异常。报错信息大意是:
Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured. Reason: Failed to determine a suitable driver class产生这个错误的最常见原因是:你引入了spring-boot-starter-data-jpa或mybatis-spring-boot-starter这类和数据源相关的依赖,但配置文件中没有写数据源 URL。自动配置里的DataSourceAutoConfiguration一看环境里没有可用的 DataSource,而 classpath 里又存在 DataSource 相关的类,于是尝试配置,结果发现 URL 是空的,直接启动失败。
解决方法也很简单:要么规范配置数据源,比如:
spring.datasource.url=jdbc:mysql://localhost:3306/demo spring.datasource.username=root spring.datasource.password=123456 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver要么明确告诉 Spring Boot 当前阶段不需要数据库,在启动类上排除自动配置:
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})如果你是先在无数据库环境下学习某个 Web 功能,临时排除数据源自动配置是完全可以的。但项目里真的要用数据库时,一定要配置完整的数据源,别把exclude留着当永久解药。
5.3 条件判断失效的诡异场景
还有一种很隐蔽的情况,我把以前踩过的坑分享给你。有次我封装一个组件,用了@ConditionalOnClass判断某个业务类是否存在,结果怎么调试都发现这个条件不成立。后来打开依赖树一看,这个业务类所在的 jar 是provided范围,编译期有,运行期没有,classpath 里自然就找不到。所以使用@ConditionalOnClass时要想清楚:判断的是运行期的 classpath,不是编译期。
另外一个常见误区是@ConditionalOnMissingBean的使用。很多新手想用它在配置类里创建默认 Bean,但忘记考虑 Bean 定义顺序,导致预期应该跳过却没有跳过。实际上@ConditionalOnMissingBean的判断时机是在容器解析 Bean 定义时,如果某个 Bean 是通过其它配置类创建的,而那个配置类的解析顺序在你这个配置类之后,那么你仍然会创建一个多余的 Bean。这也是为什么官方文档会提醒,自动配置类尽量用@AutoConfigureBefore或@AutoConfigureOrder控制加载顺序。你如果自己写组件,对这些顺序问题一定要敏感。
5.4 依赖版本冲突排查
Spring Boot 的依赖管理虽然已经做了大量平衡,但如果你额外引入第三方依赖时没有使用 starter,还是可能出现版本冲突。最典型的例子是引入gson2.9 和jackson同时存在于 classpath,某些库会优先用其中一个,结果序列化行为和你预期不一致。
排查依赖冲突,我通常用 Maven 的依赖树命令:
mvn dependency:tree然后 grep 关键字,比如:
mvn dependency:tree -Dincludes=com.fasterxml.jackson.core出现版本冲突时,Maven 会采取“最近定义优先”的原则,必要时可以用<exclusions>排除传递依赖。不过在学习阶段,我建议先坚持使用 Spring Boot 官方维护的 starter,减少踩坑概率。
6. 实操心得与后续扩展
6.1 我看自动配置的三层视角
第一层:使用者视角。只按文档引入 starter、写配置、用模板,遇到问题搜一搜,多半能解决。这个阶段最重要的不是背原理,而是多跑几个小项目,建立对 Spring Boot 常见自动配置范围的直觉。
第二层:排查者视角。当你遇到“为什么没生效”的时候,看 debug 报告,去匹配条件,看类路径,最快定位问题。这时你会发现自动配置不是黑魔法,而是有迹可循的规则。
第三层:设计者视角。当你自己去设计一个通用的 starter,或者想改造某个第三方的 starter 时,需要非常清楚条件注解、配置绑定、自动配置加载顺序的细节。这时候钱已经不在框架本身,而在你对 Spring 生态扩展机制的理解有多深。
从我个人的经验看,不少同事停留在第一层,遇到自动配置相关的疑难杂症就绕路。其实只要花一个下午把AutoConfigurationImportSelector和几个核心条件注解过一遍,很多问题都能自己解决,那种“原来如此”的顿悟过程,也是学习编程最有成就感的时刻之一。
6.2 下一步可以玩点更有意思的
HelloWorld 只是起跑线,跑通之后我建议你按这几个方向继续验证自动配置:
- 引入 MyBatis 或 Spring Data JPA,写一个最简单的数据库读写接口,感受数据层自动配置的覆盖范围。
- 写一个自己的 Redis 连接测试,观察
RedisAutoConfiguration在 debug 报告里的匹配情况。 - 学会使用
application.yml的绑定,自己定义一组myapp.*配置,用@ConfigurationProperties注入到 Bean 中。这不算难,但能让你体会“约定大于配置”的设计哲学。 - 试着排除一两个自动配置类,看看启动后的容器少了什么 Bean,接口会出什么错,再反向思考这些 Bean 平时对我们的保护有多深。
6.3 最后一个小建议
别把 Spring Boot 神话化。自动配置本质上还是 Spring 的 IoC 容器在干活,只是换了一种更聪明的装配方式。你在学 Spring 时建立的 Bean 生命周期、后置处理器、AOP 这些概念,在 Spring Boot 里依然适用。自动配置只是提前帮你想好了大部分场景的装配方案,但遇到特殊需求,你随时可以写自己的@Configuration来覆盖默认装配。想形成这个能力,没有捷径,就是多看源码路径,多写 demo,多开 debug 报告。希望这篇日记能帮你在 Spring Boot 的入门路上省下一点时间,少走一点我当初走过的弯路。