Spring MVC 核心机制详解:全局异常处理、请求跳转与数据绑定

在 Spring MVC 项目中,全局异常处理、请求转发与重定向、数据绑定都是开发接口时经常遇到的内容。它们看起来相互独立,实际上都发生在一次 HTTP 请求的处理链路中。

本文通过可直接理解的代码示例,依次讲清以下三个问题:

  1. Spring MVC 如何统一处理 Controller 抛出的异常?

  2. Controller 之间如何转发或重定向,两者有什么区别?

  3. 请求中的字符串参数,如何绑定成 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 方法需要的可能是 IntegerBooleanLocalDate,甚至一个完整的 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,把 nameage 填入对象。对于复杂类型参数,也可以显式标注 @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 等校验规则。

四、总结

本文的三个核心结论可以这样记忆:

  1. 异常处理@ExceptionHandler 定义处理方法,@RestControllerAdvice 将异常处理逻辑统一应用到多个 Controller。

  2. 请求跳转:转发是服务器内部的一次请求;重定向是浏览器发起的第二次请求。

  3. 数据绑定 :参数解析器先确定数据来源,再由 WebDataBinder、类型转换器或 HttpMessageConverter 完成转换和对象构造,最后执行参数校验。

从完整请求链路理解这些组件,比单纯记住几个注解更重要。这样不仅能写出结构清晰的接口,也能在出现参数错误、跳转异常或统一错误响应失效时,更快定位问题。

相关推荐
weixin_BYSJ19871 小时前
springboot小区物业管理小程序---附源码45555
java·javascript·spring boot·python·django·flask·php
coder_lorraine1 小时前
Halo 2.x 回收站文章无法删除?一篇文章教你彻底解决
java·运维
索隆zoro2 小时前
Army 进阶:DML 操作体系与 Spring AI 集成
java
吃饱了得干活2 小时前
为什么你的Service越写越臃肿?三层架构的“业务逻辑层”是个黑盒
java·后端·架构
趴下吧2 小时前
spring boot启动流程总结
java·spring boot·spring
码农进化录2 小时前
Java 程序员的 AI 进化论 | AI 帮我做 MySQL 表结构升级,三天活变半天
java·spring boot·openai
三言老师2 小时前
K8s集群运行时异常趋势分析预警实操
java·开发语言·kubernetes
吃饱了得干活2 小时前
从经典的三层架构到DDD:一次对“业务逻辑层”的解剖与重构
java·后端·架构
SL_staff2 小时前
中小离散制造如何用工艺路线模板解决‘人走流程丢’——JVS-APS 实践解析
java·设计模式·全栈