Skip to content

Spring Boot 基础入门:从创建项目到理解第一个请求

这一章不是让你复制一个 HelloController 就结束,而是带你从空目录创建一个 JDK 8 可运行项目,并解释 Maven 如何找到依赖、启动类为什么必须放在根包、Bean 怎么进入容器、依赖如何注入、Tomcat 为什么会启动,以及一个 HTTP 请求如何变成 Java 方法调用。

如果你已经能独立回答这些问题,可以继续学习启动流程全过程;如果还不能,请按本页顺序亲手完成 Demo 和失败实验。

一、学完后必须会什么

  1. 知道 JDK、Maven、Spring Framework、Spring Boot、Tomcat 分别负责什么。
  2. 能从空目录创建并运行 Spring Boot 2.7.18 + JDK 8 项目。
  3. 能解释 pom.xml、启动类、包结构、配置文件和测试目录的作用。
  4. 能区分普通 Java 对象、Bean、BeanDefinition 和 ApplicationContext。
  5. 能使用构造器注入组织 Controller、Service 和 Repository。
  6. 能解释 @SpringBootApplication@RestController@Bean 等常见注解。
  7. 能追踪一次 HTTP 请求从端口进入 Controller,再转换为 JSON 的过程。
  8. 能定位端口占用、404、Bean 找不到、配置没生效和依赖冲突。
  9. 能写最基本的 MockMvc 自动化测试,而不是只用浏览器点一下。

二、先认识工具链:每一层在做什么

mermaid
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 FrameworkIoC、依赖注入、AOP、事务、MVC 等核心能力Boot 没有底层容器和 Web 框架可装配
Spring Boot管理兼容版本、提供 Starter、自动配置、启动封装和运维能力仍能用 Spring,但基础设施要大量手工配置
Tomcat实现 Servlet 规范,监听端口,解析 HTTP 并执行 Servlet 链Servlet Web 应用没有服务器接收请求

不要把“Spring Boot 内嵌 Tomcat”理解为 Boot 自己实现了服务器。Tomcat 仍是独立服务器实现,只是作为依赖进入应用,由 Boot 在同一个 JVM 进程中创建和启动。

三、版本基线:先保证示例真的能运行

本页使用:

text
JDK 8 语法和字节码目标
Spring Boot 2.7.18
Spring Framework 5.3.x
Maven 3.6+
Servlet javax 命名空间

这是大量存量商业项目的重要组合。版本区别必须先讲清:

项目Boot 2.7Boot 3.x
最低 JavaJava 8Java 17
SpringSpring 5.3Spring 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 示例写成一个实际上无法运行的组合。

检查环境:

bash
java -version
javac -version
mvn -version

如果 java -versionmvn -version 显示的 Java 不是同一个版本,说明 Maven 使用了不同的 JAVA_HOME。这会导致 IDE 能运行、命令行却编译失败,或者产物字节码版本不符合预期。

四、从空目录创建项目

4.1 项目目录

text
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
<?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 编译插件完成

执行:

bash
mvn dependency:tree

你会看到 starter-web 间接带入多个依赖。这叫传递依赖。出现版本冲突时,不要随机删除 jar,应先用依赖树确定“谁引入了谁”和最终选中了哪个版本。

五、启动类为什么这样写

java
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 为起点:

mermaid
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,默认只扫描这个包及其子包,兄弟包 webapplication 就不会被发现。此时接口可能 404,或者注入 Service 时出现 NoSuchBeanDefinitionException

六、对象、Bean、BeanDefinition 和容器

6.1 普通对象

java
OrderService service = new OrderService();

这个对象由业务代码创建。Spring 不知道它存在,因此不会自动注入依赖、执行 Spring 生命周期、创建 AOP 代理或统一销毁它。

6.2 BeanDefinition

组件扫描发现 @Service 后,首先通常注册的是 BeanDefinition。它是“如何创建 Bean”的元数据,包含类、作用域、是否懒加载、依赖关系、初始化方法等信息。

6.3 Bean

容器根据 BeanDefinition 创建出的受管对象才是 Bean。对于默认单例,容器通常在刷新阶段创建并缓存一个实例,后续注入的是同一个 Bean 或它的代理。

mermaid
flowchart TD
    A["扫描 @Service 类"] --> B["注册 BeanDefinition"]
    B --> C["实例化 OrderService"]
    C --> D["注入依赖"]
    D --> E["执行初始化与后置处理器"]
    E --> F["容器保存最终 Bean 或代理"]

完整过程见Bean 生命周期全过程

七、编写领域对象和 Service

7.1 订单对象

java
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

java
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。

八、为什么推荐构造器注入

java
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,然后调用构造器。

mermaid
flowchart TD
    A["准备创建 OrderController"] --> B["读取唯一构造器"]
    B --> C["按参数类型查找 OrderService"]
    C --> D{"候选 Bean 数量"}
    D -- "0 个" --> E["启动失败:缺少 Bean"]
    D -- "1 个" --> F["调用构造器完成注入"]
    D -- "多个" --> G["继续按 @Primary、@Qualifier 等消歧"]

构造器注入的优势:

  1. 必需依赖在对象创建时就完整,不能忘记赋值。
  2. 字段可声明 final,对象状态更稳定。
  3. 单元测试可以直接 new OrderController(fakeService)
  4. 构造器依赖过多会直观暴露职责过重。

字段注入虽然代码短,但隐藏依赖、难以直接构造测试,也容易让类不断堆依赖。完整注入算法见@Autowired@Resource 全过程

九、Web 注解不是路由魔法

注解作用关键处理者
@RestController标记 MVC Controller,方法返回值默认写入响应体组件扫描、HandlerMapping、返回值处理器
@RequestMapping声明类或方法的公共路径、方法、媒体类型等RequestMappingHandlerMapping
@GetMappingGET 请求映射的组合注解RequestMappingHandlerMapping
@PathVariable从 URI 模板提取参数HandlerMethodArgumentResolver
@RequestParam从查询参数提取值HandlerMethodArgumentResolver
@RequestBody通过消息转换器读取 JSON 等请求体HttpMessageConverter
@ResponseBody通过消息转换器写响应体返回值处理器、HttpMessageConverter

应用启动时,MVC 会扫描 Controller 方法并建立“请求条件 → HandlerMethod”的映射表。请求到来后不是重新扫描所有类,而是使用已注册映射匹配处理器。

十、一次请求的完整过程

请求:

text
GET /api/orders/O-1001
mermaid
flowchart 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 响应"]

逐步解释:

  1. Tomcat 在 8080 端口监听连接,解析请求行、Header 和 Body,创建 Servlet 请求与响应对象。
  2. 请求先经过 Servlet Filter,可以在这里做 traceId、编码、底层安全过滤等。
  3. DispatcherServlet 是 Spring MVC 的前端控制器,负责协调后续组件。
  4. HandlerMapping 根据路径、HTTP 方法、请求头等条件找到 OrderController#get
  5. HandlerAdapter 让 DispatcherServlet 能以统一方式调用不同类型处理器。
  6. 参数解析器读取路径变量,将字符串 O-1001 传给 orderNo 参数。
  7. Controller 调用由构造器注入的 OrderService。
  8. 返回的 Order 不是直接变成网络字节;Jackson 消息转换器读取 getter,序列化为 JSON。
  9. Tomcat 写入状态码、响应头和响应体,再通过网络返回客户端。

预期响应:

json
{
  "orderNo": "O-1001",
  "status": "PAID",
  "amount": 99.90
}

如果请求 POST 同一路径,而代码只有 @GetMapping,通常不是 404,而可能是 405,表示路径可能存在但 HTTP 方法不允许。完整 MVC 内部链路见Spring MVC 请求执行链

十一、配置文件怎样进入应用

src/main/resources/application.yml

yaml
server:
  port: 8080

order:
  query:
    default-status: PAID

启动早期,Boot 将配置文件、环境变量、系统属性、命令行参数等配置源组织进 Environment。自动配置和业务配置绑定都从最终 Environment 读取值。

临时覆盖端口:

bash
java -jar target/boot-order-demo-1.0.0.jar --server.port=9090

命令行值优先级通常高于应用内部配置文件,因此实际监听 9090。排查配置不能只打开 application.yml 看一眼,必须确认 active profile、外部配置、环境变量和启动参数。

成组业务配置应使用 @ConfigurationProperties,完整绑定原理见配置体系全过程

十二、运行、测试和打包分别做什么

12.1 开发时运行

bash
mvn spring-boot:run

Maven 解析项目,编译代码,并由 Boot 插件启动应用。看到 Started OrderApplication 只表示启动流程完成,还应确认端口、健康检查和关键依赖是否真的可用。

12.2 运行测试

bash
mvn test

只执行测试,不生成最终可执行 Jar。测试失败时 Maven 应以非零退出码结束,CI/CD 才能阻止错误代码部署。

12.3 打包

bash
mvn clean package
java -jar target/boot-order-demo-1.0.0.jar

Boot Maven Plugin 的 repackage 会把应用类放在 BOOT-INF/classes,依赖放在 BOOT-INF/lib,再通过 Boot Launcher 建立运行时类路径。可执行 Boot Jar 不是普通“把所有 class 平铺到一起”的 fat jar。

十三、用 MockMvc 验证接口

java
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 配置。

测试层次应继续区分:

测试验证什么是否完整启动上下文
普通单元测试一个类的业务分支
@WebMvcTestController、参数、校验、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:接口找不到

按顺序检查:

  1. 请求端口和 server.port 是否一致。
  2. 是否配置了 server.servlet.context-path
  3. 类上的 @RequestMapping 与方法路径拼接后是什么。
  4. HTTP 方法是否匹配。
  5. Controller 是否在启动类扫描范围。
  6. 启动日志中是否有映射注册异常。
  7. 是否被网关、Nginx 或容器端口映射改写路径。

15.2 Bean 找不到

重点看异常需要的类型和注入位置,然后检查:类是否有组件注解、包是否被扫描、配置类是否被导入、条件注解是否满足、Profile 是否正确、Bean 是否因创建异常而失败。

15.3 配置不生效

先确认最终运行环境,而不是先怀疑 YAML 缩进:检查激活 Profile、外部文件、环境变量、JVM -D 参数、命令行 -- 参数、配置中心覆盖和配置绑定错误。

15.4 依赖类或方法找不到

ClassNotFoundException 常表示运行时缺类;NoSuchMethodError 常表示编译时和运行时加载了不兼容版本。执行:

bash
mvn dependency:tree

确认冲突来源,不要直接在本地 Maven 仓库随意删除 jar。

15.5 应用启动了但请求超时

确认端口是否真正监听、进程是否仍活着、线程是否阻塞、连接池是否耗尽、Filter/Security 是否等待、下游调用是否缺少超时。此问题已经超出“Controller 写对没有”,需要结合Actuator 监控与生产排查

十六、商业项目最基本的代码边界

mermaid
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 面试题

十九、下一步学习路线

  1. 启动流程全过程:理解 SpringApplication、Environment、Context、refresh() 和 WebServer。
  2. 配置体系全过程:理解 PropertySource、优先级、Binder 和配置排查。
  3. 自动配置原理:理解候选类、条件、顺序和 BeanDefinition。
  4. Starter 全过程:自己封装公共客户端。
  5. Web 请求链与商业工程实践:补齐校验、异常、事务、日志和测试。
  6. Actuator:从“能运行”进入“能观测、能排查”。

本章小结

第一个 Spring Boot 接口背后是一条完整链路:Maven 带入兼容依赖,JVM 调用 main,Boot 准备配置与容器,组件扫描注册业务 BeanDefinition,自动配置注册 Web 基础设施,Spring 创建并注入 Bean,Tomcat 监听端口,Spring MVC 匹配 Controller,Jackson 将返回对象写成 JSON。

只有能解释每一环“输入是什么、谁来处理、输出是什么、失败会看到什么”,才算真正完成 Spring Boot 基础入门。