5
Web 开发:Controller、参数绑定、全局异常
Spring Web MVC
Spring Boot 的 Web 层就是 Spring MVC。你写一个 @RestController,方法上贴 @GetMapping/@PostMapping,Spring 自动帮你把 HTTP 请求翻译成 Java 方法调用,再把返回值序列化成 JSON。
四种常见参数绑定注解
| 注解 | 从哪取值 |
@RequestParam | URL 查询参数,如 /user?id=1 里的 id。 |
@PathVariable | 路径变量,如 /user/{id} 里的 id。 |
@RequestBody | 请求体 JSON,反序列化成 Java 对象。 |
@RequestHeader | 请求头,如 Authorization。 |
一个完整的 REST Controller
package com.example.demo.web;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/tasks")
public class TaskController {
// GET /api/tasks?status=open
@GetMapping
public List<TaskDTO> list(@RequestParam(name = "status", required = false) String status) {
// 查询逻辑...
return List.of();
}
// GET /api/tasks/42
@GetMapping("/{id}")
public TaskDTO getOne(@PathVariable("id") long id) {
return new TaskDTO(id, "写文档", false);
}
// POST /api/tasks,请求体是 JSON
@PostMapping
public TaskDTO create(@RequestBody TaskDTO body) {
// 保存逻辑...
return body;
}
}
全局异常处理:@ControllerAdvice
别在每个 Controller 方法里 try-catch。把所有异常处理集中到一个类里,统一返回 JSON 格式的错误响应。
全局异常处理器
package com.example.demo.web;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
// 业务异常 → 400
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, Object> handleBadArg(IllegalArgumentException e) {
return Map.of("code", 400, "message", e.getMessage());
}
// 兜底:其他所有异常 → 500
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Map<String, Object> handleOther(Exception e) {
return Map.of("code", 500, "message", "服务器开小差了");
}
}
参数校验:@Valid + @Validated 分组
请求体里的字段要不要非空、长度多少、范围多少,别自己 if 检查。加 spring-boot-starter-validation,在 DTO 字段上贴注解,Controller 参数加 @Valid,非法请求自动抛异常交给全局处理器。
// DTO:字段上贴约束注解
public class UserDTO {
@NotBlank(message = "用户名不能为空")
private String username;
@Min(value = 18, message = "必须成年")
private int age;
}
@RestController
public class UserController {
// @Valid 触发校验:username 空 / age 不满 18,自动 400
@PostMapping("/user")
public Map<?,?> create(@Valid @RequestBody UserDTO dto) {
return Map.of("ok", true);
}
}
// @Validated 分组:同一个 DTO,新增和更新校验规则不同
public class UserDTO {
@Null(groups = Create.class) // 新增时 id 必须为空
@NotNull(groups = Update.class) // 更新时 id 必须有值
private Long id;
@NotBlank
private String username;
}
// Controller 里指定走哪一组:@Validated(Update.class)
@PutMapping("/user")
public Map<?,?> update(@Validated(Update.class) @RequestBody UserDTO dto) { ... }
论@Valid 和 @Validated 区别
@Valid 是标准 JSR 注解,不支持分组;@Validated 是 Spring 增强版,多了分组能力。单个 DTO 一套规则用 @Valid;同一 DTO 在新增/更新时有不同规则,用 @Validated 分组。校验失败抛 MethodArgumentNotValidException,在全局异常处理器里接住返回友好提示。
拦截器:在 Controller 前后插一脚
拦截器(HandlerInterceptor)和 AOP 类似,但它只拦 Web 请求。登录校验、日志记录、跨域处理,都适合用拦截器。比 Filter 更靠近 Controller,能拿到 Handler 方法信息。
写一个登录校验拦截器
package com.example.demo.web;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler)
throws Exception {
String token = req.getHeader("Authorization");
if (token == null || token.isBlank()) {
resp.setStatus(401);
return false; // 不放行,Controller 不会执行
}
return true;
}
}
注册拦截器:配置类实现 WebMvcConfigurer
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthInterceptor())
.addPathPatterns("/api/**") // 拦 /api/ 开头的
.excludePathPatterns("/api/login"); // 放行登录接口
}
}
跨域 CORS:前后端分离必备
前端跑在 5173(Vite),后端跑在 8080,浏览器会因为同源策略拦截请求。在 Web 配置里加 CORS 规则就行。
全局 CORS 配置
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**") // 对 /api/ 开头的路径生效
.allowedOrigins("http://localhost:5173") // 允许前端地址
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
文件上传:MultipartFile
上传文件在 Controller 方法参数里用 MultipartFile 接住,Spring 自动处理 multipart/form-data。application.yml 里可以限制大小。
上传文件接口
@PostMapping("/upload")
public Map<String, Object> upload(@RequestParam("file") MultipartFile file) throws IOException {
String filename = file.getOriginalFilename();
Path dest = Paths.get("/tmp/uploads/" + filename);
Files.copy(file.getInputStream(), dest, StandardCopyOption.REPLACE_EXISTING);
return Map.of("filename", filename, "size", file.getSize());
}
application.yml 限制上传大小
spring:
servlet:
multipart:
max-file-size: 10MB # 单个文件最大 10MB
max-request-size: 50MB # 整个请求最大 50MB
静态资源:CSS、JS、图片放哪
Spring Boot 自动把 classpath:/static/(或 public/、resources/、META-INF/resources/)下的文件当静态资源服务。把 index.html 放进 static/,启动后直接访问 http://localhost:8080/index.html。
DTO 上贴校验注解
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public class TaskDTO {
private long id;
@NotBlank(message = "标题不能为空")
@Size(max = 100, message = "标题最长 100 字")
private String title;
private boolean done;
// 省略 getter / setter
}
// Controller 参数加 @Valid,校验失败自动 MethodArgumentNotValidException
@PostMapping
public TaskDTO create(@Valid @RequestBody TaskDTO body) { ... }
完整 RESTful API:统一响应 Result<T> + 全套 CRUD
生产项目里,所有接口都返回同一个 JSON 外壳——code、message、data。前端不用针对每个接口写不同解析。下面是一套能直接拿去用的完整写法。
第一步:统一响应封装 Result<T>
package com.example.demo.common;
public record Result<T>(int code, String message, T data) {
// 静态工厂方法,调用方写起来更爽
public static <T> Result<T> ok(T data) {
return new Result<>(200, "success", data);
}
public static <T> Result<T> fail(int code, String msg) {
return new Result<>(code, msg, null);
}
}
第二步:DTO 加校验注解 + 完整 CRUD Controller
package com.example.demo.web;
import jakarta.validation.constraints.NotBlank;
import org.springframework.web.bind.annotation.*;
import java.util.List;
// 请求 DTO:title 不能为空,否则校验失败
public record TaskRequest(@NotBlank(message = "标题不能为空") String title) {}
@RestController
@RequestMapping("/api/tasks")
public class TaskController {
private final TaskService taskService;
public TaskController(TaskService taskService) { this.taskService = taskService; }
// GET /api/tasks 列表
@GetMapping
public Result<List<Task>> list() {
return Result.ok(taskService.findAll());
}
// GET /api/tasks/1 按 id 查
@GetMapping("/{id}")
public Result<Task> getOne(@PathVariable long id) {
return Result.ok(taskService.findById(id));
}
// POST /api/tasks 新增(@Valid 触发校验)
@PostMapping
public Result<Task> create(@Valid @RequestBody TaskRequest req) {
return Result.ok(taskService.create(req.title()));
}
// PUT /api/tasks/1 更新
@PutMapping("/{id}")
public Result<Task> update(@PathVariable long id, @Valid @RequestBody TaskRequest req) {
return Result.ok(taskService.update(id, req.title()));
}
// DELETE /api/tasks/1 删除
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable long id) {
taskService.delete(id);
return Result.ok(null);
}
}
第三步:全局异常统一返回 Result(而不是默认 HTML 错误页)
@RestControllerAdvice
public class GlobalExceptionHandler {
// 参数校验失败:把校验信息拼成友好提示
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValid(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldError().getDefaultMessage();
return Result.fail(400, msg);
}
// 兜底
@ExceptionHandler(Exception.class)
public Result<Void> handleOther(Exception e) {
return Result.fail(500, "服务器开小差了");
}
}
Postman 实测输出:
# 1) 正常新增 POST /api/tasks body={"title":"学 Spring Boot"}
HTTP 200
{"code":200,"message":"success","data":{"id":1,"title":"学 Spring Boot","done":false}}
# 2) 校验失败 POST /api/tasks body={"title":""}
HTTP 200(业务码是 400,前端看 code 判断)
{"code":400,"message":"标题不能为空","data":null}
# 3) 查询 GET /api/tasks/1
{"code":200,"message":"success","data":{"id":1,"title":"学 Spring Boot","done":false}}
# 统一外壳的好处:前端永远先看 code,code==200 取 data,否则弹 message。
本章面试题 · 第 5 章(Web 开发)
1.(概念题)@RequestBody 和 @RequestParam 有什么区别?分别用在什么请求上?
查看答案
答案:@RequestParam 取 URL 查询参数(/user?id=1),GET 请求常用;@RequestBody 取请求体 JSON,反序列化成对象,POST/PUT 常用。解析:GET 不该用 @RequestBody(HTTP 规范上 GET 没有请求体),复杂查询参数用对象接收。
2.(理解题)@ControllerAdvice 是怎么做到"全局"接住所有异常的?
查看答案
答案:它本质是一个特殊的 @Component,容器启动时扫描到后,Spring MVC 把它注册到异常解析链里。任何 Controller 抛出的异常都会先过这套处理器,匹配到 @ExceptionHandler 就用它返回。解析:@RestControllerAdvice = @ControllerAdvice + @ResponseBody,返回值自动序列化成 JSON。
3.(排错题)POST 一个 JSON 进来,Controller 拿到的对象全是 null。为什么?
查看答案
答案:八成是参数少了 @RequestBody——不加它,Spring 把参数当成普通表单/查询参数来绑,JSON body 自然绑不上。解析:另外检查请求头 Content-Type 是不是 application/json,Jackson 才会触发反序列化。