Spring MVC 异常处理详解:三种核心方式与实战指南
在 Spring MVC 应用中,异常处理是保障系统稳定性和用户体验的关键环节。Spring MVC 提供了 @ExceptionHandler、HandlerExceptionResolver、@ControllerAdvice + @ExceptionHandler 三种灵活的异常处理机制,覆盖从 “局部 Controller 异常” 到 “全局应用异常” 的全场景。从 “原理→实现→适用场景” 三个维度,彻底讲透 Spring MVC 异常处理的设计与实践。
核心概念:Spring MVC 异常处理流程
Spring MVC 的异常处理由 DispatcherServlet 主导,当 Controller 方法执行抛出异常时,DispatcherServlet 会调用 processHandlerException 方法,遍历所有 HandlerExceptionResolver(异常解析器),直到找到能处理该异常的解析器,最终生成错误响应(视图或 JSON)。
核心源码:DispatcherServlet#processHandlerException
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
ModelAndView exMv = null; for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) { exMv = resolver.resolveException(request, response, handler, ex); if (exMv != null) { break; } }
if (exMv != null) { if (exMv.isEmpty()) { request.setAttribute(EXCEPTION_ATTRIBUTE, ex); return null; } if (!exMv.hasView()) { exMv.setViewName(getDefaultViewName(request)); } WebUtils.exposeErrorRequestAttributes(request, ex, getServletName()); return exMv; }
throw ex; }
|
- 关键逻辑:异常解析器按优先级执行,第一个能处理异常的解析器会生成
ModelAndView(或直接写入响应),后续解析器不再执行。
方式 1:@ExceptionHandler —— 局部 Controller 异常处理
@ExceptionHandler 是方法级注解,用于处理当前 Controller 中抛出的指定类型异常,仅对当前 Controller 生效,无法跨 Controller 共享。
1. 基本使用:处理当前 Controller 异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody;
@Controller @RequestMapping("/local-exception") public class LocalExceptionController {
@GetMapping("/test-null") @ResponseBody public String testNullPointerException() { User user = null; return user.getName(); }
@GetMapping("/test-business") @ResponseBody public String testBusinessException() { throw new BusinessException("参数非法:用户ID不能为空"); }
@ExceptionHandler(NullPointerException.class) @ResponseBody public String handleNullPointerException(NullPointerException e) { return "局部处理:空指针异常 - " + e.getMessage(); }
@ExceptionHandler(BusinessException.class) @ResponseBody public String handleBusinessException(BusinessException e) { return "局部处理:业务异常 - " + e.getMessage(); }
static class BusinessException extends RuntimeException { public BusinessException(String message) { super(message); } } }
|
效果:
- 访问
/local-exception/test-null:返回 局部处理:空指针异常 - null;
- 访问
/local-exception/test-business:返回 局部处理:业务异常 - 参数非法:用户ID不能为空。
2. 扩展:通过基类实现多 Controller 共享
由于 @ExceptionHandler 仅作用于当前 Controller,若多个 Controller 需要共享异常处理逻辑,可将其抽离到基类,让目标 Controller 继承基类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public abstract class BaseExceptionController {
@ExceptionHandler(BusinessException.class) @ResponseBody public String handleBusinessException(BusinessException e) { return "基类处理:业务异常 - " + e.getMessage(); }
@ExceptionHandler(NullPointerException.class) @ResponseBody public String handleNullPointerException(NullPointerException e) { return "基类处理:空指针异常 - " + e.getMessage(); } }
@Controller @RequestMapping("/shared-exception") public class SharedExceptionController extends BaseExceptionController {
@GetMapping("/test") @ResponseBody public String test() { throw new BusinessException("共享异常处理测试"); } }
|
优缺点:
- 优点:实现简单,无需额外配置;
- 缺点:强依赖继承关系,灵活性低(若 Controller 已继承其他类则无法使用)。
方式 2:HandlerExceptionResolver —— 全局异常解析器
HandlerExceptionResolver 是接口级别的全局异常处理机制,Spring MVC 内置多个实现类,支持不同场景的异常处理,也可自定义解析器。
1. 内置异常解析器详解
Spring MVC 默认注册了 3 个核心解析器(按执行优先级排序),覆盖大部分常规场景:
| 解析器类名 |
核心功能 |
适用场景 |
ExceptionHandlerExceptionResolver |
处理 @ExceptionHandler 和 @ControllerAdvice 标注的异常方法 |
前后端分离 / 传统视图,支持细粒度异常处理 |
ResponseStatusExceptionResolver |
处理带有 @ResponseStatus 注解的自定义异常 |
需将异常映射为指定 HTTP 状态码(如 400、404) |
DefaultHandlerExceptionResolver |
将 Spring 标准异常(如 HttpRequestMethodNotSupportedException)映射为 HTTP 状态码 |
处理框架级异常(如请求方法不支持、参数绑定失败) |
(1)ResponseStatusExceptionResolver:异常→HTTP 状态码映射
通过 @ResponseStatus 注解标注自定义异常,ResponseStatusExceptionResolver 会自动将异常映射为指定的 HTTP 状态码(仅修改状态码,响应体需额外处理)。
实战示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController;
@ResponseStatus( code = HttpStatus.BAD_REQUEST, // HTTP 状态码:400(Bad Request) reason = "业务参数非法" // 错误原因(会显示在错误页面或响应头中) ) class BadRequestException extends RuntimeException { public BadRequestException(String message) { super(message); } }
@RestController @RequestMapping("/status-exception") public class StatusExceptionController {
@GetMapping("/test") public String test() { throw new BadRequestException("用户年龄必须大于 18"); } }
|
效果:
- 访问
/status-exception/test,响应状态码为 400,响应体(默认错误页面)包含 业务参数非法 信息;
- 前后端分离场景需配合
@ResponseBody 或全局异常处理,否则仅返回状态码,无 JSON 响应体。
(2)SimpleMappingExceptionResolver:异常→视图映射
适用于传统 JSP/Thymeleaf 项目,将异常类直接映射到指定视图(如 404 页面、500 页面),无需编写 Java 代码。
配置示例(XML):
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver"> <property name="defaultErrorView" value="error/default"/> <property name="exceptionMappings"> <props> <prop key="java.lang.NullPointerException">error/null</prop> <prop key="com.example.BusinessException">error/business</prop> </props> </property> <property name="exposeExceptionAttributes" value="true"/> </bean>
|
视图页面(JSP):
1 2 3 4 5 6 7 8
| <!-- error/business.jsp --> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <body> <h1>业务异常</h1> <p>错误信息:${exception.message}</p> <!-- 访问异常对象的 message 属性 --> </body> </html>
|
(3)自定义异常解析器
当内置解析器无法满足需求(如自定义响应格式、复杂异常逻辑)时,可继承 AbstractHandlerMethodExceptionResolver 实现自定义解析器。
实战示例(前后端分离场景):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.handler.AbstractHandlerMethodExceptionResolver;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.HashMap; import java.util.Map;
@Order(Ordered.HIGHEST_PRECEDENCE) @Component public class CustomExceptionResolver extends AbstractHandlerMethodExceptionResolver {
@Override protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { if (ex instanceof BusinessException) { BusinessException businessEx = (BusinessException) ex; Map<String, Object> result = new HashMap<>(); result.put("code", HttpStatus.BAD_REQUEST.value()); result.put("msg", businessEx.getMessage()); result.put("data", null);
try { response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(new ObjectMapper().writeValueAsString(result)); } catch (IOException e) { e.printStackTrace(); }
return new ModelAndView(); }
return null; } }
|
关键点:
@Order 注解:控制解析器执行顺序(值越小优先级越高),确保自定义解析器优先于内置解析器;
- 前后端分离场景:直接通过
response.getWriter() 写入 JSON 响应,返回空 ModelAndView 表示异常已处理。
2. 内置解析器执行顺序
Spring MVC 按以下优先级调用内置解析器:
ExceptionHandlerExceptionResolver(最高)→ 处理 @ExceptionHandler 和 @ControllerAdvice;
ResponseStatusExceptionResolver → 处理 @ResponseStatus 注解的异常;
DefaultHandlerExceptionResolver(最低)→ 处理 Spring 标准异常。
方式 3:@ControllerAdvice + @ExceptionHandler —— 全局异常处理
@ControllerAdvice 是类级别的全局注解,可结合 @ExceptionHandler 实现 “跨 Controller 的全局异常处理”,是当前前后端分离项目的首选方案(替代基类继承方式)。
1. 核心原理
@ControllerAdvice 本质是一个 “全局 Controller 增强器”,它会将类中的 @ExceptionHandler 方法注册为全局异常处理方法,当任何 Controller 抛出异常时,ExceptionHandlerExceptionResolver 会优先查找 @ControllerAdvice 中的 @ExceptionHandler 方法,若找到则处理异常,否则再查找当前 Controller 的 @ExceptionHandler 方法。
2. 基本使用:全局异常捕获
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| 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.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus;
@ControllerAdvice public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class) @ResponseBody @ResponseStatus(HttpStatus.BAD_REQUEST) public Map<String, Object> handleBusinessException(BusinessException e) { Map<String, Object> result = new HashMap<>(); result.put("code", 400); result.put("msg", "全局业务异常:" + e.getMessage()); result.put("data", null); return result; }
@ExceptionHandler(NullPointerException.class) @ResponseBody @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public Map<String, Object> handleNullPointerException(NullPointerException e) { Map<String, Object> result = new HashMap<>(); result.put("code", 500); result.put("msg", "全局空指针异常:" + e.getMessage()); result.put("data", null); return result; }
@ExceptionHandler(Exception.class) @ResponseBody @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public Map<String, Object> handleAllException(Exception e) { Map<String, Object> result = new HashMap<>(); result.put("code", 500); result.put("msg", "全局未知异常:" + e.getMessage()); result.put("data", null); return result; } }
|
效果:
- 任何 Controller 抛出
BusinessException,都会被 handleBusinessException 处理,返回 JSON 格式响应;
- 未匹配的异常(如
ClassCastException)会被 handleAllException 兜底处理。
3. 进阶:控制 @ControllerAdvice 生效范围
@ControllerAdvice 支持通过属性限制生效的 Controller 范围,避免全局生效带来的副作用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.RestController;
@ControllerAdvice(basePackages = "com.example.controller.admin")
@ControllerAdvice(annotations = RestController.class)
@ControllerAdvice(assignableTypes = {AdminController.class, UserController.class})
public class AdminGlobalExceptionHandler { @ExceptionHandler(BusinessException.class) @ResponseBody public Map<String, Object> handleAdminException(BusinessException e) { } }
|
4. 简化:@RestControllerAdvice
Spring 4.3+ 提供 @RestControllerAdvice 注解,它是 @ControllerAdvice 与 @ResponseBody 的组合注解,类中所有 @ExceptionHandler 方法默认返回 JSON,无需逐个添加 @ResponseBody:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.http.HttpStatus;
@RestControllerAdvice public class RestGlobalExceptionHandler {
@ExceptionHandler(BusinessException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public Map<String, Object> handleBusinessException(BusinessException e) { Map<String, Object> result = new HashMap<>(); result.put("code", 400); result.put("msg", e.getMessage()); return result; } }
|
异常处理优先级规则
当多种异常处理方式共存时,执行优先级遵循以下规则(从高到低):
- 当前 Controller 的
@ExceptionHandler 方法;
@ControllerAdvice 中的 @ExceptionHandler 方法;
- 自定义
HandlerExceptionResolver(按 @Order 优先级);
- 内置
HandlerExceptionResolver(ResponseStatusExceptionResolver → DefaultHandlerExceptionResolver)。
常见问题与解决方案
| 问题现象 |
可能原因 |
解决方案 |
@ExceptionHandler 不生效 |
1. 异常类型不匹配;2. 方法参数类型错误;3. 未添加 @ResponseBody |
1. 确保 @ExceptionHandler 注解的异常类型包含抛出的异常;2. 方法参数需包含异常对象;3. 前后端分离场景添加 @ResponseBody |
| 自定义解析器不执行 |
1. 未注入 Spring 容器;2. 优先级低于内置解析器;3. doResolveException 返回 null |
1. 添加 @Component 注解;2. 用 @Order(Ordered.HIGHEST_PRECEDENCE) 提高优先级;3. 确保异常类型匹配,返回非 null 的 ModelAndView 或直接写入响应 |
@ResponseStatus 不生效 |
1. 异常未被 ResponseStatusExceptionResolver 处理;2. 自定义解析器优先处理了异常 |
1. 确保无更高优先级的解析器处理该异常;2. 调整解析器优先级 |
| 响应中文乱码 |
StringHttpMessageConverter 默认编码为 ISO-8859-1 |
配置 StringHttpMessageConverter 编码为 UTF-8,或在自定义解析器中指定响应编码 response.setCharacterEncoding("UTF-8") |
总结与推荐方案
Spring MVC 三种异常处理方式各有适用场景,选择时需结合项目类型(传统视图 / 前后端分离)和需求(局部 / 全局处理):
| 处理方式 |
适用场景 |
优点 |
缺点 |
@ExceptionHandler(局部) |
单个 Controller 的特殊异常处理 |
实现简单,无额外配置 |
无法跨 Controller 共享,依赖继承 |
HandlerExceptionResolver |
传统视图项目(异常→视图映射)、复杂全局逻辑 |
全局生效,支持视图映射 |
前后端分离场景需手动处理 JSON 响应,代码繁琐 |
@ControllerAdvice + @ExceptionHandler |
前后端分离项目、全局统一异常处理 |
无继承依赖,全局生效,支持 JSON 响应,代码集中 |
需额外配置类,对新手有一定学习成本 |
推荐方案:
- 前后端分离项目:优先使用
@RestControllerAdvice + @ExceptionHandler,配合 @ResponseStatus 设置 HTTP 状态码,实现全局统一的 JSON 异常响应;
- 传统 JSP/Thymeleaf 项目:使用
SimpleMappingExceptionResolver 配置异常→视图映射,或 @ControllerAdvice + @ExceptionHandler 返回 ModelAndView;
- 特殊场景:如需要自定义响应格式或复杂逻辑,可实现自定义
HandlerExceptionResolver,并通过 @Order 控制优先级