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

    • 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章 扩展与异步机制

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

ControllerAdvice

本章聚焦 Spring MVC 的全局异常处理增强。前面章节讲解了 @ExceptionHandler 的局部用法,但每个 Controller 都写一遍异常处理代码显然冗余。@ControllerAdvice 允许把异常处理逻辑提取到独立的类中,统一拦截所有 Controller 的异常,实现"一处定义,全局生效"。

本章与全局的关系:前面章节讲解了局部异常处理,本章讲解全局异常处理的架构和实现。配合 @ExceptionHandler,@ControllerAdvice 构成了 Spring MVC 异常处理体系的顶层设计。


定义与作用

@ControllerAdvice 是 Spring MVC 提供的全局控制器增强注解,标注在类上,使该类成为"全局增强类"。它的核心职责包括:

  1. 全局异常处理:配合 @ExceptionHandler,拦截所有 Controller 抛出的异常
  2. 全局数据绑定:配合 @InitBinder,自定义全局类型转换器和属性编辑器
  3. 全局模型属性:配合 @ModelAttribute,为所有 Controller 方法添加共享模型数据

本章聚焦最常用的全局异常处理场景。

生活类比:公司总部的客服中心

想象飞翔科技:

  • 每个部门(Controller)原本有自己的故障处理员(局部 @ExceptionHandler)
  • 但大多数问题都是共性的——网络故障、数据库超时、参数格式错误
  • CTO 大翔决定成立总部客服中心(@ControllerAdvice)
  • 客服中心统一处理所有部门的共性问题,各部门只需处理自己特有的问题
  • 如果某部门有自己的故障处理员(局部 @ExceptionHandler),部门内部问题优先由自己处理;处理不了的,再上报总部

关键认知:@ControllerAdvice 不是替代局部 @ExceptionHandler,而是补充和兜底。局部优先,全局兜底。


核心原理

全局异常处理架构

处理优先级:

  1. 当前 Controller 的局部 @ExceptionHandler(精确匹配 > 父类匹配)
  2. @ControllerAdvice 中的全局 @ExceptionHandler(同样遵循精确匹配 > 父类匹配)
  3. 多个 @ControllerAdvice 之间,可通过 @Order 或 annotations/basePackages 限定优先级
  4. Spring Boot 默认错误处理(Whitelabel Error Page)

basePackages 限定作用域


适用位置与常用属性

@ControllerAdvice 标注在类上:

属性类型说明
basePackages / valueString[]限定生效的包范围,如 "com.feixiang.web.controller"
basePackageClassesClass<?>[]通过类所在包限定范围
assignableTypesClass<?>[]限定对特定 Controller 类型生效
annotationsClass<? 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

流程解析:

  1. EmployeeController.getEmployee(-1) 抛出 IllegalArgumentException
  2. EmployeeController 无局部 @ExceptionHandler
  3. 异常上抛给 GlobalExceptionHandler
  4. 匹配 @ExceptionHandler(IllegalArgumentException.class)
  5. 返回统一的 ErrorResponse JSON

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 处理,返回 JSON
  • com.feixiang.web.admin 包下的 Controller 异常 → 由 AdminExceptionHandler 处理,返回 HTML 错误页面
  • 两个包互不干扰

易错场景与面试考点

误区一:@ControllerAdvice 不加 basePackages 导致所有异常都被拦截

问题场景:项目中引入了第三方库的 Spring MVC 组件(如 Swagger、Actuator),它们的 Controller 抛出的异常也被你的 @ControllerAdvice 捕获,导致响应格式混乱。

纠正:生产环境建议始终指定 basePackages,明确限定作用范围:

@ControllerAdvice(basePackages = "com.feixiang.web.controller")

误区二:多个 @ControllerAdvice 的优先级混乱

问题场景:两个 @ControllerAdvice 都能处理同一种异常,到底用哪个?

纠正:Spring 通过以下规则确定优先级:

  1. 有 @Order 注解的,按 order 值排序(越小越优先)
  2. 无 @Order 的,按类名字母顺序(不稳定,不推荐依赖)
  3. 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 的统一写法。

上一页
ExceptionHandler
下一页
RestControllerAdvice