21_Java Spring Boot API
Spring Boot 3 控制器、请求校验、分层、事务、数据库、安全和测试边界。
Java Spring Boot API
学习目标:把 Java 核心知识组合成带输入校验、业务层、持久化和测试边界的 HTTP 服务。
1. 最小控制器
下面示例基于 Spring Boot 3;需要 Web 和 Validation starter。数据暂存内存,只用于演示请求与响应,不适合作为生产存储。
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
@RestController
@RequestMapping("/notes")
public class NoteController {
private final Map<Long, Note> notes = new ConcurrentHashMap<>();
private final AtomicLong ids = new AtomicLong();
public record CreateNote(@NotBlank String title) {}
public record Note(long id, String title) {}
@PostMapping
public ResponseEntity<Note> create(@Valid @RequestBody CreateNote input) {
long id = ids.incrementAndGet();
Note note = new Note(id, input.title());
notes.put(id, note);
return ResponseEntity.status(HttpStatus.CREATED).body(note);
}
@GetMapping("/{id}")
public Note get(@PathVariable long id) {
Note note = notes.get(id);
if (note == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
return note;
}
}
@Valid 在输入边界校验请求;record 适合简单 DTO。数据库字段约束仍需要在数据库层建立,不能只靠请求校验。内存示例的 ID 和数据在重启后丢失,多实例之间也不会共享。
2. 从示例走向分层
Controller → Service → Repository → Database
- Controller 负责 HTTP 协议、验证与错误映射。
- Service 负责业务规则和事务边界;跨多个写操作的用例应明确
@Transactional的回滚规则。 - Repository 负责查询;可选 Spring Data JDBC/JPA 或直接 SQL,但仍要理解索引、连接池和 N+1 查询。
- 数据库结构迁移用 Flyway/Liquibase 等工具版本化,不依赖应用启动时静默重建表。
3. 安全、测试与交付
认证后仍需逐资源授权:用户 A 不能通过换一个 ID 读取用户 B 的笔记。为外部调用配置超时;不要把密钥放在 application.yml 提交到仓库。Web 层测试覆盖 201、400、404、401/403 等状态,数据库集成测试验证约束与事务。
下一步按 综合项目 添加真实数据库、鉴权、迁移、CI 与部署。
4. 从 HTTP 请求到数据库事务
一个创建请求依次经过参数解析、输入校验、认证、业务规则和持久化。Controller 不应直接承担所有步骤:它把 DTO 交给 Service,Service 检查“当前用户是否可创建”并调用 Repository。跨多条写入的一次业务动作通常在 Service 方法上建立事务边界;在事务里调用外部 HTTP 服务可能长期占用数据库连接,应设计独立的交付或补偿流程。
POST /notes
→ CreateNoteRequest 校验
→ NoteService.create(currentUser, request)
→ NoteRepository.insert(...) 与审计记录(同一事务)
→ 201 Created + NoteResponse
数据库约束是最后一道一致性边界:NOT NULL、唯一约束和外键不能只靠 @NotBlank 替代。JPA 实体也不宜直接作为公开响应,避免把内部字段或懒加载关系意外序列化。
5. 授权与错误映射
认证解决“是谁”,授权解决“能否访问这条笔记”。查询条件可以带上 owner_id,如 findByIdAndOwnerId;只根据 ID 查出对象后信任前端隐藏按钮会发生越权。404 与 403 的选择应由 API 契约统一,避免通过不同响应泄露资源是否存在。
使用统一异常处理把可预期的业务错误映射为稳定结构,例如 { "code": "note_not_found", "message": "笔记不存在" };内部数据库异常记录在服务端,响应使用通用 500。对重复请求、版本冲突和无效参数分别设计测试,避免全部返回 500。
6. 可执行的分层练习与参考
- 把本页内存版 Controller 改为 Controller → Service → Repository 三层,并保留原 API 契约。
- 添加数据库迁移,创建
users、notes和audit;给notes(owner_id, id)建合理索引。 - 写测试覆盖 201、400、404、未登录和用户 A 越权访问用户 B 的资源。
- 运行打包后的应用,验证重启后数据仍在,迁移在空数据库能成功执行。