0%

springmvc异常处理

Spring MVC 异常处理详解:三种核心方式与实战指南

在 Spring MVC 应用中,异常处理是保障系统稳定性和用户体验的关键环节。Spring MVC 提供了 @ExceptionHandlerHandlerExceptionResolver@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 {

// 1. 业务方法:故意抛出异常
@GetMapping("/test-null")
@ResponseBody
public String testNullPointerException() {
User user = null;
return user.getName(); // 抛出 NullPointerException
}

@GetMapping("/test-business")
@ResponseBody
public String testBusinessException() {
throw new BusinessException("参数非法:用户ID不能为空"); // 自定义业务异常
}

// 2. @ExceptionHandler:处理当前 Controller 的 NullPointerException
@ExceptionHandler(NullPointerException.class)
@ResponseBody // 返回 JSON,而非视图
public String handleNullPointerException(NullPointerException e) {
return "局部处理:空指针异常 - " + e.getMessage();
}

// 3. @ExceptionHandler:处理当前 Controller 的 BusinessException
@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 {

// 处理所有 Controller 共享的异常(如 BusinessException)
@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
@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;

// 1. 自定义异常:添加 @ResponseStatus 注解
@ResponseStatus(
code = HttpStatus.BAD_REQUEST, // HTTP 状态码:400(Bad Request)
reason = "业务参数非法" // 错误原因(会显示在错误页面或响应头中)
)
class BadRequestException extends RuntimeException {
public BadRequestException(String message) {
super(message);
}
}

// 2. Controller 抛出异常
@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
<!-- Spring MVC 配置文件 -->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<!-- 默认错误视图(所有未匹配的异常都跳转到该视图) -->
<property name="defaultErrorView" value="error/default"/>
<!-- 异常类→视图映射(key:异常全类名,value:视图名) -->
<property name="exceptionMappings">
<props>
<prop key="java.lang.NullPointerException">error/null</prop>
<prop key="com.example.BusinessException">error/business</prop>
</props>
</property>
<!-- 将异常对象暴露到视图中,属性名默认为 "exception" -->
<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 控制解析器优先级(值越小,优先级越高)
@Order(Ordered.HIGHEST_PRECEDENCE)
@Component // 注入 Spring 容器,使其被 DispatcherServlet 识别
public class CustomExceptionResolver extends AbstractHandlerMethodExceptionResolver {

// 核心方法:处理异常并生成响应
@Override
protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
// 1. 判断异常类型
if (ex instanceof BusinessException) {
BusinessException businessEx = (BusinessException) ex;
// 2. 构建 JSON 响应(前后端分离场景)
Map<String, Object> result = new HashMap<>();
result.put("code", HttpStatus.BAD_REQUEST.value()); // 400
result.put("msg", businessEx.getMessage());
result.put("data", null);

// 3. 写入响应体(避免返回 ModelAndView)
try {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(new ObjectMapper().writeValueAsString(result));
} catch (IOException e) {
e.printStackTrace();
}

// 返回空 ModelAndView,表示已处理异常
return new ModelAndView();
}

// 非目标异常,返回 null,交给其他解析器处理
return null;
}
}
关键点:
  • @Order 注解:控制解析器执行顺序(值越小优先级越高),确保自定义解析器优先于内置解析器;
  • 前后端分离场景:直接通过 response.getWriter() 写入 JSON 响应,返回空 ModelAndView 表示异常已处理。

2. 内置解析器执行顺序

Spring MVC 按以下优先级调用内置解析器:

  1. ExceptionHandlerExceptionResolver(最高)→ 处理 @ExceptionHandler@ControllerAdvice
  2. ResponseStatusExceptionResolver → 处理 @ResponseStatus 注解的异常;
  3. 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:全局 Controller 增强器(默认作用于所有 Controller)
@ControllerAdvice
public class GlobalExceptionHandler {

// 1. 处理自定义业务异常(返回 JSON)
@ExceptionHandler(BusinessException.class) // 指定处理的异常类型
@ResponseBody // 返回 JSON 响应
@ResponseStatus(HttpStatus.BAD_REQUEST) // 设置 HTTP 状态码为 400
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;
}

// 2. 处理空指针异常(返回 JSON)
@ExceptionHandler(NullPointerException.class)
@ResponseBody
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // 500 服务器内部错误
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;
}

// 3. 处理所有未匹配的异常(兜底处理)
@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;

// 方式1:仅作用于指定包下的 Controller
@ControllerAdvice(basePackages = "com.example.controller.admin")

// 方式2:仅作用于带有 @RestController 注解的 Controller
@ControllerAdvice(annotations = RestController.class)

// 方式3:仅作用于指定类的子类
@ControllerAdvice(assignableTypes = {AdminController.class, UserController.class})

public class AdminGlobalExceptionHandler {
// 仅处理上述范围 Controller 的异常
@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 = @ControllerAdvice + @ResponseBody
@RestControllerAdvice
public class RestGlobalExceptionHandler {

// 无需添加 @ResponseBody,默认返回 JSON
@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;
}
}

异常处理优先级规则

当多种异常处理方式共存时,执行优先级遵循以下规则(从高到低):

  1. 当前 Controller 的 @ExceptionHandler 方法;
  2. @ControllerAdvice 中的 @ExceptionHandler 方法;
  3. 自定义 HandlerExceptionResolver(按 @Order 优先级);
  4. 内置 HandlerExceptionResolverResponseStatusExceptionResolverDefaultHandlerExceptionResolver)。

常见问题与解决方案

问题现象 可能原因 解决方案
@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 响应,代码集中 需额外配置类,对新手有一定学习成本

推荐方案:

  1. 前后端分离项目:优先使用 @RestControllerAdvice + @ExceptionHandler,配合 @ResponseStatus 设置 HTTP 状态码,实现全局统一的 JSON 异常响应;
  2. 传统 JSP/Thymeleaf 项目:使用 SimpleMappingExceptionResolver 配置异常→视图映射,或 @ControllerAdvice + @ExceptionHandler 返回 ModelAndView
  3. 特殊场景:如需要自定义响应格式或复杂逻辑,可实现自定义 HandlerExceptionResolver,并通过 @Order 控制优先级

欢迎关注我的其它发布渠道