第一个 Spring Boot 应用:三分钟跑通 Hello World
上一模块你用纯 Spring 搭过一个 Web 应用:web.xml、applicationContext.xml、外部 Tomcat,一个都不能少。Spring Boot 做的事只有一件——把这些「为了让框架跑起来」的仪式全部搬进 jar 包里预先写好。你只需要 new 一个项目、加一个依赖、写一个带 main 方法的类,剩下的它全包了。所以这一篇的重点不是「Boot 有多少功能」,而是第一次运行时,那行 SpringApplication.run(...) 到底替你干了哪八件事,以及你怎么确认它真的干成了。
纯 Spring 像毛坯房——水电要自己排、墙要自己刷、门禁要自己装,住进去之前你得先当三个月装修工。Spring Boot 是精装房:水电网线、热水器、门锁全都配好了,你只带一个行李箱(你的业务代码)就能入住。而且它很懂分寸:你自己买了沙发摆进客厅,它就不会再往那儿放它的沙发。这篇末尾我们会把这句话拆开验证。

学完这一篇,你应该能回答三个问题:
- 为什么 Boot 项目里没有
web.xml?内嵌 Tomcat 究竟从哪个依赖里冒出来的? SpringApplication.run()那一行背后依次发生哪八步?我在启动日志的哪一行能确认「现在可以访问了」?mvn package产出的 fat jar 为什么单靠java -jar就能跑?目标机器需要预装什么?
在写第一行 Boot 代码之前,先回头看上一模块留下的「配置负担清单」。用纯 Spring 起一个最普通的 Web 应用,你要准备这些东西:
- 一个
web.xml,注册DispatcherServlet并配置它的init-param - 一个
applicationContext.xml,声明组件扫描、数据源、事务管理器 - 一个
spring-mvc.xml,注册视图解析器、消息转换器 - 把上面的 war 包丢进一个外部的 Tomcat,装上、启动、看日志
这四件事里,没有一件是你的业务。 它们全是「让框架跑起来」的仪式。Spring Boot 解决的就是这层仪式——它的官方说明只有一句话:约定优于配置(Convention over Configuration)。落到实际,就是三件事:
| 纯 Spring 的负担 | Spring Boot 的答案 |
|---|---|
| 手工管理几十个依赖及其版本 | starter 一站式依赖,版本由父 POM 统一裁决 |
| 手写 xml / Java 配置类 | 自动配置:按 classpath 和已有 Bean 条件式装配 |
| 部署到外部 Tomcat | 内嵌 Tomcat,java -jar 直接跑 |

官方脚手架 start.spring.io 是上手的标准入口(IDEA 里 New Project → Spring Initializr 走的是同一个服务)。填选项时对着下表照抄即可:
| 选项 | 选择 | 说明 |
|---|---|---|
| Project | Maven | Gradle 亦可,本文用 Maven |
| Language | Java | — |
| Spring Boot | 3.2.x | 与 JDK 17 及以上匹配 |
| Group | com.example | 包名前缀 |
| Artifact | demo | 项目名 / 构件名 |
| Packaging | Jar | 不再需要 war |
| Java | 17 | 与上一模块的结论一致 |
| Dependencies | Spring Web | 只要这一个,够跑 Hello World |
点击 Generate 下载 zip,解压后用 IDEA 打开即可。注意 Artifact 会决定启动类的名字:填 demo,得到的就是 DemoApplication。
Dependencies 里只加 Spring Web 是刻意的。Boot 的依赖是「按需加载」的,加得越多、自动配置装配得越多、启动越慢。第一次上手时保持最小依赖,才能把「加一个 starter 带来什么」观察清楚。
Initializr 上那一排复选框,本质就是往 <dependencies> 里塞条目。下面这台生成器和课文用的是同一套拼接规则:勾一项,pom 立刻变;产物下面会写清这一行为什么存在、去掉会怎样。
练习路径:只勾 Spring Web → 看 <parent> 之外为什么一个版本号都没有;再加 Data Jpa 和 MySQL 驱动 → 数一数多了哪三个条目、为什么驱动的 <scope> 是 runtime;最后加 Lombok → 看它的 scope 为什么是 provided。
<?xml version="1.0" encoding="UTF-8"?>
<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.3.4</version> <!-- 版本由 BOM 统管,子依赖不写 version -->
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>demo-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</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>打开项目,目录长这样(这是 Boot 的标准骨架):
demo/├── pom.xml # 依赖与构建配置├── src/│ ├── main/│ │ ├── java/com/example/demo/│ │ │ └── DemoApplication.java # 启动类:唯一必需的类│ │ └── resources/│ │ ├── application.properties # 全局配置(默认几乎是空的)│ │ ├── static/ # 静态资源(css / js / 图片)│ │ └── templates/ # 模板文件(Thymeleaf 等)│ └── test/java/com/example/demo/│ └── DemoApplicationTests.java # 自动生成的测试类└── mvnw / mvnw.cmd # Maven Wrapper,无需预装 Maven先看 pom.xml 里最关键的两块——parent 和 starter:
<!-- 1. parent:继承 Spring Boot 的默认配置,版本由此统一裁决 --><parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.5</version> <relativePath/></parent><dependencies> <!-- 2. starter:一个依赖,开启一整族 Web 能力 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency></dependencies>spring-boot-starter-parent是依赖版本的唯一事实源:你在下面加依赖时不用写<version>,它替你锁定一整套兼容版本spring-boot-starter-web是 starter 的典型代表:它本身几乎不含代码,作用是把「Web 开发所需的一族依赖」一次性拉进来application.properties默认几乎是空的,因为默认值全在自动配置里,只有你要「偏离默认」时才需要写
说明:那个 mvnw(Maven Wrapper)是个贴心设计——它会自动下载合适版本的 Maven,所以队友和你自己的机器就算没装 Maven 也能构建,团队环境从此不会再出现「我这能跑你那不能」的版本差异。
整个项目里,只有启动类是「必须由你写」的。它短得离谱,但每一行都值得讲:
package com.example.demo;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublic class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); }}@SpringBootApplication 是个「三合一」注解,拆开看就是三件事:
| 组成注解 | 作用 |
|---|---|
@SpringBootConfiguration | 本质是 @Configuration,声明这是一个配置类 |
@EnableAutoConfiguration | 开启自动配置,让 Boot 按条件装配 Bean |
@ComponentScan | 扫描当前包及其子包下的 @Component / @Service / @Controller |
SpringApplication.run(DemoApplication.class, args)的返回值是一个ApplicationContext,也就是 IoC 容器本身。你完全可以接住它,比如ConfigurableApplicationContext ctx = SpringApplication.run(...),然后ctx.getBean(...)取 Bean 来验证装配结果main方法里只写了这一行,但真正发生的事远不止一行:创建容器、加载自动配置、启动内嵌 Tomcat、发布就绪事件,全是这一行的连锁反应
@ComponentScan 默认只扫「启动类所在包及其子包」。所以启动类要放在最外层的包(如 com.example.demo),业务代码放子包(com.example.demo.controller)。如果把启动类塞进 controller 子包,同级或外层的 Bean 就扫不到了——这是新手第一周必踩的坑。
在 com.example.demo.controller 包下新建一个类:
package com.example.demo.controller;import org.springframework.web.bind.annotation.*;@RestControllerpublic class HelloController { @GetMapping("/hello") public String hello() { return "Hello, Spring Boot!"; } // 带路径变量:/hello/张三 会返回 Hello, 张三! @GetMapping("/hello/{name}") public String helloTo(@PathVariable String name) { return "Hello, " + name + "!"; }}@RestController=@Controller+@ResponseBody:方法返回的字符串直接写进响应体,不再找视图@GetMapping("/hello")把这个方法映射到GET /hello@PathVariable把 URL 里{name}这一段绑定到形参
@RestController 和 @Controller 的区别值得单独记一句:要返回视图(HTML 页面)用 @Controller,要返回 JSON 或纯文本用 @RestController。混用会让「明明返回了对象,浏览器却报 404 找不到页面」。
这个区别不是背出来的,是按出来的。下面这个内核实验把四种情形各跑一遍:view 看 @Controller 返回 hello 时 Spring 怎样把它当视图名去找模板、找不到就给你一张白标页;json 看同一个方法换成 @RestController 之后写进响应体的是什么;missing 看那张 404 白页到底是哪个环节判的;string 看 return "ok" 在两种注解下分别被解释成什么——这也是新手「接口明明返回了字符串却 404」的真正答案。
运行 DemoApplication,控制台会吐出这样一段(已按真实格式简化):
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v3.2.5)2024-05-20T10:00:00.123 INFO --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)2024-05-20T10:00:01.456 INFO --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''2024-05-20T10:00:01.789 INFO --- [main] com.example.demo.DemoApplication : Started DemoApplication in 1.234 seconds (process running for 2.101)- 那块 ASCII 图案是 banner,可以通过
resources/banner.txt自定义,或用spring.main.banner-mode=off关掉 Tomcat initialized with port(s): 8080说明内嵌 Tomcat 已经绑定端口Started DemoApplication in 1.234 seconds是「就绪」的信号——看到这一行,浏览器才可能访问成功
内嵌 Tomcat 到底从哪来? 它藏在 spring-boot-starter-web 的传递依赖里。执行 mvn dependency:tree 能看到这棵树:
spring-boot-starter-web ├── spring-boot-starter │ ├── spring-boot │ ├── spring-boot-autoconfigure │ └── spring-boot-starter-logging ├── spring-boot-starter-json ├── spring-boot-starter-tomcat <-- 内嵌 Tomcat 从这里来 │ ├── tomcat-embed-core │ ├── tomcat-embed-el │ └── tomcat-embed-websocket └── spring-webmvc └── spring-web这就解释了为什么不用 web.xml:外部 Tomcat 场景下,是 Servlet 容器先启动、再加载你的 war 并读取 web.xml;而 Boot 把顺序倒了过来——是你的 main 先启动,亲手把 Tomcat 拉起来(TomcatServletWebServerFactory 在自动配置里完成),Servlet 的注册也因此变成了 Java 代码,不再需要一份 xml 描述文件。

「勾一个框 → 8080 能访问」中间那段路,被这张图压成了两列。要看清它是怎么一格接一格传下去的,点下面这条路径——第 ③ 格是整条链的咽喉,条件不成立,后面四格根本不会发生:
同一个启动类,有三种跑法,各自适用不同场景:
| 方式 | 命令 / 操作 | 适用场景 |
|---|---|---|
| IDEA 直接运行 | 点 main 旁的绿色三角 | 日常开发,断点调试最方便 |
| Maven 插件 | mvn spring-boot:run | 命令行开发,或 IDE 不好用的环境 |
| 可执行 jar | mvn package 后 java -jar target/demo-0.0.1-SNAPSHOT.jar | 部署、CI 验证、贴近生产 |
先在项目根目录执行打包:
# 跳过测试先跑通,测试类会在下一模块单独讲mvn clean package -DskipTests# 产物是一个「可执行 jar」(fat jar),内嵌了 Tomcat 和全部依赖java -jar target/demo-0.0.1-SNAPSHOT.jar- Boot 的
spring-boot-maven-plugin会把项目打成 fat jar:一个 jar 里塞进了应用代码、第三方依赖和内嵌 Tomcat,所以java -jar就能独立运行 - 这个 jar 是后面「Docker 部署」「CI/CD」两节的基础——部署到服务器时,目标机只需要一个 JRE,不需要预装 Tomcat
提示:-DskipTests 只是先跳过,不能当长期习惯。打包前的测试是最后一道防线,等下一模块讲完 JUnit 5 就把这一项去掉。
| 现象 | 根因 | 排查 / 解决 |
|---|---|---|
Port 8080 was already in use | 8080 被其他进程占用 | 改 server.port=8081,或杀掉占用进程 |
| 浏览器访问 404 | 路径拼错 / 类没在扫描范围 / 忘了 @RestController | 依次核对:URL、所在包是否在启动类子包内、用的是不是 @RestController |
| 改了代码访问结果没变 | 用的旧进程 / 没重新编译 | 停掉旧进程重跑;确认为何「热部署」没生效 |
404 的排查一定要有顺序,别乱试。第一步看 URL 是否和方法映射完全一致(含大小写、前缀);第二步看 Controller 所在的包是不是启动类的子包——@ComponentScan 只扫子包;第三步看注解是不是 @RestController(用 @Controller 而没配视图,就会 404)。按这三步,绝大多数 404 都能定位。
这三步之所以要按顺序,是因为它们各自排掉一整类原因:URL 不对是「你人走错了门」,包不在子树里是「容器根本不认识这个类」,注解用错是「返回值被当成视图名扔掉了」。这条动画把六帧走完,你会得到肌肉记忆而不是口诀:

Boot 的失败分析器(FailureAnalyzer)会把根因排成一张居中的横幅。新手在这里的典型动作是从上往下逐行读完,或者一慌去翻下面几百行堆栈——两种都低效。这张横幅里只有三行有信息量,你来点出最关键的那一行:
你照课文写完 HelloController,命令行执行 ./mvnw spring-boot:run。日志滚了十几秒,突然在中间打出这张横幅。
Boot 默认不会在你改代码后自动重启,因为运行中的 JVM 已经把类加载进内存了。想要「改完立刻生效」,需要加 spring-boot-devtools:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> <!-- 关键:optional,避免传递到下游依赖 --></dependency>「改了没生效」这件事读十遍解释都不如走一遍。下面这台单步台把六行摊开,右边同步刷新「此刻内存里有几个 HelloController 的 Class 对象」和「谁在加载它」——重点盯第 3 步与第 6 步,那两个数字一个不变、一个归零:
// 你在 IDEA 里把 hello() 的返回值改成新的文案,按下 Ctrl+S// 运行中的 JVM:方法区里已经躺着 HelloController 这个 Class 对象// javac 只重写了磁盘上的 target/classes/HelloController.class// 老的那个 Class 不会回头去读磁盘// 浏览器刷新 → 容器里的同一个 Bean 实例 → 同一个 Class → 旧文案// devtools 的做法:扔掉 RestartClassLoader,用新的加载器把磁盘上的字节码再读一遍| 磁盘上 | 源码已更新 |
| 内存里 | 还是旧字节码 |
| 进程 | 同一个,没重启 |
IDEA → Save → javac下面这个演示把 Boot 启动时的 Bean 装配过程做成了可交互的。切换条件开关,观察「装配的连锁反应」——哪一个 Bean 因为条件不满足而被跳过:
上面这个实验看的是「条件不满足时 Bean 怎么被跳过」。再往回退一步,有个更基本的问题:你写的那个 HelloController 到底是怎么进容器的?beanin 场景把四条路各演一遍——组件扫描(@Component 系)、@Bean 方法、@Import 直送、自动配置替你注册。最后一档「扫不到的现场」正是第十五节沙盘和第十六节速查表里那条 required a bean that could not be found 的由来:包不在启动类的子树里,扫描这一步压根没看见它。

上面那个沙盘把条件开关做成了三个按钮。下面换成命令行——你敲,浏览器里那份 Java 内核真跑真答:beans 列出的是容器此刻的真实状态,cond 改完之后的装配日志是重新算出来的,curl 打到的是内核内存里的虚拟 8080。
按这个顺序走一遍,你会亲眼看到「条件不满足」是怎么一层层传染的:
boot—— 建容器,看装配日志与条件求值记录beans—— 五个 Bean 全在,状态都是「已创建」cond jdbcOnClasspath false—— 假装 JDBC 不在类路径上beans—— 再问一次:这次已跳过和创建失败分别落在谁头上?为什么?curl /api/kernel/query—— 请求打到 Repository,看它回你什么cond jdbcOnClasspath true把开关恢复,再beans确认一切回来了
lab <场景> <参数> 是控制台通向全部内核实验的后门——课文里每个按钮,你在这里都能自己敲出来,比如 lab proxy jdk、lab refresh fail、lab tx requires_new。忘了有哪些场景就敲 help,再回看第十一到十四篇。
前面那张时间轴是「应该发生什么」。下面这个内核实验把它真的跑一遍——bootrun 场景模拟的是 SpringApplication.run() 的完整流程,四个按钮各看一条支线:
- 八步主线(full):按顺序走完准备启动器 → Banner → 建容器 → 准备环境 → 后置处理 →
refresh()→ 起 Tomcat → 调用 Runner,每一步都会打出该步产出的东西 - 事件广播(event):
ApplicationStartingEvent→EnvironmentPreparedEvent→ApplicationPreparedEvent→ApplicationStartedEvent→ApplicationReadyEvent。日志里那句Started DemoApplication in 1.234 seconds就是ApplicationStartedEvent的产物,这也是你在代码里挂「启动后做一次预热」的正确时机 - Runner 执行(runners):
ApplicationRunner与CommandLineRunner都在容器就绪之后、ApplicationReadyEvent之前执行;前者参数被包装成ApplicationArguments(能区分选项参数和非选项参数),后者拿到裸String[] - 启动失败怎么读(fail):Boot 会把异常整理成一段带边框的报告,先给
APPLICATION FAILED TO START,再分Description:/Action:两块。这一档一定要点开看,它直接对应第十六节的速查表
顺手确认一件事:run() 是有返回值的。上一模块你学过 refresh() 十二步,其实 Boot 就是把那十二步塞进这里的第 6 步。想亲眼看到容器,把 main 改三行就够:
public static void main(String[] args) { ConfigurableApplicationContext ctx = SpringApplication.run(DemoApplication.class, args); // 容器里有几个 Bean?取一个出来看看是不是你想要的类型 System.out.println("beanDefinitionCount = " + ctx.getBeanDefinitionCount()); Object hello = ctx.getBean("helloController"); System.out.println("helloController -> " + hello.getClass().getName());}第一次跑这行打印,你会得到一个比预期大得多的数字——因为自动配置已经悄悄注册了几十个 Bean。这正是下一篇《@SpringBootApplication 三合一》和第三篇《自动配置原理》要解释的现象。
第八节说 mvn package 产出「内嵌一切的 fat jar」。它凭什么能被 java -jar 直接跑?普通 jar 的 META-INF/MANIFEST.MF 里没有 Main-Class,JVM 不知道该从哪个类开始执行;而且第三方依赖也不可能被系统类加载器看见——它们被压在 jar 内部,不是散在磁盘上的目录。Boot 用两个动作解决这两件事:
target/demo-0.0.1-SNAPSHOT.jar├── META-INF/│ ├── MANIFEST.MF # Main-Class: org.springframework.boot.loader.launch.JarLauncher│ └── ... # Start-Class: com.example.demo.DemoApplication├── BOOT-INF/│ ├── classes/ # 你自己的 class 与 application.properties│ └── lib/ # 全部第三方依赖,以「嵌套 jar」原样塞进来│ ├── spring-boot-starter-web-3.2.5.jar│ ├── tomcat-embed-core-10.1.x.jar│ └── ...└── org/springframework/boot/loader/launch/ # 启动器自己(JarLauncher 等)JarLauncher是真正的入口。它新建一个LaunchedClassLoader,把BOOT-INF/classes/和BOOT-INF/lib/*.jar都交给这个加载器管——这就是 JDK 标准 classpath 规则做不到的「jar 套 jar」(类加载器的概念在第 1 篇讲过)Start-Class才是你写的那个DemoApplication。JarLauncher用反射调用它的main,于是控制权交回给你,第八步时间轴继续往下走- 拆包验证一下:
jar tf target/demo-0.0.1-SNAPSHOT.jar | more,能看到上面三层目录
这五样东西各自管什么,光看目录树是记不住的——来配一遍。左边是 jar 里的位置或清单属性,右边点它的职责,配错了当场告诉你为什么:
fatjar 场景把这四段各自演一遍:layout 看目录结构与 MANIFEST.MF 的两行关键属性;loader 看 LaunchedClassLoader 的查找顺序(父委派 → BOOT-INF/classes → BOOT-INF/lib);run 看从 java -jar 到你 main 的第一行之间的完整启动序列;war 看换成 war 部署时的差异——多出一个 WEB-INF/lib-provided,devtools 和外置容器用的 Tomcat 会被丢进那里,从而在外置 Tomcat 里不会和你的容器版本冲突。

目标机器只需要一个 JRE,不需要预装 Tomcat——这句话的技术根据就是上面这段:Tomcat 的 class 就在 BOOT-INF/lib 里,由 LaunchedClassLoader 自己负责加载。
第一次跑 Boot,八成概率会栽在这两件事上:端口撞车,或者包放错层导致 404。左边改一行配置,右边立刻给出启动日志的那几行关键输出:
Tomcat started on port 8080 (http) with context path ''GET /hello -> 200 "Hello, Spring Boot!"#startedAt=1.2s beanDefinitionCount 约 60+
沙盘第二列的三个包位置,正好对应三种结果——放根包一切正常、放子包 Bean 集体消失、放父包能用但扫描面失控。这三格请务必亲手点一遍,比读十遍文字管用。
下面的「报错原文」都可以整段粘进搜索框,别缩写、别意译:
| 报错原文(片段) | 真实原因 | 30 秒自救 | 深挖看第几篇 | |
|---|---|---|---|---|
Web server failed to start. Port 8080 was already in use. / java.net.BindException: Address already in use: bind | 上一次运行没停干净,或别的程序占了 8080 | 改 server.port=8081;Windows 用 `netstat -ano \ | findstr :8080 找到 PID 再 taskkill /PID <pid> /F,macOS/Linux 用 lsof -i:8080` | 本篇第十五节沙盘 |
APPLICATION FAILED TO START + Description: Field xxx in com.example.demo.web.UserController required a bean of type '...' that could not be found. + Action: Consider a component scan capable of placing your SpringBootApplication... | @ComponentScan 只扫「启动类所在包及子包」,那个 Bean 在兄弟包或父包里 | 先读 Description 里的完整类名,对比启动类的包名;把启动类提到根包,或用 scanBasePackages 显式列出 | 第 17 篇第四节 · 本篇第十八节第一档 | |
Consider defining a bean of type 'org.springframework.jdbc.datasource.DataSource' in your configuration | 引了 starter 却没配连接信息,或压根没加 spring-boot-starter-jdbc 这类依赖 | 加依赖;或补 spring.datasource.url/username/password | 第 18 篇第七节(--debug 看 Negative matches) | |
Caused by: java.lang.ClassNotFoundException: com.fasterxml.jackson.databind.ObjectMapper / 运行时 NoClassDefFoundError | 代码 import 了某个类,但对应的 jar 不在 classpath 上 | 看报错第一个类名属于哪个 artifact,把它加进 pom;mvn dependency:tree 核对 | 第 2 篇 Maven 依赖 · 第 18 篇 | |
The following 1 problem(s) were found while reading the POM: ... Some problems may be related to the parent project 'spring-boot-starter-parent' / spring-boot-starter-parent version not found | parent 的版本号写错、仓库里不存在,或公司私服没有该版本 | 打开 pom.xml 顶部核对 <version> 是否为真实存在的 Boot 版本;首次构建需联网下载 | 第 2 篇 · 本篇第三节 | |
UnsupportedClassVersionError: ... has been compiled by a more recent version of the Java Runtime (class file version 61.0), this version ... only recognizes ... 52.0 | 用 JDK 8 去跑 Boot 3 需要的 JDK 17 字节码 | java -version 与 IDEA 的 Project SDK 统一到 17+ | 第 1 篇第六节 | |
no main manifest attribute, in target/demo-0.0.1-SNAPSHOT.jar | 这个 jar 没被 spring-boot-maven-plugin 重打包过,不是可执行 jar | pom 里补 spring-boot-maven-plugin 的 build 插件声明,重新 mvn package | 本篇第十四节 fatjar 实验 |
Boot 3 的启动失败报告一定会分成 Description: 和 Action: 两段。先读 Action,再读 Description——Action 是官方给的修法方向(例如「考虑调整组件扫描」「检查某个属性」),Description 里则带着出问题的完整类名和属性名,两者合起来通常就能定位。千万别只看第一行的 APPLICATION FAILED TO START 就去猜。
目标:从零跑通一个 Hello World,并亲眼确认它做了什么。三步:生成、跑、验。
第一步,pom.xml 全文(可以直接覆盖 Initializr 生成的那份来对照):
<?xml version="1.0" encoding="UTF-8"?><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> <!-- 版本的唯一事实源:下面所有 starter 都不写 <version> --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.5</version> <relativePath/> </parent> <groupId>com.example</groupId> <artifactId>demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>demo</name> <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/com/example/demo/DemoApplication.java):
package com.example.demo;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RestController;@SpringBootApplication@RestController // 初学图省事,可以把 Controller 写在同一个文件里public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } @GetMapping("/hello") public String hello() { return "Hello, Spring Boot!"; } @GetMapping("/hello/{name}") public String helloTo(@PathVariable String name) { return "Hello, " + name + "!"; }}第三步,运行 mvn spring-boot:run,预期启动日志长这样(时间戳和版本号会随你的机器变):
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v3.2.5)2026-xx-xxT10:00:00.123 INFO 12345 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)2026-xx-xxT10:00:00.456 INFO 12345 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path ''2026-xx-xxT10:00:00.789 INFO 12345 --- [ main] com.example.demo.DemoApplication : Started DemoApplication in 1.234 seconds (process running for 1.567)再开一个终端验收:
curl http://localhost:8080/hello # 期望:Hello, Spring Boot!curl http://localhost:8080/hello/张三 # 期望:Hello, 张三!验收清单:① 日志里能指出哪一行代表「端口已绑定」、哪一行代表「可以访问了」;② 两条 curl 都返回 200;③ mvn package 后 jar tf target/demo-0.0.1-SNAPSHOT.jar 能看到 BOOT-INF/lib/ 与 META-INF/MANIFEST.MF。
每个变体只动一处,做完请用自己的话说出观察结论:
- 端口三连改:依次试
server.port=8081、server.port=0、server.port=abc。你会观察到 前两行换端口成功、第三行直接启动失败并报Failed to bind properties under 'server.port' to int。这一条做完,第十五节的沙盘就有实感了。 - 故意把主类挪进子包:把
DemoApplication移到com.example.demo.web,另建一个com.example.demo.service.HelloService(带@Service),在主类里@Autowired它。你会观察到 启动即失败,Description:里写着required a bean of type 'com.example.demo.service.HelloService' that could not be found——这就是速查表第二行。 - 关掉 banner 看看少了几行:加
spring.main.banner-mode=off。你会观察到 那块 ASCII 图案消失,其余日志一字不变——说明 banner 只是装饰,不参与任何启动步骤。 - 加上 devtools 再改代码:加依赖后改
hello()的返回值并保存。你会观察到 控制台出现一段restartedMain线程的日志——重启是在一个新线程上做的,这正是第 9 篇讲的容器重建。
做一个「我的第一个 Boot 工作台」workbench,要求全部用上本篇的知识点。
- 依赖只有
spring-boot-starter-web;parent锁一个真实存在的 Boot 3 版本,java.version设 17 - 包结构:根包
com.example.workbench放WorkbenchApplication,子包controller/service/common - 三个接口:
GET /ping返回pong;GET /time返回当前时间字符串;GET /echo/{msg}原样返回路径变量 - 一个自定义
banner.txt,里面至少写出自己的名字和一个数字 - 一个
CommandLineRunner(写在主类里当@Bean就行),启动完成后打印容器里的 Bean 总数
验收清单:① mvn clean package 通过,java -jar 能起;② 三个接口在浏览器和 curl 下都返回 200;③ 启动日志末尾能看到你那行 Bean 总数;④ 把 WorkbenchApplication 临时挪进 controller 包再启动,你能贴出那段 APPLICATION FAILED TO START 报告并说出根因,然后改回来;⑤ README 里写下「本机能跑需要哪三样东西」(JDK、Maven 或 mvnw、网络)。
不看上文,说出 SpringApplication.run() 的八步里,哪一步之后端口才可访问、哪一步真正实例化了单例 Bean。
web.xml 为什么在 Boot 项目里消失了?用「谁先启动」这句话说清因果。
fat jar 的 MANIFEST.MF 里有哪两行关键属性?分别指向谁?
启动失败报告的 Description: 与 Action: 各自告诉你什么?为什么建议先读 Action:?
为什么说 Boot 是「精装房」而不是「什么都没有的空房」?用 @ConditionalOnMissingBean 那一层含义解释「你放了沙发它就不放」。
starter 拉依赖、parent 定版本、主类住根包、日志盯两行(Tomcat started on port 与 Started XxxApplication)。
第一次跑通 Spring Boot 只需记住这几件事——项目从 start.spring.io 生成,pom.xml 里 parent 统一裁决版本、starter 一站式拉依赖;启动类靠三合一的 @SpringBootApplication(@SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan),SpringApplication.run 返回的是 IoC 容器本身;内嵌 Tomcat 来自 spring-boot-starter-web 的传递依赖,main 先启动、再由它拉起 Tomcat,所以 web.xml 消失了。运行时留意日志里的 Tomcat started on port 8080 与 Started XxxApplication,运行时可用 IDEA、mvn spring-boot:run 或 java -jar 三种方式,打包产物是内嵌一切的 fat jar。第一次的 404、端口占用、改了代码没重启,是必经的三道小坎,按「URL → 包路径 → 注解」的顺序排查即可。