Spring Boot 基础入门:从创建项目到理解第一个请求
这一章不是让你复制一个 HelloController 就结束,而是带你从空目录创建一个 JDK 8 可运行项目,并解释 Maven 如何找到依赖、启动类为什么必须放在根包、Bean 怎么进入容器、依赖如何注入、Tomcat 为什么会启动,以及一个 HTTP 请求如何变成 Java 方法调用。
如果你已经能独立回答这些问题,可以继续学习启动流程全过程;如果还不能,请按本页顺序亲手完成 Demo 和失败实验。
一、学完后必须会什么
- 知道 JDK、Maven、Spring Framework、Spring Boot、Tomcat 分别负责什么。
- 能从空目录创建并运行 Spring Boot 2.7.18 + JDK 8 项目。
- 能解释
pom.xml、启动类、包结构、配置文件和测试目录的作用。 - 能区分普通 Java 对象、Bean、BeanDefinition 和 ApplicationContext。
- 能使用构造器注入组织 Controller、Service 和 Repository。
- 能解释
@SpringBootApplication、@RestController、@Bean等常见注解。 - 能追踪一次 HTTP 请求从端口进入 Controller,再转换为 JSON 的过程。
- 能定位端口占用、404、Bean 找不到、配置没生效和依赖冲突。
- 能写最基本的 MockMvc 自动化测试,而不是只用浏览器点一下。
二、先认识工具链:每一层在做什么
flowchart TD
A["Java 源代码"] --> B["JDK 编译为 class 字节码"]
B --> C["Maven 解析并下载依赖"]
C --> D["Spring Boot 组织启动过程"]
D --> E["Spring 容器管理业务 Bean"]
E --> F["内嵌 Tomcat 监听 HTTP 端口"]| 工具或框架 | 职责 | 没有它会怎样 |
|---|---|---|
| JDK | 编译、运行 Java 程序,提供 JVM 和标准库 | Java 源码无法编译和运行 |
| Maven | 解析项目模型、下载依赖、编译、测试、打包 | 需要手工维护大量 jar 和编译命令 |
| Spring Framework | IoC、依赖注入、AOP、事务、MVC 等核心能力 | Boot 没有底层容器和 Web 框架可装配 |
| Spring Boot | 管理兼容版本、提供 Starter、自动配置、启动封装和运维能力 | 仍能用 Spring,但基础设施要大量手工配置 |
| Tomcat | 实现 Servlet 规范,监听端口,解析 HTTP 并执行 Servlet 链 | Servlet Web 应用没有服务器接收请求 |
不要把“Spring Boot 内嵌 Tomcat”理解为 Boot 自己实现了服务器。Tomcat 仍是独立服务器实现,只是作为依赖进入应用,由 Boot 在同一个 JVM 进程中创建和启动。
三、版本基线:先保证示例真的能运行
本页使用:
JDK 8 语法和字节码目标
Spring Boot 2.7.18
Spring Framework 5.3.x
Maven 3.6+
Servlet javax 命名空间这是大量存量商业项目的重要组合。版本区别必须先讲清:
| 项目 | Boot 2.7 | Boot 3.x |
|---|---|---|
| 最低 Java | Java 8 | Java 17 |
| Spring | Spring 5.3 | Spring 6.x |
| Servlet 包 | javax.servlet.* | jakarta.servlet.* |
| Validation 包 | javax.validation.* | jakarta.validation.* |
| 新项目选择 | 存量兼容和维护 | Java 17+ 新项目优先考虑 |
JDK 7 很重要,但不能运行 Spring Boot 2.7。维护更老项目时可能遇到 Boot 1.x 或传统 Spring;这时也没有 Lambda、Stream、java.time 等 JDK 8 能力。不要为了兼容 JDK 7,把 Boot 2.7 示例写成一个实际上无法运行的组合。
检查环境:
java -version
javac -version
mvn -version如果 java -version 和 mvn -version 显示的 Java 不是同一个版本,说明 Maven 使用了不同的 JAVA_HOME。这会导致 IDE 能运行、命令行却编译失败,或者产物字节码版本不符合预期。
四、从空目录创建项目
4.1 项目目录
boot-order-demo
├─ pom.xml
└─ src
├─ main
│ ├─ java
│ │ └─ com/example/order
│ │ ├─ OrderApplication.java
│ │ ├─ application
│ │ │ └─ OrderService.java
│ │ ├─ domain
│ │ │ └─ Order.java
│ │ └─ web
│ │ └─ OrderController.java
│ └─ resources
│ └─ application.yml
└─ test
└─ java
└─ com/example/order
└─ OrderControllerTest.java这里故意使用订单查询,而不是博客文章 CRUD。它更接近商业系统,同时又足够小,可以看清每一层。
4.2 pom.xml
<?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>2.7.18</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>boot-order-demo</artifactId>
<version>1.0.0</version>
<properties>
<java.version>8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>逐项解释:
| 配置 | 作用 | 常见误解 |
|---|---|---|
parent | 继承 Boot 的默认插件与依赖版本管理 | 不是把 Boot 源码复制进项目 |
java.version | 控制编译目标等 Java 相关默认值 | 当前运行 Maven 的 JDK 仍要满足构建要求 |
starter-web | 聚合 MVC、Tomcat、Jackson、Validation 相关基础依赖 | Starter 本身不是 Web 服务器 |
starter-test | 聚合 JUnit、Spring Test、Mockito、MockMvc 等测试工具 | test scope 不会打进生产运行包 |
| Boot Maven Plugin | 将普通 Jar 重新组织成可执行 Boot Jar | 不是 Java 编译器,编译仍由 Maven 编译插件完成 |
执行:
mvn dependency:tree你会看到 starter-web 间接带入多个依赖。这叫传递依赖。出现版本冲突时,不要随机删除 jar,应先用依赖树确定“谁引入了谁”和最终选中了哪个版本。
五、启动类为什么这样写
package com.example.order;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}5.1 main 方法仍是普通 Java 入口
JVM 先调用 main,然后代码主动调用 SpringApplication.run。没有 JVM 神秘地识别 Spring 注解这一过程;注解由 Spring Boot 启动代码读取和处理。
5.2 @SpringBootApplication 是组合注解
初学阶段可拆成三部分理解:
| 组成 | 作用 | 生效结果 |
|---|---|---|
@SpringBootConfiguration | 表明它是 Boot 主配置类,本质关联 @Configuration | 启动类可以作为配置源 |
@ComponentScan | 扫描启动类所在包及子包 | 找到 Controller、Service、Component 等候选组件 |
@EnableAutoConfiguration | 导入自动配置选择器 | 根据 classpath、配置和已有 Bean 装配默认能力 |
5.3 为什么启动类放根包
默认扫描以 com.example.order 为起点:
flowchart TD
A["com.example.order.OrderApplication"] --> B["扫描 com.example.order.*"]
B --> C["application.OrderService"]
B --> D["web.OrderController"]
B --> E["其他子包组件"]如果启动类被放进 com.example.order.bootstrap,默认只扫描这个包及其子包,兄弟包 web 和 application 就不会被发现。此时接口可能 404,或者注入 Service 时出现 NoSuchBeanDefinitionException。
六、对象、Bean、BeanDefinition 和容器
6.1 普通对象
OrderService service = new OrderService();这个对象由业务代码创建。Spring 不知道它存在,因此不会自动注入依赖、执行 Spring 生命周期、创建 AOP 代理或统一销毁它。
6.2 BeanDefinition
组件扫描发现 @Service 后,首先通常注册的是 BeanDefinition。它是“如何创建 Bean”的元数据,包含类、作用域、是否懒加载、依赖关系、初始化方法等信息。
6.3 Bean
容器根据 BeanDefinition 创建出的受管对象才是 Bean。对于默认单例,容器通常在刷新阶段创建并缓存一个实例,后续注入的是同一个 Bean 或它的代理。
flowchart TD
A["扫描 @Service 类"] --> B["注册 BeanDefinition"]
B --> C["实例化 OrderService"]
C --> D["注入依赖"]
D --> E["执行初始化与后置处理器"]
E --> F["容器保存最终 Bean 或代理"]完整过程见Bean 生命周期全过程。
七、编写领域对象和 Service
7.1 订单对象
package com.example.order.domain;
import java.math.BigDecimal;
public class Order {
private final String orderNo;
private final String status;
private final BigDecimal amount;
public Order(String orderNo, String status, BigDecimal amount) {
this.orderNo = orderNo;
this.status = status;
this.amount = amount;
}
public String getOrderNo() {
return orderNo;
}
public String getStatus() {
return status;
}
public BigDecimal getAmount() {
return amount;
}
}金额使用 BigDecimal,不使用 double。二进制浮点数无法精确表示许多十进制小数,金融计算可能出现精度误差。
7.2 Service
package com.example.order.application;
import com.example.order.domain.Order;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@Service
public class OrderService {
public Order findByOrderNo(String orderNo) {
return new Order(orderNo, "PAID", new BigDecimal("99.90"));
}
}@Service 本质上是一种组件标记。组件扫描发现它后注册 BeanDefinition,容器再创建 Bean。这里先用内存返回值聚焦请求链;真实项目的数据访问应放到 Mapper 或 Repository。
八、为什么推荐构造器注入
package com.example.order.web;
import com.example.order.application.OrderService;
import com.example.order.domain.Order;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@GetMapping("/{orderNo}")
public Order get(@PathVariable String orderNo) {
return orderService.findByOrderNo(orderNo);
}
}Spring 4.3 以后,类只有一个构造器时通常不必写 @Autowired。容器创建 Controller 前,先按构造器参数类型查找 OrderService Bean,然后调用构造器。
flowchart TD
A["准备创建 OrderController"] --> B["读取唯一构造器"]
B --> C["按参数类型查找 OrderService"]
C --> D{"候选 Bean 数量"}
D -- "0 个" --> E["启动失败:缺少 Bean"]
D -- "1 个" --> F["调用构造器完成注入"]
D -- "多个" --> G["继续按 @Primary、@Qualifier 等消歧"]构造器注入的优势:
- 必需依赖在对象创建时就完整,不能忘记赋值。
- 字段可声明
final,对象状态更稳定。 - 单元测试可以直接
new OrderController(fakeService)。 - 构造器依赖过多会直观暴露职责过重。
字段注入虽然代码短,但隐藏依赖、难以直接构造测试,也容易让类不断堆依赖。完整注入算法见@Autowired 与 @Resource 全过程。
九、Web 注解不是路由魔法
| 注解 | 作用 | 关键处理者 |
|---|---|---|
@RestController | 标记 MVC Controller,方法返回值默认写入响应体 | 组件扫描、HandlerMapping、返回值处理器 |
@RequestMapping | 声明类或方法的公共路径、方法、媒体类型等 | RequestMappingHandlerMapping |
@GetMapping | GET 请求映射的组合注解 | RequestMappingHandlerMapping |
@PathVariable | 从 URI 模板提取参数 | HandlerMethodArgumentResolver |
@RequestParam | 从查询参数提取值 | HandlerMethodArgumentResolver |
@RequestBody | 通过消息转换器读取 JSON 等请求体 | HttpMessageConverter |
@ResponseBody | 通过消息转换器写响应体 | 返回值处理器、HttpMessageConverter |
应用启动时,MVC 会扫描 Controller 方法并建立“请求条件 → HandlerMethod”的映射表。请求到来后不是重新扫描所有类,而是使用已注册映射匹配处理器。
十、一次请求的完整过程
请求:
GET /api/orders/O-1001flowchart TD
A["客户端建立 TCP 连接并发送 HTTP"] --> B["Tomcat Connector 读取请求"]
B --> C["Filter 链执行"]
C --> D["DispatcherServlet 接收请求"]
D --> E["HandlerMapping 找到 get 方法"]
E --> F["HandlerAdapter 准备调用"]
F --> G["参数解析器提取 O-1001"]
G --> H["Controller 调用 OrderService"]
H --> I["Controller 返回 Order 对象"]
I --> J["Jackson 转换对象为 JSON"]
J --> K["Tomcat 写回 HTTP 响应"]逐步解释:
- Tomcat 在
8080端口监听连接,解析请求行、Header 和 Body,创建 Servlet 请求与响应对象。 - 请求先经过 Servlet Filter,可以在这里做 traceId、编码、底层安全过滤等。
DispatcherServlet是 Spring MVC 的前端控制器,负责协调后续组件。HandlerMapping根据路径、HTTP 方法、请求头等条件找到OrderController#get。HandlerAdapter让 DispatcherServlet 能以统一方式调用不同类型处理器。- 参数解析器读取路径变量,将字符串
O-1001传给orderNo参数。 - Controller 调用由构造器注入的 OrderService。
- 返回的
Order不是直接变成网络字节;Jackson 消息转换器读取 getter,序列化为 JSON。 - Tomcat 写入状态码、响应头和响应体,再通过网络返回客户端。
预期响应:
{
"orderNo": "O-1001",
"status": "PAID",
"amount": 99.90
}如果请求 POST 同一路径,而代码只有 @GetMapping,通常不是 404,而可能是 405,表示路径可能存在但 HTTP 方法不允许。完整 MVC 内部链路见Spring MVC 请求执行链。
十一、配置文件怎样进入应用
src/main/resources/application.yml:
server:
port: 8080
order:
query:
default-status: PAID启动早期,Boot 将配置文件、环境变量、系统属性、命令行参数等配置源组织进 Environment。自动配置和业务配置绑定都从最终 Environment 读取值。
临时覆盖端口:
java -jar target/boot-order-demo-1.0.0.jar --server.port=9090命令行值优先级通常高于应用内部配置文件,因此实际监听 9090。排查配置不能只打开 application.yml 看一眼,必须确认 active profile、外部配置、环境变量和启动参数。
成组业务配置应使用 @ConfigurationProperties,完整绑定原理见配置体系全过程。
十二、运行、测试和打包分别做什么
12.1 开发时运行
mvn spring-boot:runMaven 解析项目,编译代码,并由 Boot 插件启动应用。看到 Started OrderApplication 只表示启动流程完成,还应确认端口、健康检查和关键依赖是否真的可用。
12.2 运行测试
mvn test只执行测试,不生成最终可执行 Jar。测试失败时 Maven 应以非零退出码结束,CI/CD 才能阻止错误代码部署。
12.3 打包
mvn clean package
java -jar target/boot-order-demo-1.0.0.jarBoot Maven Plugin 的 repackage 会把应用类放在 BOOT-INF/classes,依赖放在 BOOT-INF/lib,再通过 Boot Launcher 建立运行时类路径。可执行 Boot Jar 不是普通“把所有 class 平铺到一起”的 fat jar。
十三、用 MockMvc 验证接口
package com.example.order;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
class OrderControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void returnsOrder() throws Exception {
mockMvc.perform(get("/api/orders/O-1001"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.orderNo").value("O-1001"))
.andExpect(jsonPath("$.status").value("PAID"))
.andExpect(jsonPath("$.amount").value(99.90));
}
}@SpringBootTest 创建完整应用上下文,@AutoConfigureMockMvc 准备 MVC 测试客户端。MockMvc 不需要真正监听网络端口,但会经过 DispatcherServlet、HandlerMapping、参数解析和消息转换等 MVC 主链,因此比直接调用 Controller 方法更能验证 Web 配置。
测试层次应继续区分:
| 测试 | 验证什么 | 是否完整启动上下文 |
|---|---|---|
| 普通单元测试 | 一个类的业务分支 | 否 |
@WebMvcTest | Controller、参数、校验、JSON、异常处理 | 只启动 MVC 切片 |
@SpringBootTest | 多层 Bean、自动配置和整体集成 | 是 |
| 真端口集成测试 | 网络、服务器、Filter 和序列化等完整行为 | 是,并监听端口 |
不要所有测试都用 @SpringBootTest,否则测试慢、问题定位范围过大;也不要只写纯单元测试而完全不验证配置和请求链。
十四、四个失败实验:亲手看到原理
14.1 把启动类移到过深子包
把启动类移到 com.example.order.bootstrap,不改扫描范围。再请求接口,可能得到 404,因为 web 包不在默认扫描树下。
这个实验证明:@ComponentScan 有明确的包边界,不是扫描整个 classpath。
14.2 删除 @Service
删除 OrderService 的 @Service 后启动,Controller 构造器需要 OrderService,但容器没有对应 BeanDefinition,启动会失败并提示需要某类型 Bean。
这个实验证明:有一个 class 文件不等于容器中有一个 Bean。
14.3 删除 starter-web
Controller 注解类型可能直接无法编译;如果通过其他依赖恰好有部分 Spring Web 类,也不代表 Tomcat、MVC 自动配置和 JSON 能力完整存在。
这个实验证明:Starter 是场景依赖入口,但最终能力仍来自具体依赖和自动配置。
14.4 占用 8080 端口
另一个进程先监听 8080,再启动应用,会出现端口绑定失败。代码已经编译、容器可能也创建了许多 Bean,但 WebServer 无法完成监听,应用仍应启动失败。
这个实验证明:日志中“创建了 Bean”不等于服务已经可接流量。
十五、初学者问题排查表
15.1 404:接口找不到
按顺序检查:
- 请求端口和
server.port是否一致。 - 是否配置了
server.servlet.context-path。 - 类上的
@RequestMapping与方法路径拼接后是什么。 - HTTP 方法是否匹配。
- Controller 是否在启动类扫描范围。
- 启动日志中是否有映射注册异常。
- 是否被网关、Nginx 或容器端口映射改写路径。
15.2 Bean 找不到
重点看异常需要的类型和注入位置,然后检查:类是否有组件注解、包是否被扫描、配置类是否被导入、条件注解是否满足、Profile 是否正确、Bean 是否因创建异常而失败。
15.3 配置不生效
先确认最终运行环境,而不是先怀疑 YAML 缩进:检查激活 Profile、外部文件、环境变量、JVM -D 参数、命令行 -- 参数、配置中心覆盖和配置绑定错误。
15.4 依赖类或方法找不到
ClassNotFoundException 常表示运行时缺类;NoSuchMethodError 常表示编译时和运行时加载了不兼容版本。执行:
mvn dependency:tree确认冲突来源,不要直接在本地 Maven 仓库随意删除 jar。
15.5 应用启动了但请求超时
确认端口是否真正监听、进程是否仍活着、线程是否阻塞、连接池是否耗尽、Filter/Security 是否等待、下游调用是否缺少超时。此问题已经超出“Controller 写对没有”,需要结合Actuator 监控与生产排查。
十六、商业项目最基本的代码边界
flowchart TD
A["Controller:协议、校验、响应"] --> B["Application Service:用例编排"]
B --> C["Domain:业务规则和状态"]
B --> D["Repository 或 Mapper:持久化"]
B --> E["Client:外部 HTTP、RPC 或 MQ"]| 层 | 应该做 | 不应该做 |
|---|---|---|
| Controller | 接收 DTO、校验、调用 Service、转换响应 | 写事务流程、拼复杂 SQL、直接操作多个下游 |
| Service | 业务编排、事务边界、幂等入口 | 依赖 HttpServletRequest 才能运行 |
| Domain | 表达核心规则、状态变化 | 直接读取 Spring 配置或发送 HTTP |
| Mapper/Repository | 数据读写 | 决定支付、库存等业务规则 |
| Client | 封装外部通信、超时、错误转换 | 把第三方协议泄漏到所有业务代码 |
真实商业服务还必须补齐参数校验、统一异常、错误码、日志与 traceId、认证授权、事务、超时、重试、幂等、限流、监控和测试。下一步进入Web 请求链与商业工程实践。
十七、常见误解
17.1 “引入 Starter 就会创建所有对象”
错误。Starter 先带入依赖,Boot 再发现候选自动配置,只有条件满足时才注册默认 Bean,Bean 创建还可能因配置或依赖失败。
17.2 “@RestController 会启动 Tomcat”
错误。它只是 Controller 和响应体语义的组合标记。Tomcat 来自 WebServer 自动配置和服务器依赖。
17.3 “应用启动成功就是业务可用”
错误。端口监听成功也不能证明数据库、Redis、MQ、配置中心和下游接口都健康。生产系统要区分进程存活、实例就绪和业务可用。
17.4 “所有对象都应该交给 Spring”
错误。Service、Controller、基础设施客户端等长生命周期协作对象适合成为 Bean;方法中的值对象、DTO 和临时计算对象通常直接创建。滥用容器会隐藏依赖并增加复杂度。
十八、面试标准回答
Spring Boot 是什么
Spring Boot 是基于 Spring Framework 的工程化框架。它通过依赖管理、Starter、条件化自动配置、
SpringApplication启动封装、内嵌 WebServer 和 Actuator,减少通用基础设施配置。它没有替代 Spring IoC、MVC、AOP 和事务,自动配置最终仍是注册 BeanDefinition 并由 Spring 容器创建 Bean。
一个最小应用为什么能启动
JVM 从
main方法进入SpringApplication.run。Boot 准备 Environment、创建 ApplicationContext、解析启动类的组件扫描和自动配置,refresh()创建 Bean。因为starter-web带入 MVC、Jackson 和 Tomcat,相关条件满足后 Boot 创建 MVC 与 WebServer 基础设施,Tomcat 最终绑定端口并接收请求。
为什么推荐构造器注入
构造器注入能明确表达必需依赖,使对象创建后就是完整状态,字段可以是 final,也便于单元测试。单构造器在现代 Spring 中通常不需要写
@Autowired。字段注入会隐藏依赖并增加脱离容器测试的难度。
面试页只保留回答和追问,详细原理回到本页及对应深页:Spring Boot 面试题。
十九、下一步学习路线
- 启动流程全过程:理解
SpringApplication、Environment、Context、refresh()和 WebServer。 - 配置体系全过程:理解 PropertySource、优先级、Binder 和配置排查。
- 自动配置原理:理解候选类、条件、顺序和 BeanDefinition。
- Starter 全过程:自己封装公共客户端。
- Web 请求链与商业工程实践:补齐校验、异常、事务、日志和测试。
- Actuator:从“能运行”进入“能观测、能排查”。
本章小结
第一个 Spring Boot 接口背后是一条完整链路:Maven 带入兼容依赖,JVM 调用 main,Boot 准备配置与容器,组件扫描注册业务 BeanDefinition,自动配置注册 Web 基础设施,Spring 创建并注入 Bean,Tomcat 监听端口,Spring MVC 匹配 Controller,Jackson 将返回对象写成 JSON。
只有能解释每一环“输入是什么、谁来处理、输出是什么、失败会看到什么”,才算真正完成 Spring Boot 基础入门。
