Files
gc-plan/week2/教案.md
2026-04-29 23:45:17 +08:00

468 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 第二阶段教案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 不能依赖 ServiceService 不能依赖 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 集成都是在这个地基上添砖加瓦。