乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 SpringMVC概述与DispatcherServlet

    • 本章导读:Spring MVC概述与DispatcherServlet
    • Spring MVC 是什么
    • MVC 设计模式
    • 前端控制器模式
    • DispatcherServlet
    • 核心组件协作
  • 第2章 控制器与请求映射

    • 本章导读:控制器与请求映射
    • Controller
    • RestController
    • RequestMapping
    • GetMapping
    • PostMapping
    • PutMapping
    • DeleteMapping
    • PathVariable
    • RESTful
    • 请求映射原理
  • 第3章 请求参数获取与转换

    • 本章导读:请求参数获取与转换
    • RequestParam
    • RequestBody
    • RequestHeader
    • CookieValue
    • Model
    • ModelAttribute
    • 数据绑定原理
    • 数据校验
  • 第4章 响应数据与视图解析

    • 本章导读:响应数据与视图解析
    • ResponseBody
    • ResponseEntity
    • ModelAndView
    • ViewResolver
    • HttpMessageConverter
    • forward与redirect
  • 第5章 拦截器过滤器与跨域

    • 本章导读:拦截器、过滤器与跨域
    • HandlerInterceptor
    • WebMvcConfigurer
    • CrossOrigin
    • 登录验证实战
  • 第6章 文件上传与异常处理

    • 本章导读:文件上传与异常处理
    • MultipartFile
    • 文件下载
    • ExceptionHandler
    • ControllerAdvice
    • RestControllerAdvice
    • ResponseStatus
  • 第7章 高级特性与最佳实践

    • 本章导读:高级特性与最佳实践
    • SessionAttributes
    • SessionAttribute
    • RedirectAttributes
    • MockMvc测试
    • 国际化
    • 最佳实践
  • 第8章 扩展与异步机制

    • 本章导读:扩展与异步机制
    • 异步请求处理
    • 自定义参数解析器
    • 内容协商

RestControllerAdvice

本章聚焦 Spring MVC 的RESTful 全局异常处理最佳实践。前面章节讲解了 @ControllerAdvice 的全局异常处理能力,但在 RESTful API 项目中,异常处理方法的返回值需要自动序列化为 JSON。如果每次都在 @ExceptionHandler 方法上加 @ResponseBody,代码显得冗余。@RestControllerAdvice 正是为此而生的组合注解,它是 RESTful 项目全局异常处理的首选方案。

本章与全局的关系:前面章节讲解了 @ControllerAdvice 的全局异常处理,本章讲解 RESTful 场景下的简化写法。两者功能等价,只是 @RestControllerAdvice 更贴合 RESTful 项目的开发习惯。


定义与作用

@RestControllerAdvice 是 Spring 4.3 引入的组合注解,等价于 @ControllerAdvice + @ResponseBody。它的核心作用是:

  1. 继承 @ControllerAdvice 的所有能力:全局异常处理、全局数据绑定、全局模型属性
  2. 自动添加 @ResponseBody 语义:类中所有方法的返回值自动通过 HttpMessageConverter 序列化,无需逐个标注

在 RESTful API 项目中,全局异常处理类通常只关心一件事:把异常转换为 JSON 错误响应。@RestControllerAdvice 让这件事的代码更简洁。

等价关系

@RestControllerAdvice
public class GlobalExceptionHandler { }

完全等价于:

@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler { }

生活类比:公司总部的标准化客服

想象飞翔科技的客服中心升级:

  • 原来的客服中心(@ControllerAdvice)处理完问题后,需要手动填写报告、盖章、复印(手动加 @ResponseBody)
  • 升级后的标准化客服中心(@RestControllerAdvice)内置了自动报告生成机——处理完问题,报告自动生成、自动盖章、自动归档
  • 对前端黄俪来说,她收到的永远是格式统一的 JSON 错误响应,不关心后台是哪种客服中心

关键认知:@RestControllerAdvice 不是新功能,而是语法糖。它让 RESTful 项目的代码更简洁,但底层机制与 @ControllerAdvice 完全一致。


核心原理

与 @ControllerAdvice + @ResponseBody 的等价关系

底层实现:

  • @RestControllerAdvice 元注解包含 @ControllerAdvice 和 @ResponseBody
  • Spring 在解析该类时,发现 @ResponseBody 元注解,为类中所有方法自动添加"返回值序列化"行为
  • 异常处理方法的返回值(如 ErrorResponse 对象)通过 MappingJackson2HttpMessageConverter 自动转为 JSON

适用位置与常用属性

@RestControllerAdvice 标注在类上,属性与 @ControllerAdvice 完全一致:

属性类型说明
basePackages / valueString[]限定生效的包范围
basePackageClassesClass<?>[]通过类所在包限定范围
assignableTypesClass<?>[]限定对特定 Controller 类型生效
annotationsClass<? extends Annotation>[]限定对标注了特定注解的 Controller 生效

与相关注解的对比

注解组合关系适用场景
@ControllerAdvice无需要返回视图(HTML)的全局增强
@ControllerAdvice + @ResponseBody手动组合RESTful 全局增强(写法冗余)
@RestControllerAdvice@ControllerAdvice + @ResponseBodyRESTful 全局增强(推荐写法)
@RestController@Controller + @ResponseBodyRESTful Controller(与本章对应)

完整示例

场景

飞翔科技员工管理系统是纯 RESTful API 项目,所有异常响应必须是 JSON 格式。架构师白歌要求统一错误响应格式,包含错误码、错误信息、请求路径、时间戳四个字段。

统一错误响应格式

// ErrorResponse.java
package com.feixiang.web.dto;

public class ErrorResponse {
    private int code;
    private String message;
    private String path;
    private long timestamp;

    public ErrorResponse(int code, String message, String path) {
        this.code = code;
        this.message = message;
        this.path = path;
        this.timestamp = System.currentTimeMillis();
    }

    // getters
    public int getCode() { return code; }
    public String getMessage() { return message; }
    public String getPath() { return path; }
    public long getTimestamp() { return timestamp; }
}

全局异常处理类

// GlobalRestExceptionHandler.java
package com.feixiang.web.advice;

import com.feixiang.web.dto.ErrorResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.NoSuchElementException;

@RestControllerAdvice(basePackages = "com.feixiang.web.controller")
public class GlobalRestExceptionHandler {

    // 参数校验异常 —— 400
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ErrorResponse> handleIllegalArgument(IllegalArgumentException ex, HttpServletRequest request) {
        ErrorResponse error = new ErrorResponse(
            400,
            "请求参数错误: " + ex.getMessage(),
            request.getRequestURI()
        );
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
    }

    // 资源不存在 —— 404
    @ExceptionHandler(NoSuchElementException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(NoSuchElementException ex, HttpServletRequest request) {
        ErrorResponse error = new ErrorResponse(
            404,
            "资源不存在: " + ex.getMessage(),
            request.getRequestURI()
        );
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }

    // IO 异常 —— 500
    @ExceptionHandler(IOException.class)
    public ResponseEntity<ErrorResponse> handleIOException(IOException ex, HttpServletRequest request) {
        ErrorResponse error = new ErrorResponse(
            500,
            "系统服务异常: " + ex.getMessage(),
            request.getRequestURI()
        );
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
    }

    // 兜底异常 —— 500
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGenericException(Exception ex, HttpServletRequest request) {
        ErrorResponse error = new ErrorResponse(
            500,
            "服务器内部错误,请联系运维人员",
            request.getRequestURI()
        );
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
    }
}

注意:所有 @ExceptionHandler 方法都没有加 @ResponseBody,但返回值仍然自动序列化为 JSON——这就是 @RestControllerAdvice 的价值。

Controller(无任何异常处理代码)

// EmployeeController.java
package com.feixiang.web.controller;

import com.feixiang.web.dto.EmployeeDTO;
import org.springframework.web.bind.annotation.*;

import java.util.NoSuchElementException;

@RestController
@RequestMapping("/api/employees")
public class EmployeeController {

    @GetMapping("/{id}")
    public EmployeeDTO getEmployee(@PathVariable Long id) {
        if (id <= 0) {
            throw new IllegalArgumentException("ID 必须大于0");
        }
        if (id == 999) {
            throw new NoSuchElementException("员工不存在: " + id);
        }
        EmployeeDTO dto = new EmployeeDTO();
        dto.setName("张三");
        dto.setDepartment("研发部");
        return dto;
    }

    @PostMapping
    public EmployeeDTO addEmployee(@RequestBody EmployeeDTO dto) {
        if (dto.getName() == null || dto.getName().trim().isEmpty()) {
            throw new IllegalArgumentException("员工姓名不能为空");
        }
        dto.setName(dto.getName() + "-已保存");
        return dto;
    }
}

HTTP 请求示例 1:参数校验失败

$ curl -X POST http://localhost:8080/api/employees \
  -H "Content-Type: application/json" \
  -d '{"name":"","department":"研发部"}'

响应:

{
  "code": 400,
  "message": "请求参数错误: 员工姓名不能为空",
  "path": "/api/employees",
  "timestamp": 1718000000000
}

状态码:400 Bad Request

HTTP 请求示例 2:资源不存在

$ curl -X GET http://localhost:8080/api/employees/999

响应:

{
  "code": 404,
  "message": "资源不存在: 员工不存在: 999",
  "path": "/api/employees/999",
  "timestamp": 1718000000000
}

状态码:404 Not Found

HTTP 请求示例 3:未预料的异常兜底

$ curl -X GET http://localhost:8080/api/employees/null

假设 @PathVariable Long id 绑定字符串 "null" 时抛出 MethodArgumentTypeMismatchException(未被显式捕获)。

响应:

{
  "code": 500,
  "message": "服务器内部错误,请联系运维人员",
  "path": "/api/employees/null",
  "timestamp": 1718000000000
}

状态码:500 Internal Server Error

流程解析:

  • MethodArgumentTypeMismatchException 未被显式捕获
  • 匹配 @ExceptionHandler(Exception.class) 兜底方法
  • 返回统一格式的 500 错误响应,避免堆栈信息泄露

易错场景与面试考点

误区一:@RestControllerAdvice 中返回 String 被当作 JSON

代码示例:

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(Exception.class)
    public String handleException() {
        return "error";
    }
}

响应:"error"(带双引号的 JSON 字符串)

纠正:如果确实需要返回纯文本,应显式控制 Content-Type:

@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleException() {
    return ResponseEntity.status(500)
            .contentType(MediaType.TEXT_PLAIN)
            .body("error");
}

但在 RESTful 项目中,更推荐始终返回结构化的 JSON 对象。

误区二:@RestControllerAdvice 和 @ControllerAdvice 混用导致响应格式不一致

问题场景:项目中同时存在:

@ControllerAdvice
public class HtmlExceptionHandler { }

@RestControllerAdvice
public class JsonExceptionHandler { }

某个 Controller 同时被两个 Advice 匹配,到底返回 HTML 还是 JSON?

纠正:通过 basePackages 或 annotations 精确限定范围,避免重叠:

@RestControllerAdvice(basePackages = "com.feixiang.web.api")
public class JsonExceptionHandler { }

@ControllerAdvice(basePackages = "com.feixiang.web.admin")
public class HtmlExceptionHandler { }

误区三:认为 @RestControllerAdvice 只能处理异常

纠正:@RestControllerAdvice 继承自 @ControllerAdvice,同样支持 @InitBinder 和 @ModelAttribute:

@RestControllerAdvice
public class GlobalAdvice {
    // 全局异常处理
    @ExceptionHandler(Exception.class)
    public ErrorResponse handleException(Exception ex) { }

    // 全局类型转换器
    @InitBinder
    public void initBinder(WebDataBinder binder) {
        binder.registerCustomEditor(Date.class, new CustomDateEditor());
    }

    // 全局模型属性
    @ModelAttribute("companyName")
    public String companyName() {
        return "飞翔科技";
    }
}

但在 RESTful 项目中,@InitBinder 和 @ModelAttribute 使用较少,@RestControllerAdvice 主要用于异常处理。

面试高频:@RestControllerAdvice 和 @ControllerAdvice 的选择

场景推荐注解
纯 RESTful API 项目@RestControllerAdvice
混合项目(API + 页面)@ControllerAdvice + 按需加 @ResponseBody
需要返回视图的错误页面@ControllerAdvice(不加 @ResponseBody)

标准回答:如果全局异常处理类中的所有方法都返回 JSON,使用 @RestControllerAdvice 更简洁。如果部分方法返回视图、部分返回 JSON,使用 @ControllerAdvice 并在需要的方法上单独加 @ResponseBody。

面试高频:统一错误响应格式的设计

标准回答:生产环境的统一错误响应应包含:

  1. code:业务错误码(区别于 HTTP 状态码),便于前端做分支判断
  2. message:用户友好的错误描述
  3. path:出错的请求路径,便于定位问题
  4. timestamp:错误发生时间,便于日志关联
  5. traceId(可选):分布式追踪 ID,便于全链路排查

避免在响应中包含 stackTrace 等敏感信息,防止泄露系统内部结构。


小结

@RestControllerAdvice 是 @ControllerAdvice + @ResponseBody 的组合注解,是 RESTful 项目全局异常处理的首选方案。它让异常处理方法的返回值自动序列化为 JSON,无需逐个标注 @ResponseBody。在纯 API 项目中,配合统一的 ErrorResponse 结构,可以实现"一处定义,全局生效"的优雅错误处理。

本章与全局的关系:本章讲解了 RESTful 项目的全局异常处理简化写法。下一章"@ResponseStatus"将讲解如何通过注解自动映射异常到 HTTP 状态码,进一步完善异常处理体系。

上一页
ControllerAdvice
下一页
ResponseStatus