楼层: 首页/ 软件技术/ Spring Boot 3.5/ Web 开发:Controller、参数绑定、全局异常
5

Web 开发:Controller、参数绑定、全局异常

Spring Web MVC

Spring Boot 的 Web 层就是 Spring MVC。你写一个 @RestController,方法上贴 @GetMapping/@PostMapping,Spring 自动帮你把 HTTP 请求翻译成 Java 方法调用,再把返回值序列化成 JSON。

四种常见参数绑定注解

注解从哪取值
@RequestParamURL 查询参数,如 /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 才会触发反序列化。