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

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

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

ExceptionHandler

本章聚焦 Spring MVC 的局部异常处理。文件上传时可能遇到空文件、大小超限;参数绑定时可能遇到类型不匹配;业务逻辑中可能遇到各种自定义异常。如果不对这些异常做处理,Spring MVC 会把异常堆栈直接暴露给客户端,既不安全也不友好。@ExceptionHandler 允许在 Controller 内部定义异常处理方法,把异常转换为规范的 HTTP 响应。

本章与全局的关系:前面章节讲解了正常请求的处理流程,本章讲解当 Controller 抛出异常时,Spring MVC 如何在 Controller 内部捕获并处理。后续章节将扩展到全局异常处理。


定义与作用

@ExceptionHandler 是 Spring MVC 提供的局部异常处理注解,标注在 Controller 类内部的方法上,用于捕获并处理该 Controller 抛出的特定异常。它的核心职责是:

  1. 捕获异常:拦截 Controller 方法执行过程中抛出的指定类型异常
  2. 转换响应:把异常转换为规范的 HTTP 响应(如 JSON 错误对象、错误页面)
  3. 隔离处理:每个 Controller 独立管理自己的异常,不影响其他 Controller

生活类比:部门内部的故障处理员

想象飞翔科技的研发部:

  • 研发部(Controller)有自己的故障处理员(@ExceptionHandler)
  • 当小崔写的代码出 bug 抛异常时,故障处理员立即介入
  • 故障处理员只处理研发部内部的问题,产品部的问题他不管
  • 他会把技术错误翻译成"用户能看懂的话"(把异常转换为友好的错误响应)

关键认知:@ExceptionHandler 的作用域仅限于声明它的 Controller 类。其他 Controller 抛出的同类异常,不会被这里的 @ExceptionHandler 捕获。


核心原理

局部异常处理流程

流程详解:

  1. Controller 方法抛出异常
  2. HandlerAdapter 捕获异常,中断正常返回值处理
  3. DispatcherServlet 查找当前 Controller 类中是否有 @ExceptionHandler 能处理该异常类型
  4. 找到匹配的方法后,调用它,传入异常对象作为参数
  5. @ExceptionHandler 方法的返回值按正常流程处理(视图渲染或消息转换)
  6. 如果当前 Controller 没有匹配的 @ExceptionHandler,异常继续向上抛给全局异常处理器

异常匹配规则

匹配优先级:

  1. 精确匹配(抛出的异常类型与 @ExceptionHandler(ExactException.class) 完全一致)
  2. 父类匹配(抛出的异常是声明类型的子类)
  3. 无匹配时,向上抛给全局异常处理器(如 @ControllerAdvice)

适用位置与常用属性

@ExceptionHandler 标注在 Controller 类内部的方法上:

属性类型说明
valueClass<? extends Throwable>[]要捕获的异常类型数组

方法参数与返回值

参数类型说明
异常类型@ExceptionHandler 声明的异常或其子类
HttpServletRequest / HttpServletResponse原请求和响应对象
WebRequestSpring 封装的通用请求对象
返回值类型处理方式
String视图名(配合 @ResponseBody 返回字符串)
ModelAndView渲染视图
@ResponseBody + 对象通过 HttpMessageConverter 序列化
ResponseEntity完整控制状态码、头、体

完整示例

场景

飞翔科技员工管理系统的 EmployeeController 需要处理以下异常:

  1. IllegalArgumentException:参数校验失败(如姓名为空)
  2. IOException:文件读写失败
  3. Exception:其他未预料的异常兜底

项目结构

employee-web/
├── src/main/java/
│   └── com/feixiang/web/
│       ├── controller/
│       │   └── EmployeeController.java
│       └── dto/
│           └── ErrorResponse.java

错误响应 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 com.feixiang.web.dto.ErrorResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

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

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

    @PostMapping
    public EmployeeDTO addEmployee(@RequestBody EmployeeDTO dto) {
        if (dto.getName() == null || dto.getName().trim().isEmpty()) {
            throw new IllegalArgumentException("员工姓名不能为空");
        }
        if (dto.getSalary() != null && dto.getSalary() < 0) {
            throw new IllegalArgumentException("薪资不能为负数");
        }
        // 模拟保存
        dto.setName(dto.getName() + "-已保存");
        return dto;
    }

    @GetMapping("/{id}")
    public EmployeeDTO getEmployee(@PathVariable Long id) throws IOException {
        if (id <= 0) {
            throw new IllegalArgumentException("ID 必须大于0");
        }
        if (id == 999) {
            throw new IOException("数据库连接失败");
        }
        EmployeeDTO dto = new EmployeeDTO();
        dto.setName("张三");
        dto.setDepartment("研发部");
        return dto;
    }

    // ========== 局部异常处理 ==========

    @ExceptionHandler(IllegalArgumentException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleIllegalArgument(IllegalArgumentException ex, HttpServletRequest request) {
        return new ErrorResponse(400, ex.getMessage(), request.getRequestURI());
    }

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

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ErrorResponse handleGenericException(Exception ex, HttpServletRequest request) {
        return new ErrorResponse(500, "服务器内部错误", request.getRequestURI());
    }
}

HTTP 请求示例 1:参数校验异常

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

响应:

{
  "code": 400,
  "message": "员工姓名不能为空",
  "path": "/api/employees",
  "timestamp": 1718000000000
}

状态码:400 Bad Request

流程解析:

  1. addEmployee 方法检查 name 为空,抛出 IllegalArgumentException
  2. HandlerAdapter 捕获异常
  3. DispatcherServlet 查找 EmployeeController 中的 @ExceptionHandler
  4. 匹配到 handleIllegalArgument(IllegalArgumentException.class)
  5. 调用该方法,构造 ErrorResponse
  6. @ResponseStatus(HttpStatus.BAD_REQUEST) 设置状态码为 400
  7. @RestController 环境下,返回值通过 Jackson 序列化为 JSON

HTTP 请求示例 2:IO 异常

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

响应:

{
  "code": 500,
  "message": "系统IO错误: 数据库连接失败",
  "path": "/api/employees/999",
  "timestamp": 1718000000000
}

状态码:500 Internal Server Error

流程解析:

  • handleIOException 返回 ResponseEntity,完全控制 HTTP 响应的每个方面
  • 相比 @ResponseStatus,ResponseEntity 更灵活,可在运行时决定状态码

HTTP 请求示例 3:其他 Controller 的异常不被捕获

$ curl -X GET http://localhost:8080/api/departments/1

假设 DepartmentController 也抛出 IllegalArgumentException,但没有定义 @ExceptionHandler。

响应:Spring Boot 默认错误页面(Whitelabel Error Page)或全局异常处理器处理的结果。

原因:EmployeeController 中的 @ExceptionHandler(IllegalArgumentException.class) 只捕获 EmployeeController 自己抛出的异常,对 DepartmentController 无效。


易错场景与面试考点

误区一:@ExceptionHandler 方法可以随便放

错误代码:

@Service
public class EmployeeService {
    @ExceptionHandler(IllegalArgumentException.class)  // 错误!
    public void handleException() {
        // ...
    }
}

错误现象:注解不生效,异常不被捕获。

纠正:@ExceptionHandler 只能标注在 Controller 类的方法上。Service 层、Repository 层的方法上标注无效。Service 抛出的异常会向上传播到 Controller,由 Controller 的 @ExceptionHandler 捕获。

误区二:一个 @ExceptionHandler 捕获多个异常时匹配混乱

代码示例:

@ExceptionHandler({IllegalArgumentException.class, NullPointerException.class})
public ErrorResponse handleMultiple(Exception ex) {
    // 如何区分是哪种异常?
}

纠正:可以通过 instanceof 或方法重载来区分:

@ExceptionHandler(IllegalArgumentException.class)
public ErrorResponse handleIllegalArg(IllegalArgumentException ex) {
    return new ErrorResponse(400, "参数错误: " + ex.getMessage(), "");
}

@ExceptionHandler(NullPointerException.class)
public ErrorResponse handleNPE(NullPointerException ex) {
    return new ErrorResponse(500, "空指针异常", "");
}

误区三:@ExceptionHandler 和 try-catch 重复

错误代码:

@PostMapping
public EmployeeDTO addEmployee(@RequestBody EmployeeDTO dto) {
    try {
        // 业务逻辑
    } catch (IllegalArgumentException e) {
        // 这里 catch 了,@ExceptionHandler 就捕获不到了!
    }
}

纠正:如果希望在 @ExceptionHandler 中统一处理,Controller 方法内不要 catch 同类异常,让异常自然上抛。或者 catch 后重新抛出(throw e)。

面试高频:@ExceptionHandler 与 @ControllerAdvice 的关系

维度@ExceptionHandler(局部)@ControllerAdvice + @ExceptionHandler(全局)
作用范围仅当前 Controller所有 Controller(或指定包)
优先级高于全局低于局部(局部无匹配时才走全局)
适用场景特定 Controller 的特殊异常整个项目的通用异常处理

标准回答:异常处理遵循"就近原则"——先查找当前 Controller 的局部 @ExceptionHandler,如果没有匹配,再查找全局的 @ControllerAdvice。局部优先于全局。

面试高频:@ExceptionHandler 方法中如何获取原始请求信息

标准回答:可以通过方法参数注入:

  • HttpServletRequest request —— 获取请求 URI、方法、参数等
  • HttpServletResponse response —— 自定义响应头
  • WebRequest webRequest —— Spring 封装的通用请求对象
  • @RequestHeader —— 获取请求头(较少用)

小结

@ExceptionHandler 是 Spring MVC 的局部异常处理注解,标注在 Controller 类内部的方法上,捕获并处理该 Controller 抛出的指定异常。它把异常转换为规范的 HTTP 响应,避免堆栈信息直接暴露给客户端。局部异常处理遵循"就近原则",优先级高于全局异常处理器。

本章与全局的关系:本章讲解了 Controller 内部的异常处理。下一章"@ControllerAdvice"将讲解如何把异常处理逻辑提取到全局,统一处理所有 Controller 的异常。

上一页
文件下载
下一页
ControllerAdvice