Appearance
SpringMVC教程 - 16 统一异常处理
16.1 为什么需要异常处理
例如,我们现在定义了接口如下:
java
@GetMapping("/")
public String index(HttpServletRequest request) {
int a = 2 / 0;
return "index"; // 转发到index视图 index.html
}那么在访问接口的时候,必然报错,显示如下:

但是这个报错页面也太丑了,而且会泄露日志、服务器信息等,存在安全隐患。而且接口返回 JSON 的,我们也更期望返回 JSON 格式的错误信息,前端好解析。
所以我们就需要进行统一的,更安全的的异常管理。
16.2 传统MVC(返回视图)的异常处理
传统网站不是返回 JSON,而是返回 JSP/HTML。所以我们可以自定义错误页面,显示自定义的错误信息和比较友好的提示。
1 自定义错误页面
首先创建一个错误页面,例如 error.html :
html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>出错啦</title>
<style>
body {
font-family: sans-serif;
padding: 40px;
text-align: center;
color: #444;
}
h1 {
color: #d33;
font-weight: normal;
}
a {
color: #06c;
text-decoration: none;
}
a:hover { text-decoration: underline; }
</style>
</head>
<body>
<h1>出错啦</h1>
<p id="msg" th:text="${error}"></p>
<p><a href="/">返回首页</a></p>
</body>
</html>- 可以根据自己的想象来自定义错误页面;
- 上面还接收了错误信息,那么后面可以传递错误信息到页面中。
2 使用@ExceptionHandler局部异常处理
看一下下面的代码:
java
package com.foooor.hellospringmvc.controller;
import com.foooor.hellothymeleaf.common.Result;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Slf4j
@Controller
public class IndexController {
@GetMapping("/")
public String index(HttpServletRequest request) {
// 算术异常,抛出异常
int a = 2 / 0;
return "index"; // 转发到index视图 index.html
}
@GetMapping("/hello")
@ResponseBody
public Result hello(Model model) {
// 索引超出范围,抛出异常
String abc = "abc".substring(10);
return Result.success("For技术栈");
}
/**
* 处理运行时异常
*/
@ExceptionHandler(RuntimeException.class)
public String handleRuntimeException(RuntimeException ex, Model model) {
// 将错误信息传递到error视图
model.addAttribute("error", ex.getMessage());
return "error"; // 转发到error视图 error.html
}
/**
* 处理StringIndexOutOfBoundsException异常
*/
@ExceptionHandler(StringIndexOutOfBoundsException.class)
public String handleStringIndexOutOfBoundsException(StringIndexOutOfBoundsException ex, Model model) {
// 将错误信息传递到error视图
model.addAttribute("error", "系统异常:" + ex.getMessage());
return "error"; // 转发到error视图 error.html
}
}- 首先定义了两个接口,运行的时候会报错,抛出了两种类型的异常;
- 然后使用了
@ExceptionHandler注解定义了异常的处理,并指定处理什么类型的异常;可以看到异常处理可以定义多个; - 在处理异常的时候,会优先匹配范围小的异常,所以如果上面将
handleStringIndexOutOfBoundsException方法去掉,两个接口的异常都会被handleRuntimeException处理,并跳转到错误页面; - 这种在 Controller 中定义的处理方式,只对当前 Controller 中的接口抛出异常有效,其他 Controller 抛出的异常是没办法捕获处理的。
所以这种方式,每个 Controller 都要写,用的不多,跳过!
3 报错显示效果
上面添加了错误页面,再次请求接口,显示如下:

4 全局处理器@ControllerAdvice
很明显,我们需要一个全局的处理,对整个应用生效。
@ControllerAdvice 会扫描所有 @Controller,它是 全局控制器增强,不是异常处理专用,但在异常处理中最常用。
下面创建一个类,使用 @ControllerAdvice 注解,并搭配 @ExceptionHandler 进行处理,这样所有接口报错,都可以被捕获跳转到错误页面:
java
package com.foooor.hellospringmvc.handler;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* 全局异常处理, 捕获所有异常
*/
@ExceptionHandler(Exception.class)
public String handle(Exception e, Model model) {
model.addAttribute("error", e.getMessage());
return "error"; // 转发到error视图 error.html
}
}- 上面的异常捕获是可以定义多个的,
Exception会捕获所有的异常,如果想针对指定的异常进行处理,可以再针对指定的异常进行定义。
在传统的项目中,上面的异常处理其实还不够完善,因为只能返回到页面。而哪怕在传统项目中,也难免使用 AJAX 请求,如果 AJAX 请求出错了也返回页面,显示是不合理的。这个待会再完善。
16.3 前后端分离(返回 JSON)异常处理
下面先介绍一下在前后端分离的项目中的异常处理写法,返回 JSON 格式的错误信息。
1 统一返回结果
首先统一一下返回结果,我们之前在 HelloWorld 中已经定义了 Result 类,用来返回数据:
java
package com.foooor.hellospringmvc.common;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data // Lombok 自动生成 getter/setter 等方法
@NoArgsConstructor
@AllArgsConstructor // Lombok 自动生成全参构造方法
public class Result {
private int code;
private String message;
private Object data;
public static Result success() {
return success(null);
}
public static Result success(Object data) {
return new Result(200, "success", data);
}
public static Result error(int code, String message) {
return new Result(code, message, null);
}
}也就是说,不论成功返回结果,还是出错,我们希望都是通过 Result 来返回,这样前端可以根据返回码 code 来判断成功还是失败。
前端得到的 JSON 始终是如下格式:
json
{
"code": 500,
"message": "用户不存在",
"data": {
//...
}
}2 全局异常处理
和上面一样,定义一个全局处理器,在类上使用 @ControllerAdvice 和 @ResponseBody 注解。
java
package com.foooor.hellospringmvc.handler;
import com.foooor.hellospringmvc.common.Result;
import lombok.extern.slf4j.Slf4j;
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;
@Slf4j
@ControllerAdvice
@ResponseBody // 保证返回 JSON
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public Result handleException(Exception e) {
log.error("统一异常处理:", e);
return Result.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), "服务器内部错误");
}
}@ResponseBody标识将Result对象转换为 JSON 返回。
@ControllerAdvice + @ResponseBody 也可以合并,使用 @RestControllerAdvice 注解来代替。
java
@RestControllerAdvice
public class GlobalExceptionHandler {
}3 报错显示效果

16.4 兼容返回页面+返回 JSON
对于前后端分离的项目而言,使用上面的异常处理方式就可以了,反正也不会返回页面给前端。
而如果是传统的项目,可能也会包含 AJAX 请求的接口,除了可能返回页面,也可能返回 JSON,所以就需要兼容处理一下。
代码如下:
java
package com.foooor.hellospringmvc.handler;
import com.foooor.hellospringmvc.common.Result;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
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.RestController;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.ModelAndView;
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
@ResponseBody
public Object handle(Exception ex,
HttpServletRequest request,
HandlerMethod handlerMethod) {
log.error("SpringMVC 异常", ex);
if (shouldReturnJson(request, handlerMethod)) {
return Result.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), ex.getMessage());
}
// 2.否则返回页面
ModelAndView mv = new ModelAndView();
mv.addObject("error", ex.getMessage());
mv.setViewName("error"); // 视图名称
return mv;
}
private boolean shouldReturnJson(HttpServletRequest request,
HandlerMethod handlerMethod) {
// 1. 判断Controller / Method 标注,是否有 ResponseBody 和 RestController注解
if (handlerMethod != null &&
(handlerMethod.hasMethodAnnotation(ResponseBody.class) ||
handlerMethod.getBeanType().isAnnotationPresent(RestController.class))) {
return true;
}
// 2. AJAX 头
String xhr = request.getHeader("X-Requested-With");
if ("XMLHttpRequest".equalsIgnoreCase(xhr)) {
return true;
}
// 3. Accept 头
String accept = request.getHeader("Accept");
return accept != null && accept.contains("application/json");
}
}- 首先
HandlerMethod handlerMethod接收到的是 Controller 中的方法,然后判断这个方法是否有@ResponseBody和@RestController注解然、是否是 Ajax 请求以及是否是 json 的请求格式,如果是就以 JSON 格式返回错误信息。如果不是,就返回视图对象ModelAndView。 - 需要注意,
handleException是添加了@ResponseBody注解的,这样错误信息才能以 JSON 格式返回,但是这个@ResponseBody不会影响返回页面,因为ModelAndView类型会被自动识别并走视图渲染。 - 上面只处理了
Exception异常,如果想针对特定的异常处理,可以定义多个异常处理方法,添加@ExceptionHandler(异常类.class)进行处理即可。
16.5 自定义异常
上面我们在返回异常的时候,使用的是 Spring 自带的通用返回码,但是在实际的 Web 开发中,系统异常(例如空指针、除零、数据库连接错误)并不等于业务异常(余额不足、用户名重复、权限不够)。
为了让前端能够准确识别业务问题,并给用户展示友好的提示,我们通常需要 自定义业务异常。
1 定义统一的错误码
可以定义一些通用的错误码,也可以针对具体的业务逻辑来定义错误码。
java
package com.foooor.hellospringmvc.common;
public enum ErrorCode {
SUCCESS(0, "成功"),
SYSTEM_ERROR(1000, "系统内部错误"),
PARAM_ERROR(1001, "参数错误"),
NO_PERMISSION(1002, "无权限访问"),
USER_NOT_FOUND(1003, "用户不存在");
public final int code;
public final String msg;
ErrorCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
}- 使用枚举的方式来定义,规范且易于维护。
然后我们可以修改一下 Result 类,添加几个方法,接收 ErrorCode 作为参数,方便调用:
java
package com.foooor.hellospringmvc.common;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data // Lombok 自动生成 getter/setter 等方法
@NoArgsConstructor
@AllArgsConstructor // Lombok 自动生成全参构造方法
public class Result {
private int code;
private String message;
private Object data;
public static Result success() {
return success(null);
}
public static Result success(Object data) {
return new Result(200, "success", data);
}
public static Result error(ErrorCode errorCode) {
return new Result(errorCode.code, errorCode.msg, null);
}
public static Result error(int code, String message) {
return new Result(code, message, null);
}
public static Result error(int code, String message, Object data) {
return new Result(code, message, data);
}
}- 根据需要自己定义方法就可以了。
2 定义业务异常类
定义一个业务异常类,如果需要主动抛出异常的时候,可以在代码中抛出异常,例如查找用户查找不到可以抛出异常。
java
package com.foooor.hellospringmvc.handler;
import com.foooor.hellospringmvc.common.ErrorCode;
public class BizException extends RuntimeException {
private final ErrorCode errorCode;
public BizException(ErrorCode errorCode) {
super(errorCode.msg);
this.errorCode = errorCode;
}
public ErrorCode getErrorCode() {
return errorCode;
}
}- 继承自运行时异常类
RuntimeException。
3 全局增加业务异常处理
在全局异常处理中,增加对业务异常的处理:
java
package com.foooor.hellospringmvc.handler;
import com.foooor.hellospringmvc.common.ErrorCode;
import com.foooor.hellospringmvc.common.Result;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
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.RestController;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.ModelAndView;
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* 全局异常处理兜底
*/
@ExceptionHandler(Exception.class)
@ResponseBody
public Object handle(Exception ex,
HttpServletRequest request,
HandlerMethod handlerMethod) {
log.error("全局异常:{}", ex);
return handleException(ErrorCode.SYSTEM_ERROR, request, handlerMethod);
}
/**
* 业务异常处理
*/
@ExceptionHandler(BizException.class)
@ResponseBody
public Object handleBizException(BizException e,
HttpServletRequest request,
HandlerMethod handlerMethod) {
log.error("业务异常:{}", e);
return handleException(e.getErrorCode(), request, handlerMethod);
}
/**
* 统一异常处理
*/
private Object handleException(ErrorCode errorCode,
HttpServletRequest request,
HandlerMethod handlerMethod) {
log.error("SpringMVC 异常:{}", errorCode);
if (shouldReturnJson(request, handlerMethod)) {
return Result.error(errorCode);
}
// 2.否则返回页面
ModelAndView mv = new ModelAndView();
mv.addObject("error", errorCode.msg);
mv.setViewName("error"); // 视图名称
return mv;
}
/**
* 判断是否返回 JSON 格式异常信息
*/
private boolean shouldReturnJson(HttpServletRequest request,
HandlerMethod handlerMethod) {
// 1. Controller / Method 标注
if (handlerMethod != null &&
(handlerMethod.hasMethodAnnotation(ResponseBody.class) ||
handlerMethod.getBeanType().isAnnotationPresent(RestController.class))) {
return true;
}
// 2. AJAX 头
String xhr = request.getHeader("X-Requested-With");
if ("XMLHttpRequest".equalsIgnoreCase(xhr)) {
return true;
}
// 3. Accept 头
String accept = request.getHeader("Accept");
return accept != null && accept.contains("application/json");
}
}- 这样可以捕获主动抛出的
BizException异常,并按照统一的格式返回。 - 如果要针对不同的异常进行不同的处理,那么可以自定义多个异常类,然后针对性的进行处理。
4 在业务代码中抛出异常
上面的工作都准备好了,下面就可以在代码中抛出异常了。
java
@GetMapping("/user/{id}")
@ResponseBody
public Result getUser(@PathVariable int id) {
User user = userService.findById(id);
if (user == null) {
throw new BizException(ErrorCode.USER_NOT_FOUND);
}
return Result.success(user);
}throw new BizException(...)不需要try...catch,SpringMVC 会自动走GlobalExceptionHandler,最终返回统一格式 JSON,方便前端处理。- 也可以直接在 Service 类中抛出异常,都可以。
16.6 类型转换异常
在类型转换章节,当时说过,当类型转换失败时,SpringMVC会抛出异常,所以我们只需要在全局异常处理中捕获相关的异常,然后进行处理就可以了:
java
/**
* 类型转换异常处理
*/
@ExceptionHandler({
HttpMessageNotReadableException.class,
MissingRequestValueException.class,
MethodArgumentTypeMismatchException.class,
HttpMessageConversionException.class,
HandlerMethodValidationException.class,
ServletRequestBindingException.class,
BindException.class,
TypeMismatchException.class
})
@ResponseBody
public Object handleRequestParamException(Exception ex,
HttpServletRequest request,
HandlerMethod handlerMethod) {
log.info("参数异常:{}, e: {}", ex);
return handleException(ErrorCode.PARAM_ERROR, request, handlerMethod);
}- 异常有多个,有的参数类型不匹配、有的缺失、有的绑定失败,如果有为囊括的,可以再添加一下。
16.7 Filter中的异常处理
拦截器中的异常也是可以被全局异常处理捕获并处理的。但是 Filter 位于 Servlet 层,执行顺序在 SpringMVC 之前。因此,Filter 中发生的异常并不会直接被 @ControllerAdvice 捕捉到,需要手动处理。
我们可以添加一个 Filter,专门将这个过滤器尽量配置到前面,这样就可以尽早被调用,这样如何其他过滤器出现异常也可以捕获。
这里我创建一个 GlobalExceptionFilter.java
java
package com.foooor.hellospringmvc.filter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.foooor.hellospringmvc.common.ErrorCode;
import com.foooor.hellospringmvc.common.Result;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
public class GlobalExceptionFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
try {
chain.doFilter(req, res);
} catch (Exception e) {
if (isAjax(request)) {
writeJson(response, Result.error(ErrorCode.SYSTEM_ERROR));
} else {
// 其他异常,跳转到自定义错误页面
response.sendRedirect("/error");
}
}
}
/**
* 写 JSON 响应
*/
private void writeJson(HttpServletResponse response, Result result) throws IOException {
// 1. 一定要先判断 response 是否已经提交
if (response.isCommitted()) {
return;
}
// 2. 重置响应(避免之前的 HTML / 错误页面内容)
response.reset();
// 3. 设置状态码
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
// 4. 设置返回类型和编码
response.setContentType("application/json;charset=UTF-8");
// 5. 写 JSON
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(result);
response.getWriter().write(json);
response.getWriter().flush();
}
/**
* 判断是否是 AJAX 请求
*/
private boolean isAjax(HttpServletRequest request) {
// 1. X-Requested-With(传统 AJAX)
String xRequestedWith = request.getHeader("X-Requested-With");
if ("XMLHttpRequest".equalsIgnoreCase(xRequestedWith)) {
return true;
}
// 2. Accept 包含 application/json(fetch / axios)
String accept = request.getHeader("Accept");
if (accept != null && accept.contains("application/json")) {
return true;
}
// 3. Content-Type 是 JSON(POST / PUT)
String contentType = request.getContentType();
if (contentType != null && contentType.contains("application/json")) {
return true;
}
return false;
}
}- 在上面的 Filter 中捕获了抛出的异常,根据请求的类型,判断写出 Json 还是跳转到
/error接口。
那么我们需要写一个 /error 接口,跳转到错误页面:
java
package com.foooor.hellospringmvc.controller;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/error")
public class ErrorController {
@RequestMapping("/")
public String error() {
return "error";
}
@RequestMapping("/{code}")
public Object handleError(@PathVariable String code,
HttpServletRequest request) {
return "error/" + code;
}
}- 定义跳转到错误页面的接口,可以跳转到错误页面。
通过上面的操作,就可以捕获 Filter 中的异常了。
16.8 容器级别的异常处理
虽然上面对 SpringMVC 中和 Filter 中的异常进行了处理,但是还是有一些异常是无法捕获的,就是容器级别抛出的异常。
我们可以在 web.xml 中配置,针对不同的错误码的异常进行处理:
xml
<error-page>
<error-code>404</error-code>
<location>/error</location>
</error-page>
<error-page>
<exception-type>java.lang.Throwable</exception-type>
<location>/error</location>
</error-page>上面针对 404 异常和 Throwable 根异常类进行捕获,并跳转到错误页面。
通过上面的异常处理可以看到,异常处理体系是分层的。
- SpringMVC 负责处理业务异常,如果 SpringMVC 异常处理没有兜住异常并处理,就会向下传递;
- Filter 负责请求链的前置兜底;
- Servlet 容器负责最终失败的展示。
内容未完......