SpringBoot 实现统一异常处理

143 阅读4分钟

前言

Spring Boot中进行统一的异常处理,包括了两种方式的处理:

  • 对 API 接口进行异常处理,统一封装返回格式
  • 对模板页面请求的异常处理,统一处理错误页面。

准备工作

(1)pom 文件

在 pom 文件中需要引入的依赖如下所示:

 <dependencies>
     <dependency>
         <!--web依赖-->
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-web</artifactId>
     </dependency>
     <!--thymeleaf依赖-->
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-thymeleaf</artifactId>
     </dependency>
     <!--测试依赖-->
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-test</artifactId>
     </dependency>
     <!--lombok依赖-->
     <dependency>
         <groupId>org.projectlombok</groupId>
         <artifactId>lombok</artifactId>
     </dependency>
 </dependencies>

(2)项目配置

在 application.yaml 配置 文件中主要是对 thymeleaf 进行配置,如下:

 spring:
   thymeleaf:
     cache: false # 关闭缓存
     mode: HTML5 # 页面是html5类型
     encoding: UTF-8 # 采用utf-8编码
     servlet:
       content-type: text/html # 文档MIME类型是text/html

(3)错误处理页面

构建一个错误页面处理视图,如下,这里的 message 表示异常信息

 <!DOCTYPE html>
 <html xmlns:th="http://www.thymeleaf.org">
 <head lang="en">
     <meta charset="UTF-8"/>
     <title>统一页面异常处理</title>
 </head>
 <body>
 <h1>统一页面异常处理</h1>
 <div th:text="${message}"></div>
 </body>
 </html>

构建项目

(1)定义响应状态枚举

响应状态枚举类,规定了 Api 接口响应的实体类型

 package com.project.enums;
 ​
 /**
  * @author 孟亚辉
  * @time 2022/10/9 16:03
  */
 public enum StatusEnum {
     /**
      * 操作成功
      */
     SUCCESS(200, "操作成功"),
     /**
      *未知异常
      */
     UNKNOWN_ERROR(500, "服务器出错啦");
 ​
     /**
      * 状态码
      */
     final int code;
     /**
      * 内容
      */
     final String msg;
 ​
     StatusEnum(int code, String message) {
         this.code = code;
         this.msg = message;
     }
 ​
     public int getCode() {
         return code;
     }
 ​
     public String getMsg() {
         return msg;
     }
 }

(2)封装 Api 响应实体

 package com.project.domain;
 ​
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.project.enums.StatusEnum;
 import com.project.exception.SystemException;
 import lombok.Data;
 ​
 /**
  * @author 孟亚辉
  * @time 2022/10/9 16:01
  */
 @Data
 @JsonInclude(JsonInclude.Include.NON_NULL)// 为 null 的字段不序列化
 public class ApiResult {
     private Integer code;
     private String msg;
     private Object data;
 ​
     private ApiResult() {
 ​
     }
 ​
     public ApiResult(Integer code, String msg, Object data) {
         this.code = code;
         this.msg = msg;
         this.data = data;
     }
 ​
     /**
      * 构造一个自定义的响应实体
      *
      * @param code    状态码
      * @param message 返回内容
      * @param data    返回数据
      * @return ResponseResult
      */
     public static ApiResult of(Integer code, String message, Object data) {
         return new ApiResult(code, message, data);
     }
 ​
     /**
      * 构造一个有状态且带数据的响应实体返回
      *
      * @param status 状态枚举
      * @param data   响应数据
      * @return ResponseResult
      */
     public static ApiResult ofStatus(StatusEnum status, Object data) {
         return of(status.getCode(), status.getMsg(), data);
     }
 ​
     /**
      * 构造一个有状态的响应实体返回
      *
      * @param status 响应状态
      * @return ResponseResult
      */
     public static ApiResult ofStatus(StatusEnum status) {
         return ofStatus(status, null);
     }
 ​
     /**
      * 构造一个成功且带数据的响应实体返回
      *
      * @param data 响应数据
      * @return ResponseResult
      */
     public static ApiResult ofSuccess(Object data) {
         return ofStatus(StatusEnum.SUCCESS, data);
     }
 ​
     /**
      * 构造一个成功且自定义消息的响应实体返回
      *
      * @param message 响应消息
      * @return ResponseResult
      */
     public static ApiResult ofMessage(String message) {
         return of(StatusEnum.SUCCESS.getCode(), message, null);
     }
 ​
     /**
      * 构造一个异常且带数据的响应实体返回
      *
      * @param t    异常
      * @param data 响应数据
      * @param <T>  {@link SystemException} 的子类
      * @return ResponseResult
      */
     public static <T extends SystemException> ApiResult ofException(T t, Object data) {
         return of(t.getCode(), t.getMsg(), data);
     }
 ​
     /**
      * 构造一个异常的响应实体返回
      *
      * @param t   异常
      * @param <T> {@link SystemException} 的子类
      * @return ResponseResult
      */
     public static <T extends SystemException> ApiResult ofException(T t) {
         return ofException(t, t.getMsg());
     }
 }

(3)封装异常

上面在进行封装 Api 响应实体时,使用了 SystemException ,此异常为自定义异常,往下看就会知道它的妙用。

 package com.project.exception;
 ​
 import com.project.enums.StatusEnum;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 ​
 /**
  * @author 孟亚辉
  * @time 2022/10/9 16:18
  */
 @Data
 @EqualsAndHashCode(callSuper = true)
 public class SystemException extends RuntimeException{
 ​
     private Integer code;
     private String msg;
 ​
 ​
     public SystemException(StatusEnum result) {
         this.code = result.getCode();
         this.msg = result.getMsg();
     }
 ​
     public SystemException(Integer code, String msg) {
         this.code = code;
         this.msg = msg;
     }
 }

它继承了 RuntimeException 所以,项目在抛出 SystemException 时不会停止,所以我们可以拦截此类型的异常,获取异常信息进行处理,对于如何拦截异常下面会讲到。此外,可以看到 SystemException 的成员变量为响应状态码和响应消息,所以在构造方法中可以使用响应状态枚举类。

对于 Api 异常信息和模板页面异常信息的拦截这里分别自定义异常 JsonException 和 PageException 来处理,这两个异常均继承自 SystemException 。

  • JsonException
 package com.project.exception;
 ​
 import com.project.enums.StatusEnum;
 ​
 /**
  * @author 孟亚辉
  * @time 2022/10/9 16:55
  */
 public class JsonException extends SystemException{
     public JsonException(StatusEnum result) {
         super(result);
     }
 ​
     public JsonException(Integer code, String message) {
         super(code, message);
     }
 }
  • PageException
 package com.project.exception;
 ​
 import com.project.enums.StatusEnum;
 ​
 /**
  * @author 孟亚辉
  * @time 2022/10/9 16:56
  */
 public class PageException extends SystemException{
     public PageException(StatusEnum result) {
         super(result);
     }
     public PageException(Integer code, String message) {
         super(code, message);
     }
 }

(4)定义全局异常处理器

@ControllerAdvice 是@Controller 的增强版,主要用来处理全局数据,一般搭配 @ExceptionHandler、@ModelAttribute 以及 @InitBinder 使用。其中最常见的使用场景就是配合 @ExceptionHandler 实现全局异常处理。

抛出的 JsonException 类型的异常会被 jsonErrorHandler 方法拦截,同样抛出的 PageException 类型的异常会被 pageErrorHandler 方法拦截。

 package com.project.handler;
 ​
 import com.project.domain.ApiResult;
 import com.project.exception.JsonException;
 import com.project.exception.PageException;
 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.servlet.ModelAndView;
 ​
 /**
  * @author 孟亚辉
  * @time 2022/10/9 16:53
  */
 @Slf4j
 @ControllerAdvice
 public class GlobalExceptionHandler {
 ​
     private static final String DEFAULT_ERROR_VIEW = "error";
 ​
     @ResponseBody
     @ExceptionHandler(value = JsonException.class)
     public ApiResult jsonErrorHandler(JsonException exception) {
         log.error("【JsonException】:{}", exception.getMsg());
         return ApiResult.ofException(exception);
     }
 ​
     @ExceptionHandler(value = PageException.class)
     public ModelAndView pageErrorHandler(PageException exception){
         log.error("【PageException】:{}", exception.getMsg());
         ModelAndView view = new ModelAndView();
         view.addObject("message", exception.getMsg());
         view.setViewName(DEFAULT_ERROR_VIEW);
         return view;
     }
 }

测试

(1)测试代码

测试的代码无非是在 controller 中抛出异常,代码如下:

 package com.project.controller;
 ​
 import com.project.domain.ApiResult;
 import com.project.enums.StatusEnum;
 import com.project.exception.JsonException;
 import com.project.exception.PageException;
 import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.ResponseBody;
 import org.springframework.web.servlet.ModelAndView;
 ​
 /**
  * @author 孟亚辉
  * @time 2022/10/9 17:00
  */
 @Controller
 public class TestController {
 ​
     @GetMapping("/json")
     @ResponseBody
     public ApiResult jsonException() {
         throw new JsonException(StatusEnum.UNKNOWN_ERROR);
     }
 ​
     @GetMapping("/page")
     public ModelAndView pageException() {
         throw new PageException(StatusEnum.UNKNOWN_ERROR);
     }
 }

(2)测试结果

启动项目后,在浏览器中访问下面两个地址:

结果如下图所示:

image-20221009180823824.png

image-20221009180904587.png