468 lines
15 KiB
Markdown
468 lines
15 KiB
Markdown
# 第二阶段教案:Spring Boot 入门(第 2 周)
|
||
|
||
> **学习周期**:7 天
|
||
> **每日用时**:2-3 小时
|
||
> **最终产出**:RESTful 学生管理 API(分层架构 + 参数校验 + 多环境配置)
|
||
> **项目结构**:Maven + Spring Boot 3.2
|
||
|
||
---
|
||
|
||
## Maven 项目导入
|
||
|
||
在 IDEA 中:File → Open → 选择 `week2/pom.xml` → Open as Project。
|
||
IDEA 会自动下载依赖,等待右下角进度条完成即可。
|
||
|
||
---
|
||
|
||
## 第 1 天:Spring 是什么?IoC 和 DI
|
||
|
||
### 核心概念
|
||
|
||
**没有 Spring 时**:你写代码手动 `new` 对象,所有对象的创建和关联都由你自己管理。
|
||
|
||
```java
|
||
UserRepository repo = new UserRepository(); // 自己创建
|
||
UserService service = new UserService(); // 自己创建
|
||
service.setUserRepository(repo); // 自己关联
|
||
```
|
||
|
||
**有了 Spring 后**:对象的创建和关联由容器负责,你只需要声明"我需要什么"。
|
||
|
||
```java
|
||
@Service
|
||
public class UserService {
|
||
private final UserRepository repo; // Spring 会自动注入
|
||
// ...
|
||
}
|
||
```
|
||
|
||
### 三个关键术语
|
||
|
||
| 术语 | 白话解释 |
|
||
|------|---------|
|
||
| **IoC**(控制反转) | 创建对象的控制权从你手里"反转"给了 Spring 容器 |
|
||
| **DI**(依赖注入) | 当 A 需要 B 时,Spring 自动把 B "注入"到 A 里,你不需要 `new` |
|
||
| **容器** | 一个巨大的 Map,存着所有被 Spring 管理的对象(称为 Bean) |
|
||
|
||
### 动手:运行 IoC 演示
|
||
|
||
打开 `src/main/java/com/learn/ioc/SimpleIocDemo.java`,右键 Run。
|
||
|
||
这个文件包含了一个用纯 Java 手写的极简 IoC 容器(约 100 行),没有依赖任何框架。它会输出:
|
||
|
||
```
|
||
========== IoC 容器原理演示 ==========
|
||
|
||
--- 方式一:手动 setter 注入 ---
|
||
调用结果: 张三(来自数据库)
|
||
|
||
--- 方式二:注解自动注入 ---
|
||
调用结果: 订单 #12345
|
||
|
||
========== 总结 ==========
|
||
没有 IoC 容器时:A a = new A(); a.setB(new B()); // 自己控制一切
|
||
有了 IoC 容器后:@Autowired B b; // 容器自动注入,你只管用
|
||
```
|
||
|
||
**今日任务**:
|
||
1. 逐行读懂 `SimpleIocDemo.java`,理解容器/注册/注入三步
|
||
2. 尝试在 `SimpleIocDemo` 里新增一个类(如 `ProductService`),并把它注册到容器中
|
||
3. 思考:为什么反射(Reflection)是实现 IoC 的关键技术?
|
||
|
||
---
|
||
|
||
## 第 2 天:Spring Boot 项目结构
|
||
|
||
### 核心概念
|
||
|
||
Spring Boot 不是新框架,它是对 Spring 的**封装和简化**。传统 Spring 需要大量 XML 配置,Spring Boot 通过"约定大于配置"消除了这些繁琐步骤。
|
||
|
||
### 项目结构详解
|
||
|
||
```
|
||
week2/
|
||
├── pom.xml # Maven 配置(依赖管理)
|
||
└── src/
|
||
├── main/
|
||
│ ├── java/com/learn/
|
||
│ │ ├── Week2Application.java # 启动入口
|
||
│ │ ├── controller/ # 控制器层(接收 HTTP 请求)
|
||
│ │ ├── service/ # 业务逻辑层
|
||
│ │ ├── repository/ # 数据访问层
|
||
│ │ ├── model/ # 数据模型
|
||
│ │ ├── config/ # 配置类
|
||
│ │ └── exception/ # 全局异常处理
|
||
│ └── resources/
|
||
│ ├── application.yml # 主配置文件
|
||
│ ├── application-dev.yml # 开发环境
|
||
│ └── application-prod.yml # 生产环境
|
||
└── test/ # 测试代码
|
||
```
|
||
|
||
### Spring Boot 启动流程
|
||
|
||
1. `main()` 调用 `SpringApplication.run()`
|
||
2. 创建 `ApplicationContext`(即 IoC 容器)
|
||
3. 扫描 `@SpringBootApplication` 所在包及子包
|
||
4. 找到所有带 `@Component` / `@Service` / `@Repository` / `@Controller` 的类
|
||
5. 实例化它们并处理依赖注入
|
||
6. 启动内嵌 Tomcat,监听 8080 端口
|
||
|
||
### 动手:观察启动日志
|
||
|
||
在 IDEA 中运行 `Week2Application.main()`,观察控制台输出:
|
||
|
||
```
|
||
Started Week2Application in 1.234 seconds
|
||
Tomcat started on port 8080
|
||
```
|
||
|
||
你会看到 Spring Boot 的 Banner、自动配置报告、Tomcat 启动信息。
|
||
|
||
**今日任务**:
|
||
1. 对着上面的目录结构,在你的项目中找到每个文件的位置
|
||
2. 打开 `pom.xml`,理解每个 `<dependency>` 的作用
|
||
3. 尝试改 `application.yml` 中的 `server.port` 为 9090,重启验证
|
||
|
||
---
|
||
|
||
## 第 3 天:第一个 REST API
|
||
|
||
### 核心概念
|
||
|
||
| 注解 | 作用 | 举例 |
|
||
|------|------|------|
|
||
| `@RestController` | 标记 REST 控制器,方法返回值自动转 JSON | 写在类上 |
|
||
| `@GetMapping("/xxx")` | 映射 HTTP GET 请求 | `@GetMapping("/hello")` |
|
||
| `@PathVariable` | 从 URL 路径中取值 | `/hello/{id}` → `@PathVariable Long id` |
|
||
| `@RequestParam` | 从 URL 查询参数中取值 | `/greet?name=张三` → `@RequestParam String name` |
|
||
|
||
### 动手:测试接口
|
||
|
||
运行 `Week2Application`,打开浏览器或 Postman 访问:
|
||
|
||
| URL | 预期结果 |
|
||
|-----|---------|
|
||
| `http://localhost:8080/hello` | `Hello, Spring Boot!` |
|
||
| `http://localhost:8080/hello/小明` | `你好,小明!欢迎来到 Spring Boot 的世界。` |
|
||
| `http://localhost:8080/greet?name=张三&age=20` | `你好,张三!你今年 20 岁。` |
|
||
| `http://localhost:8080/greet` | `你好,同学!`(默认值生效) |
|
||
| `http://localhost:8080/info` | JSON 对象 `{"status":"UP","message":"...","timestamp":...}` |
|
||
|
||
### 关键理解
|
||
|
||
`@RestController` 的请求处理流程:
|
||
|
||
```
|
||
HTTP 请求 → DispatcherServlet(前端控制器)
|
||
→ 找到匹配的 Controller 方法
|
||
→ 执行方法,得到返回值
|
||
→ Jackson 将返回值序列化为 JSON
|
||
→ HTTP 响应返回给客户端
|
||
```
|
||
|
||
**今日任务**:
|
||
1. 在 `HelloController` 中新增一个接口:`GET /echo?msg=xxx`,返回 `{"echo": "xxx"}`
|
||
2. 用 Postman 测试所有接口,观察请求头和响应头
|
||
3. 思考:`@RestController` 和普通 `@Controller` 有什么区别?(提示:`@ResponseBody`)
|
||
|
||
---
|
||
|
||
## 第 4 天:RESTful CRUD(上)—— 请求映射
|
||
|
||
### 核心概念
|
||
|
||
RESTful API 用 HTTP 方法表达操作意图:
|
||
|
||
| HTTP 方法 | 对应注解 | 语义 |
|
||
|-----------|---------|------|
|
||
| GET | `@GetMapping` | 查询(安全、幂等) |
|
||
| POST | `@PostMapping` | 新增 |
|
||
| PUT | `@PutMapping` | 更新(幂等) |
|
||
| DELETE | `@DeleteMapping` | 删除(幂等) |
|
||
|
||
### @RequestBody —— 接收 JSON 请求体
|
||
|
||
```java
|
||
@PostMapping
|
||
public ResponseEntity<?> add(@RequestBody Student student) {
|
||
// Spring 自动将请求体的 JSON → Student 对象
|
||
}
|
||
```
|
||
|
||
客户端发送:
|
||
```json
|
||
POST /api/students
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"name": "张三",
|
||
"age": 20,
|
||
"email": "zhangsan@mail.com",
|
||
"score": 85
|
||
}
|
||
```
|
||
|
||
### 动手:测试 CRUD 接口
|
||
|
||
> 第 4 天的代码在 `StudentController` 中,此时还没有 Service/Repository 分层。
|
||
> 阅读 `StudentController` 的代码,理解每个方法的注解和 URL 设计。
|
||
|
||
| 操作 | 方法 | URL | 请求体 |
|
||
|------|------|-----|--------|
|
||
| 查全部 | GET | `/api/students` | - |
|
||
| 查单个 | GET | `/api/students/1` | - |
|
||
| 搜索 | GET | `/api/students?keyword=张` | - |
|
||
| 新增 | POST | `/api/students` | JSON |
|
||
| 更新 | PUT | `/api/students/1` | JSON |
|
||
| 删除 | DELETE | `/api/students/1` | - |
|
||
|
||
用 Postman 逐个测试(启动后已有 5 条预置数据)。
|
||
|
||
**今日任务**:
|
||
1. 用 Postman 完成"新增→查询→更新→删除"的完整流程
|
||
2. 观察 PUT 更新时只传部分字段的效果
|
||
3. 尝试故意传一个错误的 JSON(少字段、错类型),观察返回什么
|
||
|
||
---
|
||
|
||
## 第 5 天:分层架构 —— Controller → Service → Repository
|
||
|
||
### 为什么需要分层?
|
||
|
||
第 4 天所有逻辑挤在 Controller 里。当项目变复杂时:
|
||
|
||
- Controller 既处理请求又写业务逻辑,又操作数据 → **难以维护**
|
||
- 一个地方改了,可能影响不相关的地方 → **难以测试**
|
||
|
||
### 三层架构
|
||
|
||
```
|
||
┌──────────────────────────────────────┐
|
||
│ Controller 层(@RestController) │ ← 接收请求、参数校验、返回响应
|
||
│ "我只负责接待客人,不干活" │
|
||
├──────────────────────────────────────┤
|
||
│ Service 层(@Service) │ ← 业务逻辑、事务管理
|
||
│ "我负责干活,不知道客人从哪来的" │
|
||
├──────────────────────────────────────┤
|
||
│ Repository 层(@Repository) │ ← 数据存取
|
||
│ "我只负责存取数据,不知道业务逻辑" │
|
||
└──────────────────────────────────────┘
|
||
```
|
||
|
||
### 依赖方向(重要!)
|
||
|
||
```
|
||
Controller → Service → Repository
|
||
↑ ↑
|
||
只能从上往下依赖,不能反着来
|
||
```
|
||
|
||
每个层只依赖下一层,不跨层调用。Repository 不能依赖 Service,Service 不能依赖 Controller。
|
||
|
||
### 本项目的分层实现
|
||
|
||
打开对应文件,理解各层职责:
|
||
|
||
| 文件 | 层 | 关键点 |
|
||
|------|-----|--------|
|
||
| `controller/StudentController.java` | Controller | 只做参数提取和响应封装,调用 Service |
|
||
| `service/StudentService.java` | Service | 业务逻辑(校验、组合),调用 Repository |
|
||
| `repository/StudentRepository.java` | Repository | 数据存取,使用 ConcurrentHashMap 模拟数据库 |
|
||
| `model/Student.java` | Model | 贯穿三层的数据载体 |
|
||
|
||
### 注入方式对比
|
||
|
||
```java
|
||
// ❌ 字段注入(不推荐——测试困难,掩盖依赖关系)
|
||
@Autowired
|
||
private StudentService service;
|
||
|
||
// ✅ 构造方法注入(推荐——依赖明确,易于测试)
|
||
private final StudentService service;
|
||
public StudentController(StudentService service) {
|
||
this.service = service;
|
||
}
|
||
```
|
||
|
||
**今日任务**:
|
||
1. 画出三层架构的依赖关系图
|
||
2. 在 `StudentService` 中新增一个方法:`getTopStudents(int n)`,返回成绩最高的 n 个学生
|
||
3. 为这个新方法在 Controller 中新增接口 `GET /api/students/top?n=3`
|
||
|
||
---
|
||
|
||
## 第 6 天:配置管理
|
||
|
||
### application.yml vs application.properties
|
||
|
||
```yaml
|
||
# YAML 格式(推荐,层次清晰)
|
||
server:
|
||
port: 8080
|
||
spring:
|
||
application:
|
||
name: my-app
|
||
```
|
||
|
||
```properties
|
||
# properties 格式(传统,扁平)
|
||
server.port=8080
|
||
spring.application.name=my-app
|
||
```
|
||
|
||
两者等效,YAML 更直观。
|
||
|
||
### 多环境配置
|
||
|
||
```
|
||
application.yml # 公共配置
|
||
application-dev.yml # 开发环境
|
||
application-prod.yml # 生产环境
|
||
```
|
||
|
||
`application.yml` 中通过 `spring.profiles.active: dev` 指定激活哪个环境。
|
||
|
||
激活方式(优先级从高到低):
|
||
1. 命令行:`java -jar app.jar --spring.profiles.active=prod`
|
||
2. 环境变量:`SPRING_PROFILES_ACTIVE=prod`
|
||
3. application.yml 中的 `spring.profiles.active`
|
||
|
||
### @Value 读取配置
|
||
|
||
```java
|
||
@Value("${app.name}")
|
||
private String appName;
|
||
```
|
||
|
||
### 自定义配置
|
||
|
||
在 YAML 中写:
|
||
```yaml
|
||
app:
|
||
name: 学生管理系统
|
||
version: 1.0.0
|
||
```
|
||
|
||
在代码中读:
|
||
```java
|
||
@Value("${app.name}")
|
||
private String appName;
|
||
```
|
||
|
||
**今日任务**:
|
||
1. 修改 `spring.profiles.active` 为 `prod`,观察端口和日志变化
|
||
2. 在 `HelloController` 中用 `@Value` 注入 `app.name`,新增接口返回应用名称
|
||
3. 想一想:为什么数据库密码、API Key 要放在配置文件中而不是代码里?
|
||
|
||
---
|
||
|
||
## 第 7 天:整合与总结
|
||
|
||
### 全局异常处理
|
||
|
||
复习 `exception/GlobalExceptionHandler.java`:
|
||
|
||
```java
|
||
@RestControllerAdvice // 拦截所有 Controller 的异常
|
||
public class GlobalExceptionHandler {
|
||
|
||
@ExceptionHandler(MethodArgumentNotValidException.class) // 参数校验失败
|
||
public ResponseEntity<?> handleValidation(...) { ... }
|
||
|
||
@ExceptionHandler(Exception.class) // 兜底:所有未捕获的异常
|
||
public ResponseEntity<?> handleAll(...) { ... }
|
||
}
|
||
```
|
||
|
||
有了这个,Controller 的任何异常都会被拦截并返回友好的 JSON 错误信息。
|
||
|
||
### 参数校验(Bean Validation)
|
||
|
||
看 `Student` 类中的注解:
|
||
```java
|
||
@NotBlank(message = "姓名不能为空")
|
||
private String name;
|
||
|
||
@Min(value = 1, message = "年龄必须大于 0")
|
||
private int age;
|
||
|
||
@Email(message = "邮箱格式不正确")
|
||
private String email;
|
||
```
|
||
|
||
Controller 中用 `@Valid` 触发校验:
|
||
```java
|
||
@PostMapping
|
||
public ResponseEntity<?> add(@Valid @RequestBody Student student) { ... }
|
||
```
|
||
|
||
### 本周完整的请求处理流程
|
||
|
||
```
|
||
1. HTTP 请求到达 → DispatcherServlet 接收
|
||
2. 根据 URL + HTTP Method 匹配 Controller 方法
|
||
3. @Valid 触发参数校验(失败 → GlobalExceptionHandler 捕获 → 返回 400)
|
||
4. Controller 调用 Service 处理业务逻辑
|
||
5. Service 调用 Repository 存取数据
|
||
6. 结果返回 Controller,封装为 ResponseEntity
|
||
7. Jackson 序列化为 JSON,写入 HTTP 响应
|
||
```
|
||
|
||
### 测试(预习第 8 周)
|
||
|
||
打开 `StudentControllerTest.java`,这是一个集成测试示例,用 `TestRestTemplate` 模拟 HTTP 请求来测试真实接口。
|
||
|
||
运行方式:右键类名 → Run。观察所有测试通过。
|
||
|
||
### 最终验收清单
|
||
|
||
用 Postman 完成以下测试:
|
||
|
||
- [ ] `GET /api/students` 返回 5 条预置数据
|
||
- [ ] `GET /api/students/1` 返回张三的信息
|
||
- [ ] `GET /api/students?keyword=李` 只返回李四
|
||
- [ ] `POST /api/students` 新增一个学生,返回 201
|
||
- [ ] `POST /api/students` 传空 name,返回 400 校验错误
|
||
- [ ] `PUT /api/students/1` 修改张三的年龄,返回更新后数据
|
||
- [ ] `DELETE /api/students/1` 删除张三,返回成功
|
||
- [ ] `DELETE /api/students/999` 删除不存在的,返回 404
|
||
- [ ] 重启应用,之前新增/修改的数据被重置(内存存储)
|
||
|
||
---
|
||
|
||
## 本周产出
|
||
|
||
**一个完整的 RESTful 学生管理 API 服务**,具备:
|
||
|
||
```
|
||
✅ 三层分层架构(Controller → Service → Repository)
|
||
✅ 完整的 CRUD 接口(GET/POST/PUT/DELETE)
|
||
✅ 模糊搜索功能
|
||
✅ 统一 JSON 响应格式(code + message + data)
|
||
✅ 参数校验(@Valid + Bean Validation)
|
||
✅ 全局异常处理(@RestControllerAdvice)
|
||
✅ 多环境配置(dev/prod)
|
||
✅ 启动时预置测试数据(CommandLineRunner)
|
||
✅ 基础集成测试
|
||
```
|
||
|
||
---
|
||
|
||
## 常见问题排查
|
||
|
||
| 问题 | 原因 | 解决 |
|
||
|------|------|------|
|
||
| 启动报端口占用 | 8080 端口已被占用 | 改 `application.yml` 中的 `server.port` |
|
||
| `@Autowired` 报红 | 类上没有 `@Service` 等注解 | 检查是否漏了 `@Service` / `@Repository` / `@Component` |
|
||
| 找不到 Bean | 类不在启动类所在包的子包下 | Spring 默认扫描启动类同级及子包 |
|
||
| POST 请求 415 | Content-Type 没设对 | Postman 中 Body 选 raw → JSON |
|
||
| 中文乱码 | 未设编码 | Spring Boot 默认 UTF-8,如果乱码检查 Postman 设置 |
|
||
| JSON 返回 406 | 可能缺少 Jackson 依赖 | `spring-boot-starter-web` 已包含,检查 pom.xml |
|
||
| 修改代码不生效 | 需要重启 | 后续第 8 周会学 DevTools 热重载 |
|
||
|
||
---
|
||
|
||
> **本周核心**:理解 Spring 的 IoC/DI 思想 → 掌握分层架构 → 能写标准的 RESTful CRUD API。
|
||
> 这 7 天你建立的是整个 Spring 技术栈的地基,后续 JPA、Security、AI 集成都是在这个地基上添砖加瓦。
|