ExceptionHandler
本章聚焦 Spring MVC 的局部异常处理。文件上传时可能遇到空文件、大小超限;参数绑定时可能遇到类型不匹配;业务逻辑中可能遇到各种自定义异常。如果不对这些异常做处理,Spring MVC 会把异常堆栈直接暴露给客户端,既不安全也不友好。@ExceptionHandler 允许在 Controller 内部定义异常处理方法,把异常转换为规范的 HTTP 响应。
本章与全局的关系:前面章节讲解了正常请求的处理流程,本章讲解当 Controller 抛出异常时,Spring MVC 如何在 Controller 内部捕获并处理。后续章节将扩展到全局异常处理。
定义与作用
@ExceptionHandler 是 Spring MVC 提供的局部异常处理注解,标注在 Controller 类内部的方法上,用于捕获并处理该 Controller 抛出的特定异常。它的核心职责是:
- 捕获异常:拦截 Controller 方法执行过程中抛出的指定类型异常
- 转换响应:把异常转换为规范的 HTTP 响应(如 JSON 错误对象、错误页面)
- 隔离处理:每个 Controller 独立管理自己的异常,不影响其他 Controller
生活类比:部门内部的故障处理员
想象飞翔科技的研发部:
- 研发部(Controller)有自己的故障处理员(@ExceptionHandler)
- 当小崔写的代码出 bug 抛异常时,故障处理员立即介入
- 故障处理员只处理研发部内部的问题,产品部的问题他不管
- 他会把技术错误翻译成"用户能看懂的话"(把异常转换为友好的错误响应)
关键认知:@ExceptionHandler 的作用域仅限于声明它的 Controller 类。其他 Controller 抛出的同类异常,不会被这里的 @ExceptionHandler 捕获。
核心原理
局部异常处理流程
流程详解:
- Controller 方法抛出异常
- HandlerAdapter 捕获异常,中断正常返回值处理
- DispatcherServlet 查找当前 Controller 类中是否有
@ExceptionHandler能处理该异常类型 - 找到匹配的方法后,调用它,传入异常对象作为参数
- @ExceptionHandler 方法的返回值按正常流程处理(视图渲染或消息转换)
- 如果当前 Controller 没有匹配的 @ExceptionHandler,异常继续向上抛给全局异常处理器
异常匹配规则
匹配优先级:
- 精确匹配(抛出的异常类型与
@ExceptionHandler(ExactException.class)完全一致) - 父类匹配(抛出的异常是声明类型的子类)
- 无匹配时,向上抛给全局异常处理器(如 @ControllerAdvice)
适用位置与常用属性
@ExceptionHandler 标注在 Controller 类内部的方法上:
| 属性 | 类型 | 说明 |
|---|---|---|
value | Class<? extends Throwable>[] | 要捕获的异常类型数组 |
方法参数与返回值
| 参数类型 | 说明 |
|---|---|
| 异常类型 | @ExceptionHandler 声明的异常或其子类 |
HttpServletRequest / HttpServletResponse | 原请求和响应对象 |
WebRequest | Spring 封装的通用请求对象 |
| 返回值类型 | 处理方式 |
|---|---|
String | 视图名(配合 @ResponseBody 返回字符串) |
ModelAndView | 渲染视图 |
@ResponseBody + 对象 | 通过 HttpMessageConverter 序列化 |
ResponseEntity | 完整控制状态码、头、体 |
完整示例
场景
飞翔科技员工管理系统的 EmployeeController 需要处理以下异常:
IllegalArgumentException:参数校验失败(如姓名为空)IOException:文件读写失败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
流程解析:
addEmployee方法检查 name 为空,抛出IllegalArgumentException- HandlerAdapter 捕获异常
- DispatcherServlet 查找
EmployeeController中的 @ExceptionHandler - 匹配到
handleIllegalArgument(IllegalArgumentException.class) - 调用该方法,构造
ErrorResponse @ResponseStatus(HttpStatus.BAD_REQUEST)设置状态码为 400@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 的异常。