在 Spring MVC 项目中,全局异常处理、请求转发与重定向、数据绑定都是开发接口时经常遇到的内容。它们看起来相互独立,实际上都发生在一次 HTTP 请求的处理链路中。
本文通过可直接理解的代码示例,依次讲清以下三个问题:
-
Spring MVC 如何统一处理 Controller 抛出的异常?
-
Controller 之间如何转发或重定向,两者有什么区别?
-
请求中的字符串参数,如何绑定成 Java 方法参数或对象?
示例基于 Spring Boot 3,默认使用 Spring MVC 和 Jakarta Validation。
一、Spring MVC 如何进行全局异常处理?
1. 为什么需要统一异常处理?
如果在每个 Controller 中都使用 try-catch,代码会出现大量重复逻辑:
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
try {
return userService.getById(id);
} catch (Exception e) {
// 记录日志并拼装错误结果
throw e;
}
}
Controller 最好只负责接收参数和调用业务逻辑。异常转换、错误码和错误信息应交给统一的异常处理器完成。
2. 使用 @ExceptionHandler 处理局部异常
@ExceptionHandler 可以写在 Controller 内部,负责处理当前 Controller 抛出的指定异常:
@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping("/{id}")
public String getUser(@PathVariable Long id) {
if (id <= 0) {
throw new IllegalArgumentException("用户 ID 必须大于 0");
}
return "user-" + id;
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ErrorResponse> handleIllegalArgument(
IllegalArgumentException exception) {
ErrorResponse body = new ErrorResponse(400, exception.getMessage());
return ResponseEntity.badRequest().body(body);
}
}
统一错误响应对象如下:
public record ErrorResponse(int code, String message) {
}
这种方式适合某个 Controller 独有的异常,但如果多个 Controller 都需要相同处理逻辑,就会再次产生重复代码。
3. 使用 @RestControllerAdvice 实现全局异常处理
@RestControllerAdvice 可以理解为:
@ControllerAdvice + @ResponseBody
它能够集中处理多个 Controller 抛出的异常,并将返回对象序列化为 JSON。
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(
BusinessException exception) {
ErrorResponse body = new ErrorResponse(
exception.getCode(), exception.getMessage());
return ResponseEntity.badRequest().body(body);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationException(
MethodArgumentNotValidException exception) {
String message = exception.getBindingResult()
.getFieldErrors()
.stream()
.findFirst()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.orElse("请求参数不合法");
return ResponseEntity.badRequest()
.body(new ErrorResponse(400, message));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleUnknownException(
Exception exception) {
// 实际项目中应在这里记录完整异常日志
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse(500, "服务器内部错误"));
}
}
自定义业务异常:
public class BusinessException extends RuntimeException {
private final int code;
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
public int getCode() {
return code;
}
}
4. @ControllerAdvice 和 @RestControllerAdvice 的区别
| 注解 | 返回值默认如何处理 | 常见场景 |
|---|---|---|
@ControllerAdvice |
可以返回视图名称 | 传统 MVC 页面项目 |
@RestControllerAdvice |
返回值写入响应体,一般转成 JSON | 前后端分离 REST API |
如果使用 @ControllerAdvice,但某个异常处理方法需要返回 JSON,可以单独添加 @ResponseBody。
5. 异常处理的大致执行过程
Controller 方法抛出异常后,DispatcherServlet 会把异常交给 HandlerExceptionResolver 处理。其中,ExceptionHandlerExceptionResolver 会查找匹配的 @ExceptionHandler 方法。
可以简单记为:
Controller 抛出异常
↓
HandlerExceptionResolver 尝试解析
↓
查找匹配的 @ExceptionHandler
↓
执行异常处理方法
↓
生成 JSON 响应或返回错误视图
通常会优先考虑当前 Controller 内部适用的异常处理方法,再考虑全局 Advice 中的方法。设计异常处理时,还应让具体异常处理器负责具体异常,用 Exception.class 作为最后兜底,不要把所有错误都统一返回 HTTP 200。
二、Controller 之间如何重定向与转发?
Spring MVC 可以通过返回带有特殊前缀的字符串实现请求跳转:
-
forward:表示转发; -
redirect:表示重定向。
下面假设系统存在目标接口:
@Controller
public class PageController {
@GetMapping("/target")
@ResponseBody
public String target(@RequestParam(required = false) String message) {
return "目标页面:" + message;
}
}
1. 请求转发
@GetMapping("/forward-demo")
public String forwardDemo(HttpServletRequest request) {
request.setAttribute("source", "forward-demo");
return "forward:/target?message=forward";
}
转发由服务器内部完成。浏览器只发送一次请求,地址栏仍然显示 /forward-demo。
转发前后的代码共享同一个 HttpServletRequest,因此可以通过请求属性传递数据:
request.setAttribute("userId", 1001L);
目标方法可以这样读取:
@GetMapping("/target-request")
@ResponseBody
public String targetRequest(HttpServletRequest request) {
return "userId=" + request.getAttribute("userId");
}
2. 请求重定向
@GetMapping("/redirect-demo")
public String redirectDemo(RedirectAttributes attributes) {
attributes.addAttribute("message", "redirect");
return "redirect:/target";
}
重定向时,服务器第一次返回 3xx 响应和新的访问地址,浏览器再向新地址发送第二次请求。因此,浏览器地址栏会变成 /target?message=redirect。
除了查询参数,还可以使用 Flash Attribute 临时传值:
@PostMapping("/orders")
public String createOrder(RedirectAttributes attributes) {
attributes.addFlashAttribute("successMessage", "订单创建成功");
return "redirect:/orders/result";
}
Flash Attribute 通常依赖 Session 暂存一次数据,重定向后的请求读取完即可移除,适合传递短期提示信息。
3. 转发和重定向的区别
|------------|---------------|-----------------|
| 对比项 | 转发 forward: | 重定向 redirect: |
| 请求次数 | 1 次 | 2 次 |
| 执行位置 | 服务器内部 | 浏览器再次请求 |
| 地址栏 | 不改变 | 改变为新地址 |
| Request 数据 | 可以共享 | 不能直接共享 |
| 访问范围 | 通常限于当前应用 | 可以跳转到其他站点 |
| 刷新影响 | 可能重复提交原请求 | 常用于避免重复提交 |
4. 应该选择哪一种?
-
服务器内部继续处理当前请求,且需要保留 Request 数据:使用转发。
-
表单提交成功后进入结果页:通常使用重定向,即 PRG(Post/Redirect/Get)模式。
-
跳转到外部网站:使用重定向。
-
前后端分离项目中,后端一般不直接跳转,而是返回状态码或目标地址,由前端决定页面导航。
需要注意,转发和重定向不是普通 Java 方法之间的调用。如果两个 Controller 只是要复用业务逻辑,更合理的做法是把公共逻辑提取到 Service,而不是让 Controller 相互调用。
三、Spring MVC 是如何实现数据绑定的?
1. 什么是数据绑定?
HTTP 请求中的大部分参数最初都是字符串。例如:
/users?page=1&enabled=true&birthday=2026-08-17
但是 Controller 方法需要的可能是 Integer、Boolean、LocalDate,甚至一个完整的 Java 对象。Spring MVC 数据绑定的作用,就是把请求数据转换并填充到目标参数中。
2. 常见绑定方式
绑定查询参数
@GetMapping("/users")
@ResponseBody
public String listUsers(
@RequestParam Integer page,
@RequestParam Boolean enabled) {
return "page=" + page + ", enabled=" + enabled;
}
Spring 会将字符串 "1" 转成 Integer,将 "true" 转成 Boolean。
绑定路径变量
@GetMapping("/users/{id}")
@ResponseBody
public String getUser(@PathVariable Long id) {
return "id=" + id;
}
绑定表单或查询参数到对象
public class UserQuery {
private String name;
private Integer age;
// getter 和 setter 省略
}
@GetMapping("/users/search")
@ResponseBody
public UserQuery search(UserQuery query) {
return query;
}
请求:
/users/search?name=Tom&age=20
Spring 会创建 UserQuery,再按照属性名称调用 Setter,把 name 和 age 填入对象。对于复杂类型参数,也可以显式标注 @ModelAttribute。
绑定 JSON 请求体
public record CreateUserRequest(
@NotBlank(message = "用户名不能为空") String name,
@Min(value = 18, message = "年龄不能小于 18") Integer age) {
}
@PostMapping("/users")
@ResponseBody
public CreateUserRequest createUser(
@Valid @RequestBody CreateUserRequest request) {
return request;
}
JSON 请求:
{
"name": "Tom",
"age": 20
}
这里需要特别区分:
-
@ModelAttribute或普通复杂对象主要通过WebDataBinder绑定查询参数、表单字段; -
@RequestBody主要由HttpMessageConverter读取请求体,例如使用 Jackson 把 JSON 反序列化为 Java 对象。
因此,不能简单地说所有参数都只靠 WebDataBinder 完成绑定。
3. 数据绑定的大致流程
当 DispatcherServlet 找到 Controller 方法后,RequestMappingHandlerAdapter 负责调用该方法。调用前,它需要先准备方法的所有实参。
HTTP 请求进入 DispatcherServlet
↓
找到对应的 Controller 方法
↓
HandlerMethodArgumentResolver 解析每个方法参数
↓
WebDataBinder 进行属性绑定和类型转换
或 HttpMessageConverter 读取 JSON 请求体
↓
执行参数校验
↓
调用 Controller 方法
其中几个重要组件分别负责:
-
HandlerMethodArgumentResolver:判断参数应该从哪里取,例如查询参数、路径变量、请求体或请求头。 -
WebDataBinder:把请求参数绑定到对象属性,并协调类型转换和校验。 -
ConversionService:负责常见类型转换。 -
HttpMessageConverter:负责读取和写出请求体,例如 JSON 与 Java 对象之间的转换。 -
Validator:执行
@Valid、@NotBlank、@Min等校验规则。
四、总结
本文的三个核心结论可以这样记忆:
-
异常处理 :
@ExceptionHandler定义处理方法,@RestControllerAdvice将异常处理逻辑统一应用到多个 Controller。 -
请求跳转:转发是服务器内部的一次请求;重定向是浏览器发起的第二次请求。
-
数据绑定 :参数解析器先确定数据来源,再由
WebDataBinder、类型转换器或HttpMessageConverter完成转换和对象构造,最后执行参数校验。
从完整请求链路理解这些组件,比单纯记住几个注解更重要。这样不仅能写出结构清晰的接口,也能在出现参数错误、跳转异常或统一错误响应失效时,更快定位问题。