RestControllerAdvice
本章聚焦 Spring MVC 的RESTful 全局异常处理最佳实践。前面章节讲解了 @ControllerAdvice 的全局异常处理能力,但在 RESTful API 项目中,异常处理方法的返回值需要自动序列化为 JSON。如果每次都在 @ExceptionHandler 方法上加 @ResponseBody,代码显得冗余。@RestControllerAdvice 正是为此而生的组合注解,它是 RESTful 项目全局异常处理的首选方案。
本章与全局的关系:前面章节讲解了 @ControllerAdvice 的全局异常处理,本章讲解 RESTful 场景下的简化写法。两者功能等价,只是 @RestControllerAdvice 更贴合 RESTful 项目的开发习惯。
定义与作用
@RestControllerAdvice 是 Spring 4.3 引入的组合注解,等价于 @ControllerAdvice + @ResponseBody。它的核心作用是:
- 继承 @ControllerAdvice 的所有能力:全局异常处理、全局数据绑定、全局模型属性
- 自动添加 @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 / value | String[] | 限定生效的包范围 |
basePackageClasses | Class<?>[] | 通过类所在包限定范围 |
assignableTypes | Class<?>[] | 限定对特定 Controller 类型生效 |
annotations | Class<? extends Annotation>[] | 限定对标注了特定注解的 Controller 生效 |
与相关注解的对比
| 注解 | 组合关系 | 适用场景 |
|---|---|---|
@ControllerAdvice | 无 | 需要返回视图(HTML)的全局增强 |
@ControllerAdvice + @ResponseBody | 手动组合 | RESTful 全局增强(写法冗余) |
@RestControllerAdvice | @ControllerAdvice + @ResponseBody | RESTful 全局增强(推荐写法) |
@RestController | @Controller + @ResponseBody | RESTful 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。
面试高频:统一错误响应格式的设计
标准回答:生产环境的统一错误响应应包含:
- code:业务错误码(区别于 HTTP 状态码),便于前端做分支判断
- message:用户友好的错误描述
- path:出错的请求路径,便于定位问题
- timestamp:错误发生时间,便于日志关联
- traceId(可选):分布式追踪 ID,便于全链路排查
避免在响应中包含 stackTrace 等敏感信息,防止泄露系统内部结构。
小结
@RestControllerAdvice 是 @ControllerAdvice + @ResponseBody 的组合注解,是 RESTful 项目全局异常处理的首选方案。它让异常处理方法的返回值自动序列化为 JSON,无需逐个标注 @ResponseBody。在纯 API 项目中,配合统一的 ErrorResponse 结构,可以实现"一处定义,全局生效"的优雅错误处理。
本章与全局的关系:本章讲解了 RESTful 项目的全局异常处理简化写法。下一章"@ResponseStatus"将讲解如何通过注解自动映射异常到 HTTP 状态码,进一步完善异常处理体系。