Appearance
第58课:Spring Boot Web 开发
🎯 学习目标
- 掌握 RESTful API 设计规范
- 掌握参数接收的各种方式
- 掌握参数校验
- 实现统一响应格式
- 实现全局异常处理
- 掌握跨域配置
📖 一、RESTful API 设计
1. RESTful 规范
| HTTP 方法 | 操作 | 示例 | 说明 |
|---|---|---|---|
| GET | 查询 | GET /users | 查询所有用户 |
| GET | 查询 | GET /users/1 | 查询ID为1的用户 |
| POST | 创建 | POST /users | 创建新用户 |
| PUT | 更新 | PUT /users/1 | 完全更新用户1 |
| PATCH | 更新 | PATCH /users/1 | 部分更新用户1 |
| DELETE | 删除 | DELETE /users/1 | 删除用户1 |
2. 完整的 RESTful Controller
java
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
// 查询所有用户:GET /api/users
@GetMapping
public List<User> getAllUsers() {
return userService.findAll();
}
// 分页查询:GET /api/users?page=1&size=10
@GetMapping
public Page<User> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size
) {
return userService.findAll(PageRequest.of(page, size));
}
// 根据ID查询:GET /api/users/1
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User not found"));
}
// 创建用户:POST /api/users
@PostMapping
public User createUser(@RequestBody @Valid User user) {
return userService.save(user);
}
// 更新用户:PUT /api/users/1
@PutMapping("/{id}")
public User updateUser(@PathVariable Long id, @RequestBody @Valid User user) {
return userService.update(id, user);
}
// 部分更新:PATCH /api/users/1
@PatchMapping("/{id}")
public User patchUser(@PathVariable Long id, @RequestBody Map<String, Object> updates) {
return userService.patch(id, updates);
}
// 删除用户:DELETE /api/users/1
@DeleteMapping("/{id}")
public void deleteUser(@PathVariable Long id) {
userService.deleteById(id);
}
// 搜索用户:GET /api/users/search?keyword=Alice
@GetMapping("/search")
public List<User> searchUsers(@RequestParam String keyword) {
return userService.search(keyword);
}
}📖 二、参数接收
1. @RequestParam - 查询参数
java
// GET /api/users?name=Alice&age=25
@GetMapping("/users")
public String getUser(
@RequestParam String name, // 必填
@RequestParam(required = false) Integer age, // 可选
@RequestParam(defaultValue = "0") int page // 默认值
) {
return "name=" + name + ", age=" + age + ", page=" + page;
}
// 接收多个同名参数:GET /api/users?id=1&id=2&id=3
@GetMapping("/users")
public List<User> getUsers(@RequestParam List<Long> id) {
return userService.findByIds(id);
}2. @PathVariable - 路径变量
java
// GET /api/users/123
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id);
}
// 多个路径变量:GET /api/posts/5/comments/10
@GetMapping("/posts/{postId}/comments/{commentId}")
public Comment getComment(
@PathVariable Long postId,
@PathVariable Long commentId
) {
return commentService.find(postId, commentId);
}
// 正则约束:GET /api/users/123(只接受数字)
@GetMapping("/users/{id:[0-9]+}")
public User getUser(@PathVariable Long id) {
return userService.findById(id);
}3. @RequestBody - 请求体
java
// POST /api/users
// Content-Type: application/json
// Body: {"name":"Alice","age":25}
@PostMapping("/users")
public User createUser(@RequestBody User user) {
return userService.save(user);
}
// 使用 DTO 接收
@PostMapping("/users")
public User createUser(@RequestBody UserDTO dto) {
User user = new User();
BeanUtils.copyProperties(dto, user);
return userService.save(user);
}4. @RequestHeader - 请求头
java
@GetMapping("/info")
public String getInfo(
@RequestHeader("User-Agent") String userAgent,
@RequestHeader(value = "Authorization", required = false) String token
) {
return "User-Agent: " + userAgent;
}5. @CookieValue - Cookie
java
@GetMapping("/welcome")
public String welcome(@CookieValue("sessionId") String sessionId) {
return "Session ID: " + sessionId;
}6. 对象接收(自动绑定)
java
// GET /api/search?name=Alice&age=25&email=alice@example.com
@GetMapping("/search")
public List<User> search(UserSearchDTO dto) {
// Spring 自动将参数绑定到 DTO 的属性
return userService.search(dto);
}
@Data
public class UserSearchDTO {
private String name;
private Integer age;
private String email;
}📖 三、参数校验
1. 添加依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>2. 实体类添加校验注解
java
import jakarta.validation.constraints.*;
@Data
public class UserDTO {
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 20, message = "用户名长度必须在3-20之间")
private String username;
@NotBlank(message = "密码不能为空")
@Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{8,}$",
message = "密码必须至少8位,包含大小写字母和数字")
private String password;
@Email(message = "邮箱格式不正确")
private String email;
@Min(value = 0, message = "年龄不能小于0")
@Max(value = 150, message = "年龄不能大于150")
private Integer age;
@NotNull(message = "性别不能为空")
private Gender gender;
@Past(message = "生日必须是过去的日期")
private LocalDate birthday;
@DecimalMin(value = "0.0", message = "金额不能为负")
private BigDecimal amount;
@Size(max = 11, message = "手机号长度不能超过11位")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
}3. Controller 使用 @Valid
java
@RestController
@RequestMapping("/api/users")
public class UserController {
// @Valid 触发校验
@PostMapping
public User createUser(@RequestBody @Valid UserDTO dto) {
return userService.create(dto);
}
// @Validated 支持分组校验
@PutMapping("/{id}")
public User updateUser(
@PathVariable Long id,
@RequestBody @Validated(Update.class) UserDTO dto
) {
return userService.update(id, dto);
}
}4. 常用校验注解
| 注解 | 说明 | 示例 |
|---|---|---|
| @NotNull | 不能为 null | @NotNull private String name; |
| @NotEmpty | 不能为 null 且长度>0 | @NotEmpty private List<String> tags; |
| @NotBlank | 不能为 null 且去除空格后长度>0 | @NotBlank private String username; |
| @Size | 长度范围 | @Size(min=3, max=20) private String name; |
| @Min/@Max | 数值范围 | @Min(0) @Max(150) private Integer age; |
| 邮箱格式 | @Email private String email; | |
| @Pattern | 正则匹配 | @Pattern(regexp="^1[3-9]\d{9}$") private String phone; |
| @Past/@Future | 过去/将来的日期 | @Past private LocalDate birthday; |
| @AssertTrue/@AssertFalse | 布尔值 | @AssertTrue private boolean agreed; |
5. 自定义校验注解
java
// 1. 定义注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
public @interface Phone {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// 2. 实现校验器
public class PhoneValidator implements ConstraintValidator<Phone, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null || value.isEmpty()) {
return true; // 空值由 @NotBlank 处理
}
return value.matches("^1[3-9]\\d{9}$");
}
}
// 3. 使用
@Data
public class UserDTO {
@Phone
private String phone;
}📖 四、统一响应格式
1. 响应结果类
java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result<T> {
private Integer code; // 状态码
private String message; // 消息
private T data; // 数据
private Long timestamp; // 时间戳
// 成功响应
public static <T> Result<T> success(T data) {
return new Result<>(200, "success", data, System.currentTimeMillis());
}
public static <T> Result<T> success() {
return success(null);
}
// 失败响应
public static <T> Result<T> error(Integer code, String message) {
return new Result<>(code, message, null, System.currentTimeMillis());
}
public static <T> Result<T> error(String message) {
return error(500, message);
}
}2. Controller 使用
java
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public Result<User> getUser(@PathVariable Long id) {
User user = userService.findById(id);
return Result.success(user);
}
@PostMapping
public Result<User> createUser(@RequestBody @Valid UserDTO dto) {
User user = userService.create(dto);
return Result.success(user);
}
@DeleteMapping("/{id}")
public Result<Void> deleteUser(@PathVariable Long id) {
userService.deleteById(id);
return Result.success();
}
}3. 响应示例
json
// 成功响应
{
"code": 200,
"message": "success",
"data": {
"id": 1,
"username": "alice",
"email": "alice@example.com"
},
"timestamp": 1701234567890
}
// 失败响应
{
"code": 400,
"message": "用户名已存在",
"data": null,
"timestamp": 1701234567890
}📖 五、全局异常处理
1. 自定义异常
java
// 业务异常
public class BusinessException extends RuntimeException {
private Integer code;
public BusinessException(String message) {
super(message);
this.code = 400;
}
public BusinessException(Integer code, String message) {
super(message);
this.code = code;
}
public Integer getCode() {
return code;
}
}
// 资源未找到异常
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}2. 全局异常处理器
java
@RestControllerAdvice
public class GlobalExceptionHandler {
// 处理业务异常
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e) {
return Result.error(e.getCode(), e.getMessage());
}
// 处理资源未找到
@ExceptionHandler(ResourceNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Result<Void> handleResourceNotFoundException(ResourceNotFoundException e) {
return Result.error(404, e.getMessage());
}
// 处理参数校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Map<String, String>> handleValidationException(
MethodArgumentNotValidException e
) {
Map<String, String> errors = new HashMap<>();
e.getBindingResult().getFieldErrors().forEach(error -> {
errors.put(error.getField(), error.getDefaultMessage());
});
return Result.error(400, "参数校验失败").setData(errors);
}
// 处理约束违反异常
@ExceptionHandler(ConstraintViolationException.class)
public Result<Void> handleConstraintViolationException(
ConstraintViolationException e
) {
return Result.error(400, e.getMessage());
}
// 处理所有未捕获的异常
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
log.error("系统异常:", e);
return Result.error(500, "系统繁忙,请稍后再试");
}
}3. 使用示例
java
@Service
public class UserService {
public User findById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("用户不存在"));
}
public User create(UserDTO dto) {
if (userRepository.existsByUsername(dto.getUsername())) {
throw new BusinessException("用户名已存在");
}
// ...
}
}📖 六、跨域配置
方式1:全局配置
java
@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 所有路径
.allowedOrigins("http://localhost:3000") // 允许的源
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*") // 允许的请求头
.allowCredentials(true) // 允许携带凭证
.maxAge(3600); // 预检请求缓存时间
}
};
}
}方式2:注解配置
java
@RestController
@RequestMapping("/api/users")
@CrossOrigin(origins = "http://localhost:3000") // 允许该源跨域
public class UserController {
// ...
}📖 七、文件上传下载
1. 文件上传
java
@RestController
@RequestMapping("/api/files")
public class FileController {
@Value("${file.upload.path}")
private String uploadPath;
// 单文件上传
@PostMapping("/upload")
public Result<String> upload(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
throw new BusinessException("文件不能为空");
}
// 生成唯一文件名
String originalFilename = file.getOriginalFilename();
String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
String filename = UUID.randomUUID().toString() + extension;
// 保存文件
Path path = Paths.get(uploadPath, filename);
Files.createDirectories(path.getParent());
file.transferTo(path.toFile());
return Result.success(filename);
}
// 多文件上传
@PostMapping("/uploads")
public Result<List<String>> uploads(@RequestParam("files") MultipartFile[] files) {
List<String> filenames = new ArrayList<>();
for (MultipartFile file : files) {
// 保存文件...
filenames.add(filename);
}
return Result.success(filenames);
}
}2. 文件下载
java
@GetMapping("/download/{filename}")
public ResponseEntity<Resource> download(@PathVariable String filename) {
Path path = Paths.get(uploadPath, filename);
Resource resource = new UrlResource(path.toUri());
if (!resource.exists()) {
throw new ResourceNotFoundException("文件不存在");
}
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + filename + "\"")
.body(resource);
}3. 配置文件大小限制
yaml
spring:
servlet:
multipart:
max-file-size: 10MB # 单文件最大大小
max-request-size: 50MB # 请求最大大小💡 最佳实践
- 使用 DTO 模式(Controller 不直接使用 Entity)
- 统一响应格式(便于前端处理)
- 全局异常处理(避免代码中到处 try-catch)
- 参数校验(使用 @Valid,不要手动校验)
- RESTful 规范(资源命名用复数,动词用 HTTP 方法)
⚠️ 常见陷阱
1. Controller 直接返回 Entity
Entity 可能包含密码、内部状态、懒加载关联和数据库字段,直接返回会造成安全和维护问题。
2. 统一响应滥用
并不是所有响应都必须强行包一层。文件下载、流式响应、健康检查等场景可以返回原生 ResponseEntity。
3. 异常处理吞掉真实错误
全局异常处理应该记录未知异常堆栈,但返回给前端时隐藏内部细节。
4. 文件上传信任原始文件名
MultipartFile.getOriginalFilename() 来自客户端,不能直接作为服务器保存路径,必须重命名并防路径穿越。
5. RESTful 只学 URL,不学语义
RESTful 不只是路径写成 /users,还包括正确使用 HTTP 方法、状态码、幂等性和资源建模。
🆚 Java vs C 对比
| 维度 | C Web 服务 | Spring Boot Web |
|---|---|---|
| 路由 | 手动注册或框架路由 | 注解映射 |
| 请求体解析 | 手动调用 JSON 库 | @RequestBody 自动绑定 |
| 参数校验 | 手写 if | Bean Validation |
| 错误响应 | 手动组织 | @RestControllerAdvice |
| 文件上传 | 手动解析 multipart | MultipartFile |
| API 风格 | 取决于框架 | REST Controller 标准化 |
Spring Boot Web 的优势是把 HTTP、JSON、校验、异常、文件上传等常见 Web 能力集成到统一开发模型中。
📝 练习
完成 练习/Ex58_SpringBoot_Web.java:
- 实现完整的 CRUD API
- 参数校验
- 统一响应和异常处理
- 文件上传下载
- 分页查询
- 综合:博客系统 API
🎓 下一步
- 第59课:Spring Data JPA - 数据库访问、关系映射