Week 1-8: Spring Boot 学习计划完整项目
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
53
week2/pom.xml
Normal file
53
week2/pom.xml
Normal file
@@ -0,0 +1,53 @@
|
||||
<?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.2.5</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.learn</groupId>
|
||||
<artifactId>week2-spring-boot</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<name>Week 2: Spring Boot 入门</name>
|
||||
<description>从 IoC 手写到 RESTful API 的 Spring Boot 入门项目</description>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot Web 启动器(内置 Tomcat + Spring MVC) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Bean Validation:参数校验 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Test -->
|
||||
<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>
|
||||
21
week2/src/main/java/com/learn/Week2Application.java
Normal file
21
week2/src/main/java/com/learn/Week2Application.java
Normal file
@@ -0,0 +1,21 @@
|
||||
package com.learn;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* Spring Boot 应用主入口
|
||||
*
|
||||
* @SpringBootApplication 是三个注解的组合:
|
||||
* @SpringBootConfiguration —— 标记为配置类
|
||||
* @EnableAutoConfiguration —— 自动装配(Spring Boot 的核心能力)
|
||||
* @ComponentScan —— 扫描当前包及子包的组件
|
||||
*
|
||||
* 运行方式:IDEA 中右键此类 → Run 'Week2Application'
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class Week2Application {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Week2Application.class, args);
|
||||
}
|
||||
}
|
||||
44
week2/src/main/java/com/learn/config/AppConfig.java
Normal file
44
week2/src/main/java/com/learn/config/AppConfig.java
Normal file
@@ -0,0 +1,44 @@
|
||||
package com.learn.config;
|
||||
|
||||
import com.learn.model.Student;
|
||||
import com.learn.repository.StudentRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 第 6 天:Spring 配置类
|
||||
*
|
||||
* @Configuration 标记这是一个配置类,相当于一个"工厂",用于创建和管理 Bean。
|
||||
* 替代了传统 Spring 中繁琐的 XML 配置文件。
|
||||
*
|
||||
* @Bean 方法返回的对象会被自动注册到 Spring 容器中。
|
||||
*/
|
||||
@Configuration
|
||||
public class AppConfig {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(AppConfig.class);
|
||||
|
||||
/**
|
||||
* 应用启动后执行的初始化逻辑
|
||||
*
|
||||
* CommandLineRunner:Spring Boot 启动完成后自动回调 run 方法。
|
||||
* 常用于预置测试数据。
|
||||
*/
|
||||
@Bean
|
||||
CommandLineRunner initSampleData(StudentRepository repository) {
|
||||
return args -> {
|
||||
log.info("正在初始化示例数据...");
|
||||
|
||||
repository.save(new Student(null, "张三", 20, "zhangsan@mail.com", 85));
|
||||
repository.save(new Student(null, "李四", 22, "lisi@mail.com", 92));
|
||||
repository.save(new Student(null, "王五", 19, "wangwu@mail.com", 78));
|
||||
repository.save(new Student(null, "赵六", 21, "zhaoliu@mail.com", 88));
|
||||
repository.save(new Student(null, "孙七", 23, "sunqi@mail.com", 95));
|
||||
|
||||
log.info("预置 {} 条学生数据完成", repository.count());
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.learn.controller;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 第 3 天:第一个 Spring MVC 控制器
|
||||
*
|
||||
* @RestController = @Controller + @ResponseBody
|
||||
* 表示这个类中所有方法的返回值都直接写入 HTTP 响应体(JSON 格式)
|
||||
*
|
||||
* 知识点:
|
||||
* @GetMapping —— 处理 HTTP GET 请求
|
||||
* @PathVariable —— 从 URL 路径中提取变量(如 /hello/123)
|
||||
* @RequestParam —— 从 URL 查询参数中提取变量(如 /hello?name=xxx)
|
||||
*/
|
||||
@RestController
|
||||
public class HelloController {
|
||||
|
||||
/**
|
||||
* 最简接口 —— 访问 http://localhost:8080/hello
|
||||
*/
|
||||
@GetMapping("/hello")
|
||||
public String hello() {
|
||||
return "Hello, Spring Boot!";
|
||||
}
|
||||
|
||||
/**
|
||||
* 路径参数 —— 访问 http://localhost:8080/hello/你的名字
|
||||
*/
|
||||
@GetMapping("/hello/{name}")
|
||||
public String helloName(@PathVariable String name) {
|
||||
return "你好," + name + "!欢迎来到 Spring Boot 的世界。";
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询参数 —— 访问 http://localhost:8080/greet?name=张三&age=20
|
||||
*
|
||||
* @RequestParam 默认要求参数必传,可以设置 required=false 表示可选
|
||||
* defaultValue 设置默认值,当参数未传时使用
|
||||
*/
|
||||
@GetMapping("/greet")
|
||||
public String greet(
|
||||
@RequestParam(defaultValue = "同学") String name,
|
||||
@RequestParam(defaultValue = "0") int age) {
|
||||
|
||||
if (age > 0) {
|
||||
return String.format("你好,%s!你今年 %d 岁。", name, age);
|
||||
}
|
||||
return "你好," + name + "!";
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 JSON 对象 —— Spring Boot 自动将对象序列化为 JSON
|
||||
* 访问 http://localhost:8080/info
|
||||
*/
|
||||
@GetMapping("/info")
|
||||
public Object info() {
|
||||
return new Status("UP", "Spring Boot 学习项目运行中", System.currentTimeMillis());
|
||||
}
|
||||
|
||||
// 一个简单的内部类,用于返回 JSON
|
||||
record Status(String status, String message, long timestamp) {}
|
||||
}
|
||||
135
week2/src/main/java/com/learn/controller/StudentController.java
Normal file
135
week2/src/main/java/com/learn/controller/StudentController.java
Normal file
@@ -0,0 +1,135 @@
|
||||
package com.learn.controller;
|
||||
|
||||
import com.learn.model.Student;
|
||||
import com.learn.service.StudentService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 第 4-7 天:学生管理 RESTful API
|
||||
*
|
||||
* RESTful 设计原则:
|
||||
* GET /api/students → 查询全部
|
||||
* GET /api/students/{id} → 查询单个
|
||||
* POST /api/students → 新增
|
||||
* PUT /api/students/{id} → 更新
|
||||
* DELETE /api/students/{id} → 删除
|
||||
*
|
||||
* @RestController —— REST 控制器,返回 JSON
|
||||
* @RequestMapping —— 统一的前缀路径
|
||||
* @Valid —— 触发 Bean Validation 校验
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/students")
|
||||
public class StudentController {
|
||||
|
||||
private final StudentService service;
|
||||
|
||||
public StudentController(StudentService service) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
// ==================== 查询 ====================
|
||||
|
||||
/** GET /api/students —— 查询全部(支持按姓名搜索) */
|
||||
@GetMapping
|
||||
public ResponseEntity<Map<String, Object>> list(
|
||||
@RequestParam(required = false) String keyword) {
|
||||
|
||||
List<Student> students;
|
||||
if (keyword != null && !keyword.trim().isEmpty()) {
|
||||
students = service.search(keyword);
|
||||
} else {
|
||||
students = service.list();
|
||||
}
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("code", 200);
|
||||
result.put("total", students.size());
|
||||
result.put("data", students);
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
/** GET /api/students/{id} —— 根据 ID 查询单个 */
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<Map<String, Object>> getById(@PathVariable Long id) {
|
||||
return service.getById(id)
|
||||
.map(student -> {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("code", 200);
|
||||
result.put("data", student);
|
||||
return ResponseEntity.ok(result);
|
||||
})
|
||||
.orElseGet(() -> {
|
||||
Map<String, Object> error = new HashMap<>();
|
||||
error.put("code", 404);
|
||||
error.put("message", "学生不存在,ID: " + id);
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 新增 ====================
|
||||
|
||||
/** POST /api/students —— 新增学生 */
|
||||
@PostMapping
|
||||
public ResponseEntity<Map<String, Object>> add(@Valid @RequestBody Student student) {
|
||||
Student saved = service.add(student);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("code", 201);
|
||||
result.put("message", "添加成功");
|
||||
result.put("data", saved);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(result);
|
||||
}
|
||||
|
||||
// ==================== 更新 ====================
|
||||
|
||||
/** PUT /api/students/{id} —— 更新学生 */
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<Map<String, Object>> update(
|
||||
@PathVariable Long id,
|
||||
@Valid @RequestBody Student student) {
|
||||
|
||||
return service.update(id, student)
|
||||
.map(updated -> {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("code", 200);
|
||||
result.put("message", "更新成功");
|
||||
result.put("data", updated);
|
||||
return ResponseEntity.ok(result);
|
||||
})
|
||||
.orElseGet(() -> {
|
||||
Map<String, Object> error = new HashMap<>();
|
||||
error.put("code", 404);
|
||||
error.put("message", "学生不存在,ID: " + id);
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 删除 ====================
|
||||
|
||||
/** DELETE /api/students/{id} —— 删除学生 */
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Map<String, Object>> delete(@PathVariable Long id) {
|
||||
if (service.delete(id)) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("code", 200);
|
||||
result.put("message", "删除成功");
|
||||
return ResponseEntity.ok(result);
|
||||
} else {
|
||||
Map<String, Object> error = new HashMap<>();
|
||||
error.put("code", 404);
|
||||
error.put("message", "学生不存在,ID: " + id);
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
|
||||
}
|
||||
}
|
||||
|
||||
/** DELETE /api/students —— 清空全部(调试用) */
|
||||
// 注:这里简化处理,实际项目中应放在测试或管理接口中
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.learn.exception;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 第 7 天:全局异常处理
|
||||
*
|
||||
* @RestControllerAdvice 拦截所有 @RestController 中抛出的异常,
|
||||
* 统一返回友好的 JSON 格式错误信息,而不是默认的 500 错误页面。
|
||||
*
|
||||
* 好处:Controller 中不需要到处写 try-catch,代码更简洁。
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
/**
|
||||
* 处理参数校验失败异常(@Valid 触发)
|
||||
* 返回友好的字段错误信息
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleValidation(MethodArgumentNotValidException ex) {
|
||||
// 收集所有字段的校验错误
|
||||
String errors = ex.getBindingResult().getFieldErrors().stream()
|
||||
.map(e -> e.getField() + ": " + e.getDefaultMessage())
|
||||
.collect(Collectors.joining("; "));
|
||||
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("code", 400);
|
||||
body.put("message", "参数校验失败: " + errors);
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body);
|
||||
}
|
||||
|
||||
/**
|
||||
* 兜底:处理所有未捕获的异常
|
||||
*/
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<Map<String, Object>> handleAll(Exception ex) {
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("code", 500);
|
||||
body.put("message", "服务器内部错误: " + ex.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(body);
|
||||
}
|
||||
}
|
||||
164
week2/src/main/java/com/learn/ioc/SimpleIocDemo.java
Normal file
164
week2/src/main/java/com/learn/ioc/SimpleIocDemo.java
Normal file
@@ -0,0 +1,164 @@
|
||||
package com.learn.ioc;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 第 1 天:手写简易 IoC 容器 —— 理解控制反转的本质
|
||||
*
|
||||
* 核心概念:
|
||||
* IoC(Inversion of Control):把"创建对象"的控制权从程序员手里
|
||||
* 交给容器。以前是 new 一个对象,现在是容器帮我们创建并注入。
|
||||
*
|
||||
* DI(Dependency Injection):依赖注入,IoC 最常见的实现方式。
|
||||
* 当 A 类需要 B 类时,不需要 A 自己 new B,而是由容器把 B 注入到 A 中。
|
||||
*
|
||||
* 这个类是一个极简 IoC 容器实现,只依赖 JDK,用于理解 Spring 的核心思想。
|
||||
*
|
||||
* 运行方式:IDEA 中右键此类 → Run 'SimpleIocDemo.main()'
|
||||
*/
|
||||
public class SimpleIocDemo {
|
||||
|
||||
// ==================== 1. 定义两个简单的"业务类" ====================
|
||||
|
||||
/** 模拟一个"数据访问层"组件 */
|
||||
static class UserRepository {
|
||||
public String findUser() {
|
||||
return "张三(来自数据库)";
|
||||
}
|
||||
}
|
||||
|
||||
/** 模拟一个"业务逻辑层"组件 —— 它依赖 UserRepository */
|
||||
static class UserService {
|
||||
// UserService 需要 UserRepository,但自己不 new
|
||||
private UserRepository userRepository;
|
||||
|
||||
// 通过 setter 方法接收依赖(这就是"注入")
|
||||
public void setUserRepository(UserRepository userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
public String getUserInfo() {
|
||||
return userRepository.findUser();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 2. 极简 IoC 容器 ====================
|
||||
|
||||
/**
|
||||
* 一个玩具级 IoC 容器,实现两个能力:
|
||||
* 1. 注册 Bean(把对象放进容器)
|
||||
* 2. 注入依赖(帮对象建立关联关系)
|
||||
*/
|
||||
static class SimpleContainer {
|
||||
// 存储所有 Bean:名字 → 对象
|
||||
private final Map<String, Object> beans = new HashMap<>();
|
||||
|
||||
/** 注册一个 Bean */
|
||||
public void register(String name, Object bean) {
|
||||
beans.put(name, bean);
|
||||
}
|
||||
|
||||
/** 获取一个 Bean */
|
||||
public Object getBean(String name) {
|
||||
return beans.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动注入:扫描每个 Bean 中带 @Autowired 注解的字段,自动赋值
|
||||
* (这里用我们自定义的注解来模拟 Spring 的 @Autowired)
|
||||
*/
|
||||
public void autowire() {
|
||||
for (Object bean : beans.values()) {
|
||||
for (Field field : bean.getClass().getDeclaredFields()) {
|
||||
if (field.isAnnotationPresent(MyAutowired.class)) {
|
||||
field.setAccessible(true);
|
||||
try {
|
||||
// 根据字段类型找到匹配的 Bean
|
||||
Object dependency = findBeanByType(field.getType());
|
||||
if (dependency != null) {
|
||||
field.set(bean, dependency);
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Object findBeanByType(Class<?> type) {
|
||||
for (Object bean : beans.values()) {
|
||||
if (type.isAssignableFrom(bean.getClass())) {
|
||||
return bean;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 3. 自定义注解(模拟 Spring 的 @Autowired) ====================
|
||||
|
||||
@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
|
||||
@interface MyAutowired {
|
||||
}
|
||||
|
||||
// ==================== 4. 使用注解版本测试自动注入 ====================
|
||||
|
||||
static class OrderRepository {
|
||||
public String getOrder() {
|
||||
return "订单 #12345";
|
||||
}
|
||||
}
|
||||
|
||||
static class OrderService {
|
||||
@MyAutowired // 告诉容器:帮我把 OrderRepository 注入进来
|
||||
private OrderRepository orderRepository;
|
||||
|
||||
public String getOrderInfo() {
|
||||
return orderRepository.getOrder();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 5. 主方法——跑起来看效果 ====================
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("========== IoC 容器原理演示 ==========\n");
|
||||
|
||||
// --- 方式一:手动注入(模拟 XML 配置时代的做法) ---
|
||||
System.out.println("--- 方式一:手动 setter 注入 ---");
|
||||
SimpleContainer container1 = new SimpleContainer();
|
||||
|
||||
UserRepository userRepo = new UserRepository();
|
||||
UserService userService = new UserService();
|
||||
|
||||
container1.register("userRepository", userRepo);
|
||||
container1.register("userService", userService);
|
||||
|
||||
// 手动注入依赖
|
||||
userService.setUserRepository(userRepo);
|
||||
|
||||
UserService service1 = (UserService) container1.getBean("userService");
|
||||
System.out.println("调用结果: " + service1.getUserInfo());
|
||||
|
||||
// --- 方式二:自动注入(模拟注解时代的做法) ---
|
||||
System.out.println("\n--- 方式二:注解自动注入 ---");
|
||||
SimpleContainer container2 = new SimpleContainer();
|
||||
|
||||
container2.register("orderRepository", new OrderRepository());
|
||||
container2.register("orderService", new OrderService());
|
||||
|
||||
// 一行代码,自动完成所有依赖注入
|
||||
container2.autowire();
|
||||
|
||||
OrderService service2 = (OrderService) container2.getBean("orderService");
|
||||
System.out.println("调用结果: " + service2.getOrderInfo());
|
||||
|
||||
// --- 对比总结 ---
|
||||
System.out.println("\n========== 总结 ==========");
|
||||
System.out.println("没有 IoC 容器时:A a = new A(); a.setB(new B()); // 自己控制一切");
|
||||
System.out.println("有了 IoC 容器后:@Autowired B b; // 容器自动注入,你只管用");
|
||||
System.out.println("\n这就是 Spring 的核心理念——控制反转!");
|
||||
}
|
||||
}
|
||||
65
week2/src/main/java/com/learn/model/Student.java
Normal file
65
week2/src/main/java/com/learn/model/Student.java
Normal file
@@ -0,0 +1,65 @@
|
||||
package com.learn.model;
|
||||
|
||||
import jakarta.validation.constraints.*;
|
||||
|
||||
/**
|
||||
* 学生实体类 —— 第 4 天引入
|
||||
*
|
||||
* 用于接收请求参数和返回响应数据。
|
||||
* 添加了 Bean Validation 注解做参数校验(第 6 天深化)。
|
||||
*/
|
||||
public class Student {
|
||||
|
||||
private Long id; // 唯一标识
|
||||
|
||||
@NotBlank(message = "姓名不能为空")
|
||||
@Size(min = 1, max = 20, message = "姓名长度 1-20")
|
||||
private String name; // 姓名
|
||||
|
||||
@Min(value = 1, message = "年龄必须大于 0")
|
||||
@Max(value = 150, message = "年龄必须小于 150")
|
||||
private int age; // 年龄
|
||||
|
||||
@NotBlank(message = "邮箱不能为空")
|
||||
@Email(message = "邮箱格式不正确")
|
||||
private String email; // 邮箱
|
||||
|
||||
@Min(value = 0, message = "成绩不能为负数")
|
||||
@Max(value = 100, message = "成绩不能超过 100")
|
||||
private int score; // 成绩(0-100)
|
||||
|
||||
// ==================== 构造方法 ====================
|
||||
|
||||
public Student() {}
|
||||
|
||||
public Student(Long id, String name, int age, String email, int score) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
this.email = email;
|
||||
this.score = score;
|
||||
}
|
||||
|
||||
// ==================== Getter / Setter ====================
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
|
||||
public int getAge() { return age; }
|
||||
public void setAge(int age) { this.age = age; }
|
||||
|
||||
public String getEmail() { return email; }
|
||||
public void setEmail(String email) { this.email = email; }
|
||||
|
||||
public int getScore() { return score; }
|
||||
public void setScore(int score) { this.score = score; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("Student{id=%d, name='%s', age=%d, email='%s', score=%d}",
|
||||
id, name, age, email, score);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.learn.repository;
|
||||
|
||||
import com.learn.model.Student;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* 第 5 天:数据访问层 —— 学生仓库
|
||||
*
|
||||
* 负责数据的存储和查询。这里使用内存存储(ConcurrentHashMap),
|
||||
* 后续学习 JPA/MyBatis-Plus 时可以替换为真实数据库。
|
||||
*
|
||||
* @Repository 注解:标记这是数据访问层组件,被 Spring 扫描并管理
|
||||
*/
|
||||
@Repository
|
||||
public class StudentRepository {
|
||||
|
||||
// 线程安全的内存存储
|
||||
private final Map<Long, Student> store = new ConcurrentHashMap<>();
|
||||
// 自增 ID 生成器
|
||||
private final AtomicLong idGenerator = new AtomicLong(1);
|
||||
|
||||
/** 保存(新增或更新) */
|
||||
public Student save(Student student) {
|
||||
if (student.getId() == null) {
|
||||
// 新增:分配 ID
|
||||
student.setId(idGenerator.getAndIncrement());
|
||||
}
|
||||
store.put(student.getId(), student);
|
||||
return student;
|
||||
}
|
||||
|
||||
/** 根据 ID 查找 */
|
||||
public Optional<Student> findById(Long id) {
|
||||
return Optional.ofNullable(store.get(id));
|
||||
}
|
||||
|
||||
/** 查找全部 */
|
||||
public List<Student> findAll() {
|
||||
return new ArrayList<>(store.values());
|
||||
}
|
||||
|
||||
/** 根据姓名模糊搜索 */
|
||||
public List<Student> findByName(String keyword) {
|
||||
return store.values().stream()
|
||||
.filter(s -> s.getName().contains(keyword))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
public boolean deleteById(Long id) {
|
||||
return store.remove(id) != null;
|
||||
}
|
||||
|
||||
/** 总数 */
|
||||
public long count() {
|
||||
return store.size();
|
||||
}
|
||||
|
||||
/** 清空(用于测试) */
|
||||
public void clear() {
|
||||
store.clear();
|
||||
idGenerator.set(1);
|
||||
}
|
||||
}
|
||||
76
week2/src/main/java/com/learn/service/StudentService.java
Normal file
76
week2/src/main/java/com/learn/service/StudentService.java
Normal file
@@ -0,0 +1,76 @@
|
||||
package com.learn.service;
|
||||
|
||||
import com.learn.model.Student;
|
||||
import com.learn.repository.StudentRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 第 5 天:业务逻辑层 —— 学生服务
|
||||
*
|
||||
* 负责业务规则的校验和处理,介于 Controller 和 Repository 之间。
|
||||
* Controller 只负责接收请求和返回响应,业务逻辑全部在 Service 中。
|
||||
*
|
||||
* @Service 注解:标记这是业务逻辑层组件,被 Spring 扫描并管理
|
||||
*/
|
||||
@Service
|
||||
public class StudentService {
|
||||
|
||||
private final StudentRepository repository;
|
||||
|
||||
/**
|
||||
* 构造方法注入(推荐方式,比 @Autowired 字段注入更安全,也更容易测试)
|
||||
* Spring 会自动从容器中找到 StudentRepository 并传入
|
||||
*/
|
||||
public StudentService(StudentRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
/** 新增学生 */
|
||||
public Student add(Student student) {
|
||||
// 业务校验可以放在这里,例如检查邮箱是否已注册等
|
||||
return repository.save(student);
|
||||
}
|
||||
|
||||
/** 查询全部 */
|
||||
public List<Student> list() {
|
||||
return repository.findAll();
|
||||
}
|
||||
|
||||
/** 根据 ID 查询 */
|
||||
public Optional<Student> getById(Long id) {
|
||||
return repository.findById(id);
|
||||
}
|
||||
|
||||
/** 模糊搜索 */
|
||||
public List<Student> search(String keyword) {
|
||||
if (keyword == null || keyword.trim().isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return repository.findByName(keyword);
|
||||
}
|
||||
|
||||
/** 更新学生 */
|
||||
public Optional<Student> update(Long id, Student updated) {
|
||||
return repository.findById(id).map(existing -> {
|
||||
// 只更新非 null 的字段
|
||||
if (updated.getName() != null) existing.setName(updated.getName());
|
||||
if (updated.getAge() > 0) existing.setAge(updated.getAge());
|
||||
if (updated.getEmail() != null) existing.setEmail(updated.getEmail());
|
||||
if (updated.getScore() >= 0) existing.setScore(updated.getScore());
|
||||
return repository.save(existing);
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除学生 */
|
||||
public boolean delete(Long id) {
|
||||
return repository.deleteById(id);
|
||||
}
|
||||
|
||||
/** 总数 */
|
||||
public long count() {
|
||||
return repository.count();
|
||||
}
|
||||
}
|
||||
15
week2/src/main/resources/application-dev.yml
Normal file
15
week2/src/main/resources/application-dev.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
# ==========================================
|
||||
# 开发环境配置
|
||||
# ==========================================
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.learn: DEBUG # 开发时打印详细日志
|
||||
|
||||
# 自定义配置(演示 @Value 注解的使用)
|
||||
app:
|
||||
name: Week2 学生管理系统
|
||||
version: 1.0.0-DEV
|
||||
description: 开发环境,启用 DEBUG 日志和热重载
|
||||
15
week2/src/main/resources/application-prod.yml
Normal file
15
week2/src/main/resources/application-prod.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
# ==========================================
|
||||
# 生产环境配置
|
||||
# ==========================================
|
||||
server:
|
||||
port: 80
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.learn: WARN # 生产环境只打印警告和错误
|
||||
org.springframework: WARN
|
||||
|
||||
app:
|
||||
name: Week2 学生管理系统
|
||||
version: 1.0.0-RELEASE
|
||||
description: 生产环境,仅记录 WARN 级别日志
|
||||
24
week2/src/main/resources/application.yml
Normal file
24
week2/src/main/resources/application.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
# ==========================================
|
||||
# Spring Boot 核心配置文件(第 6 天重点)
|
||||
# ==========================================
|
||||
|
||||
# 应用基本信息
|
||||
spring:
|
||||
application:
|
||||
name: week2-spring-boot
|
||||
|
||||
# 激活 dev 环境配置
|
||||
profiles:
|
||||
active: dev
|
||||
|
||||
# 内嵌 Tomcat 端口和上下文路径
|
||||
server:
|
||||
port: 8080
|
||||
servlet:
|
||||
context-path: /
|
||||
|
||||
# 日志级别(开发环境建议 DEBUG)
|
||||
logging:
|
||||
level:
|
||||
com.learn: DEBUG
|
||||
org.springframework.web: INFO
|
||||
55
week2/src/test/java/com/learn/StudentControllerTest.java
Normal file
55
week2/src/test/java/com/learn/StudentControllerTest.java
Normal file
@@ -0,0 +1,55 @@
|
||||
package com.learn;
|
||||
|
||||
import com.learn.model.Student;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.web.client.TestRestTemplate;
|
||||
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||
import org.springframework.http.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* 第 8 周才正式学习测试,这里先体验一下集成测试的基本写法。
|
||||
*
|
||||
* @SpringBootTest(webEnvironment = ...RANDOM_PORT) 启动一个真实的 Web 容器
|
||||
* TestRestTemplate 是 Spring Boot 提供的 HTTP 测试客户端
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
class StudentControllerTest {
|
||||
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
|
||||
@Autowired
|
||||
private TestRestTemplate restTemplate;
|
||||
|
||||
private String baseUrl() {
|
||||
return "http://localhost:" + port + "/api/students";
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
void shouldListStudents() {
|
||||
ResponseEntity<Map> response = restTemplate.getForEntity(baseUrl(), Map.class);
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
assertThat((Integer) response.getBody().get("code")).isEqualTo(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
void shouldAddStudent() {
|
||||
Student s = new Student(null, "测试学生", 18, "test@mail.com", 90);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
HttpEntity<Student> request = new HttpEntity<>(s, headers);
|
||||
|
||||
ResponseEntity<Map> response = restTemplate.postForEntity(baseUrl(), request, Map.class);
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
|
||||
}
|
||||
}
|
||||
467
week2/教案.md
Normal file
467
week2/教案.md
Normal file
@@ -0,0 +1,467 @@
|
||||
# 第二阶段教案: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 集成都是在这个地基上添砖加瓦。
|
||||
Reference in New Issue
Block a user