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

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 异常

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 继承基类:

// 异常处理基类
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 状态码(仅修改状态码,响应体需额外处理)。

实战示例:
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):
<!-- 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):
<!-- 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 实现自定义解析器。

实战示例(前后端分离场景):
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. 基本使用:全局异常捕获

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 范围,避免全局生效带来的副作用:

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

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 控制优先级