ControllerAdvice
本章聚焦 Spring MVC 的全局异常处理增强。前面章节讲解了 @ExceptionHandler 的局部用法,但每个 Controller 都写一遍异常处理代码显然冗余。@ControllerAdvice 允许把异常处理逻辑提取到独立的类中,统一拦截所有 Controller 的异常,实现"一处定义,全局生效"。
本章与全局的关系:前面章节讲解了局部异常处理,本章讲解全局异常处理的架构和实现。配合 @ExceptionHandler,@ControllerAdvice 构成了 Spring MVC 异常处理体系的顶层设计。
定义与作用
@ControllerAdvice 是 Spring MVC 提供的全局控制器增强注解,标注在类上,使该类成为"全局增强类"。它的核心职责包括:
- 全局异常处理:配合
@ExceptionHandler,拦截所有 Controller 抛出的异常 - 全局数据绑定:配合
@InitBinder,自定义全局类型转换器和属性编辑器 - 全局模型属性:配合
@ModelAttribute,为所有 Controller 方法添加共享模型数据
本章聚焦最常用的全局异常处理场景。
生活类比:公司总部的客服中心
想象飞翔科技:
- 每个部门(Controller)原本有自己的故障处理员(局部 @ExceptionHandler)
- 但大多数问题都是共性的——网络故障、数据库超时、参数格式错误
- CTO 大翔决定成立总部客服中心(@ControllerAdvice)
- 客服中心统一处理所有部门的共性问题,各部门只需处理自己特有的问题
- 如果某部门有自己的故障处理员(局部 @ExceptionHandler),部门内部问题优先由自己处理;处理不了的,再上报总部
关键认知:@ControllerAdvice 不是替代局部 @ExceptionHandler,而是补充和兜底。局部优先,全局兜底。
核心原理
全局异常处理架构
处理优先级:
- 当前 Controller 的局部 @ExceptionHandler(精确匹配 > 父类匹配)
- @ControllerAdvice 中的全局 @ExceptionHandler(同样遵循精确匹配 > 父类匹配)
- 多个 @ControllerAdvice 之间,可通过
@Order或annotations/basePackages限定优先级 - Spring Boot 默认错误处理(Whitelabel Error Page)
basePackages 限定作用域
适用位置与常用属性
@ControllerAdvice 标注在类上:
| 属性 | 类型 | 说明 |
|---|---|---|
basePackages / value | String[] | 限定生效的包范围,如 "com.feixiang.web.controller" |
basePackageClasses | Class<?>[] | 通过类所在包限定范围 |
assignableTypes | Class<?>[] | 限定对特定 Controller 类型生效 |
annotations | Class<? extends Annotation>[] | 限定对标注了特定注解的 Controller 生效 |
组合注解
| 注解组合 | 作用 |
|---|---|
@ControllerAdvice + @ExceptionHandler | 全局异常处理(本章重点) |
@ControllerAdvice + @InitBinder | 全局数据绑定定制 |
@ControllerAdvice + @ModelAttribute | 全局模型属性 |
完整示例
场景
飞翔科技员工管理系统有多个 Controller:EmployeeController、DepartmentController、FileUploadController。架构师白歌要求统一异常响应格式,避免每个 Controller 重复编写 @ExceptionHandler。
项目结构
employee-web/
├── src/main/java/
│ └── com/feixiang/web/
│ ├── controller/
│ │ ├── EmployeeController.java
│ │ └── DepartmentController.java
│ ├── advice/
│ │ └── GlobalExceptionHandler.java
│ └── dto/
│ └── ErrorResponse.java
全局异常处理类
// GlobalExceptionHandler.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.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.NoSuchElementException;
@ControllerAdvice(basePackages = "com.feixiang.web.controller")
public class GlobalExceptionHandler {
// 参数校验异常 —— 400
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleIllegalArgument(IllegalArgumentException ex, HttpServletRequest request) {
return new ErrorResponse(
400,
"请求参数错误: " + ex.getMessage(),
request.getRequestURI()
);
}
// 资源不存在 —— 404
@ExceptionHandler(NoSuchElementException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleNotFound(NoSuchElementException ex, HttpServletRequest request) {
return new ErrorResponse(
404,
"资源不存在: " + ex.getMessage(),
request.getRequestURI()
);
}
// 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)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleGenericException(Exception ex, HttpServletRequest request) {
return new ErrorResponse(
500,
"服务器内部错误,请联系运维人员",
request.getRequestURI()
);
}
}
错误响应 DTO
// 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; }
}
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;
}
}
// DepartmentController.java
package com.feixiang.web.controller;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/departments")
public class DepartmentController {
@GetMapping("/{id}")
public String getDepartment(@PathVariable Long id) {
if (id == 404) {
throw new IllegalArgumentException("部门ID格式错误");
}
return "研发部";
}
}
HTTP 请求示例 1:EmployeeController 参数错误
$ curl -X GET http://localhost:8080/api/employees/-1
响应:
{
"code": 400,
"message": "请求参数错误: ID 必须大于0",
"path": "/api/employees/-1",
"timestamp": 1718000000000
}
状态码:400 Bad Request
流程解析:
EmployeeController.getEmployee(-1)抛出IllegalArgumentExceptionEmployeeController无局部 @ExceptionHandler- 异常上抛给
GlobalExceptionHandler - 匹配
@ExceptionHandler(IllegalArgumentException.class) - 返回统一的
ErrorResponseJSON
HTTP 请求示例 2:DepartmentController 参数错误
$ curl -X GET http://localhost:8080/api/departments/404
响应:
{
"code": 400,
"message": "请求参数错误: 部门ID格式错误",
"path": "/api/departments/404",
"timestamp": 1718000000000
}
关键观察:DepartmentController 没有写任何异常处理代码,但异常响应格式与 EmployeeController 完全一致——这就是全局异常处理的价值。
HTTP 请求示例 3:资源不存在
$ curl -X GET http://localhost:8080/api/employees/999
响应:
{
"code": 404,
"message": "资源不存在: 员工不存在: 999",
"path": "/api/employees/999",
"timestamp": 1718000000000
}
状态码:404 Not Found
basePackages 限定作用域示例
场景
飞翔科技有两个子系统:
- API 子系统(
com.feixiang.web.api):面向前端,返回 JSON - Admin 子系统(
com.feixiang.web.admin):面向管理后台,返回 HTML 错误页面
需要不同的全局异常处理策略。
// ApiExceptionHandler.java
package com.feixiang.web.advice;
import com.feixiang.web.dto.ErrorResponse;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import javax.servlet.http.HttpServletRequest;
@ControllerAdvice(basePackages = "com.feixiang.web.api")
public class ApiExceptionHandler {
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleApiException(Exception ex, HttpServletRequest request) {
return new ErrorResponse(500, "API服务异常: " + ex.getMessage(), request.getRequestURI());
}
}
// AdminExceptionHandler.java
package com.feixiang.web.advice;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;
@ControllerAdvice(basePackages = "com.feixiang.web.admin")
public class AdminExceptionHandler {
@ExceptionHandler(Exception.class)
public ModelAndView handleAdminException(Exception ex) {
ModelAndView mav = new ModelAndView("error/admin-error");
mav.addObject("message", ex.getMessage());
return mav;
}
}
效果:
com.feixiang.web.api包下的 Controller 异常 → 由ApiExceptionHandler处理,返回 JSONcom.feixiang.web.admin包下的 Controller 异常 → 由AdminExceptionHandler处理,返回 HTML 错误页面- 两个包互不干扰
易错场景与面试考点
误区一:@ControllerAdvice 不加 basePackages 导致所有异常都被拦截
问题场景:项目中引入了第三方库的 Spring MVC 组件(如 Swagger、Actuator),它们的 Controller 抛出的异常也被你的 @ControllerAdvice 捕获,导致响应格式混乱。
纠正:生产环境建议始终指定 basePackages,明确限定作用范围:
@ControllerAdvice(basePackages = "com.feixiang.web.controller")
误区二:多个 @ControllerAdvice 的优先级混乱
问题场景:两个 @ControllerAdvice 都能处理同一种异常,到底用哪个?
纠正:Spring 通过以下规则确定优先级:
- 有
@Order注解的,按 order 值排序(越小越优先) - 无
@Order的,按类名字母顺序(不稳定,不推荐依赖) basePackages更精确的优先于更宽泛的
推荐做法:
@Order(1)
@ControllerAdvice(basePackages = "com.feixiang.web.controller")
public class PrimaryExceptionHandler { }
@Order(2)
@ControllerAdvice
public class FallbackExceptionHandler { }
误区三:@ControllerAdvice 中使用了 @ResponseBody 但返回的是视图名
错误代码:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
@ResponseBody // 错误!
public String handleException() {
return "error"; // 本意是返回 error.html 视图
}
}
错误现象:客户端收到字符串 "error",而不是渲染的视图。
纠正:如果需要返回视图,不要加 @ResponseBody;如果需要返回 JSON,加 @ResponseBody 或使用 @RestControllerAdvice。
面试高频:@ControllerAdvice 的 basePackages 和 annotations 的区别
| 属性 | 作用 |
|---|---|
basePackages | 按包路径限定,如 "com.feixiang.web.api" |
basePackageClasses | 按类所在包限定,如 UserController.class |
assignableTypes | 按 Controller 类型限定,如 UserController.class |
annotations | 按 Controller 上的注解限定,如 @RestController.class |
标准回答:basePackages 最常用,按包路径批量限定。annotations 适合按架构分层限定(如只对标注了 @ApiController 自定义注解的 Controller 生效)。
面试高频:局部 @ExceptionHandler 和全局 @ControllerAdvice 的优先级
标准回答:局部优先于全局。当异常抛出时,Spring 先查找当前 Controller 的局部 @ExceptionHandler,如果有匹配则使用;无匹配时,再查找所有 @ControllerAdvice,按优先级排序后选择第一个匹配的。这种设计遵循"就近原则",允许特定 Controller 覆盖全局行为。
小结
@ControllerAdvice 是 Spring MVC 的全局控制器增强注解,配合 @ExceptionHandler 实现"一处定义,全局生效"的异常处理。通过 basePackages、annotations 等属性,可以精确限定作用范围,避免对不需要的 Controller 产生干扰。局部 @ExceptionHandler 优先级高于全局 @ControllerAdvice,遵循"就近原则"。
本章与全局的关系:本章讲解了全局异常处理的架构。下一章"@RestControllerAdvice"将讲解 RESTful 项目的全局异常处理最佳实践——直接返回 JSON 的统一写法。