SpringBoot 全局异常处理详解

SpringBoot 全局异常处理详解

文章目录

  !版权声明:本博客内容均为原创,每篇博文作为知识积累,写博不易,转载请注明出处。


相关地址:

系统环境:

  • Jdk 版本:jdk 8
  • SpringBoot 版本:2.1.7.RELEASE

一、本文简介

在使用 Spring 框架开发程序时经常要写接口,尤其现在 SpringBoot 更推荐用 Restful 风格来写接口,在我们写接口中经常遇见的问题就是调用接口后,执行逻辑过程中报错,产生一堆异常信息栈。遇见这种问题是程序错误处理过程中没有考虑全面,漏掉某些错误没有进行异常处理。

在正常的情况下,我们都会使用 try-catch 来处理异常从而避免发生一些未知的错误导致的不必要的损失,而在 Spring 框架中,它有没有针对异常处理有一些好的方法呢?这个问题回答当然是肯定的,Spring 框架提供了异常处理器来处理通过 Controller 接口访问应用产生的异常,只需要配置和添加一些注解就可以处理通过 Controller 接口抛出的全部的异常。

这里简单介绍下 Java 中的异常和 Spring 中如何处对异常进行处理的。

二、Java 异常概念简介

Java异常的分类

Java 标准库内建了一些通用的异常,这些类以 Throwable 为顶层父类,Throwable 又派生出 Error 类和 Exception 类:

  • Error(错误): Error 类以及他的子类的实例,一般标识 JVM 本身的错误。错误不能被程序员通过代码处理,Error 很少出现。因此,程序员应该关注Exception为父类的分支下的各种异常类。
  • Exception(异常): Exception 以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被 Java 异常处理机制使用,是异常处理的核心。

根据 JAVAC 对项目在编译时是否进行检查又可以分为 unckecked exception(非检查异常)与 checked exception(检查异常):

  • Unckecked Exception(非检查异常): Error 和 RuntimeException 以及他们的子类在编译时,发现与提醒存在异常信息,所以也就不会发现与处理这些异常。对于这些异常发生的原因多半是代码写的有问题导致,一般我们应该修正代码,而不是去通过异常处理器处理。
  • Checked Exception(检查异常): 除了 Error 和 RuntimeException 之外的其它异常,JAVAC 强制要求开发人员提前预备处理异常工作,如使用 try-catchfinally 或者 throws,在方法中要么用 try-catch 捕获并处理异常,要么用 throws 声明抛出交给上级处理,否则编译将不会通过。这样的异常一般是由开发人员的运行环境引起的,因为程序可能被运行在各种未知的环境下,而开发人员无法干预用户如何使用他编写的程序,于是开发人员就应该提前考虑到可能发生的异常信息并处理。

非检查异常如:

  • ArithmeticException
  • ClassCastException
  • ArrayIndexOutOfBoundsException
  • NullPointerException

检查异常如:

  • SQLException
  • IOException
  • ClassNotFoundException

异常类结构图

异常关键字

  • try: try 代码块中放可能发生异常的代码,当发生异常时候会调用后面的 catch 处理异常。
  • catch: 当 try 代码块中的代码出现异常时,按代码顺序依次和多个 catch 中设置的异常类型进行检测,如果类型匹配就进行异常处理。
  • throw: 用来在方法中使用其抛出指定的异常对象,当抛出异常时会检查该代码中是否有处理异常逻辑,有处理即按照处理逻辑运行,未处理时程序将抛出异常信息,然后程序终止。
  • throws: 用在方法头部中声明抛出多个异常,调用方法会提醒处理异常,不处理即不能通过编译。
  • finally: 不管异常是否出现都会执行 finally 代码块中的内容,这个代码块中主要做一些清理资源的工作,如流的关闭,数据库连接的关闭等,不推荐用于 return 返回信息。

三、JAVA 中如何自定义异常类

如果要自定义异常类可与继承 Exception 或者 RuntimeException 类即可,其中继承 Exception 类的都属于检查异常(checked exception),继承 RuntimeException 属于非检查异常。

按照惯例,自定义的异常应该总是包含如下的构造函数:

  • 无参构造函数。
  • 带有 String 参数的构造函数,并传递给父类的构造函数。
  • 带有 String 参数和 Throwable 参数,并都传递给父类构造函数。
  • 带有 Throwable 参数的构造函数,并传递给父类的构造函数。
 1public class MyException extends RuntimeException{
 2
 3    public MyException() {
 4        super();
 5    }
 6    
 7    public MyException(String message) {
 8        super(message);
 9    }
10    
11    public MyException(String message, Throwable cause) {
12        super(message, cause);
13    }
14    
15    public MyException(Throwable cause) {
16        super(cause);
17    }
18
19}

四、SpringBoot 中异常处理机制

SpringBoot 默认的异常处理机制

默认情况下 SpringBoot 为两种情况提供了不同的响应方式,一种是浏览器客户端请求一个不存在的地址,另一种是服务端内部发生错误。不管这两种情况哪一种发生,SpringBoot 都会访问 /error 地址返回对应信息,在访问 /error 时,根据 Headers 请求头中的 Accept 值进行判断,如果是 Accept: text/html 则判断为网页访问,这样将会根据状态码返回对应的 HTML 错误页面。如果 Headers 请求头中不存在 Accept 属性或者的值不为 text/html,那么将会返回一个包含错误信息的 JSON 对象。

这里用 Postman 工具调用一个会抛出异常的 SpringBoot 接口进行测试:

  • Headers 不带 Accept 调用接口测试
1<html>
2    <body>
3        <h1>Whitelabel Error Page</h1>
4        <p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>
5        <div id='created'>Sat Aug 10 00:34:01 CST 2019</div>
6        <div>There was an unexpected error (type=Method Not Allowed, status=405).</div>
7        <div>Request method &#39;POST&#39; not supported</div>
8    </body>
9</html>
  • Headers 带 Accept 并且值为 "text/html" 调用接口测试
1{
2    "timestamp": "2019-08-09T16:39:21.715+0000",
3    "status": 405,
4    "error": "Method Not Allowed",
5    "message": "Request method 'POST' not supported",
6    "path": "/exception"
7}

可以看到分别返回了 HTML 与 JSON,由此推测 SpringBoot 确实根据 Accept 中的值是否为 "text/html" 进行判断是返回 HTML 错误页面还是返回 JSON 信息。

俗话说耳听为虚眼见为实,还是去看看 SpringBoot 中默认异常处理源码 BasicErrorController 类,里面包含了 SpringBoot 默认异常处理逻辑,源码如下:

BasicErrorController 类在 SpringBoot 官方 Github 地址为:BasicErrorController.java

 1@Controller
 2@RequestMapping({"${server.error.path:${error.path:/error}}"})
 3public class BasicErrorController extends AbstractErrorController {
 4    ......
 5    
 6    public String getErrorPath() {
 7        return this.errorProperties.getPath();
 8    }
 9
10    /**
11     * 根据状态码,返回对应 HTML 错误页面。
12     */
13    @RequestMapping(
14        produces = {"text/html"}
15    )
16    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
17        HttpStatus status = this.getStatus(request);
18        Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML)));
19        response.setStatus(status.value());
20        ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
21        return modelAndView != null ? modelAndView : new ModelAndView("error", model);
22    }
23
24    /**
25     * 使用 JSON 返回错误信息。
26     */
27    @RequestMapping
28    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
29        Map<String, Object> body = this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.ALL));
30        HttpStatus status = this.getStatus(request);
31        return new ResponseEntity(body, status);
32    }
33
34    ......
35}

使用 ControllerAdvice 注解实现全局异常处理

Spring 框架提供了 @ControllerAdvice 注解,它作用于全部 Controller 控制器,ControllerAdvice 字面上意思是 "控制器通知",Advice 即是 "劝告"、"意见" 和 "通知"的意思。注解 @ControllerAdvice 是在类上声明的,可以与添加其注解的类中的方法结合,在方法上可以添加 @ExceptionHandler@InitBinder@ModelAttribute 三种注解与不同的方法结合有不同的作用如下:

  • 与 @ModelAttribute 注解,表示其标注的方法将会在目标Controller方法执行之前执行。
  • 与 @InitBinder 注解,用于request中自定义参数解析方式进行注册,从而达到自定义指定格式参数的目的。
  • 与 @ExceptionHandler 注解,用于捕获Controller中抛出的指定类型的异常,从而达到不同类型的异常区别处理的目的。

这里主要使用 @ControllerAdvice 结合 @ExceptionHandler 注解,来对程序中的异常进行处理,下面是一个简单的使用示例:

 1@ControllerAdvice
 2public class GlobalExceptionHandler {
 3
 4    /**
 5     * 全局异常处理器
 6     * @param e
 7     */
 8    @ResponseBody
 9    @ExceptionHandler(Exception.class)
10    public Object globleExceptionHandler(Exception e){
11        Map<String,Object> responseInfo = new HashMap<>();
12        responseInfo.put("异常名称","NullPointException");
13        responseInfo.put("错误信息",e.getMessage());
14        return responseInfo;
15    }
16    
17}

五、SpringBoot 全局异常处理

1、Maven 引入相关依赖

Maven 引入两个依赖:

  • SpringBoot Web:创建 SpringBoot Web 程序必须的依赖。
  • Lombok:引入该依赖能够简化实体类的创建。
 1<?xml version="1.0" encoding="UTF-8"?>
 2<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 3         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 4    <modelVersion>4.0.0</modelVersion>
 5    <parent>
 6        <groupId>org.springframework.boot</groupId>
 7        <artifactId>spring-boot-starter-parent</artifactId>
 8        <version>2.1.7.RELEASE</version>
 9    </parent>
10    <groupId>club.mydlq</groupId>
11    <artifactId>springboot-exception-handler</artifactId>
12    <version>0.0.1-SNAPSHOT</version>
13    <name>springboot-exception-handler</name>
14    <description>Demo project for Spring Boot</description>
15
16    <properties>
17        <java.version>1.8</java.version>
18    </properties>
19
20    <dependencies>
21        <dependency>
22            <groupId>org.springframework.boot</groupId>
23            <artifactId>spring-boot-starter-web</artifactId>
24        </dependency>
25        <dependency>
26            <groupId>org.projectlombok</groupId>
27            <artifactId>lombok</artifactId>
28        </dependency>
29    </dependencies>
30
31    <build>
32        <plugins>
33            <plugin>
34                <groupId>org.springframework.boot</groupId>
35                <artifactId>spring-boot-maven-plugin</artifactId>
36            </plugin>
37        </plugins>
38    </build>
39
40</project>

2、创建响应枚举类

提前创建好一个枚举类,这里面定义两个属性,分别是 codemessage ,然后再定义多个成功与异常标识的常量,这样来简化开发中多个地方都要输入重复的异常信息的问题。

 1/**
 2 * 定义返回码的枚举类
 3 */
 4public enum ResultEnum {
 5
 6    /** 成功 */
 7    SUCCESS(1000, "成功"),
 8    /** 无法找到资源错误 */
 9    NOT_FOUNT_RESOURCE(1001,"没有找到相关资源!"),
10    /** 请求参数有误 */
11    PARAMETER_ERROR(1002,"请求参数有误!"),
12    /** 确少必要请求参数异常 */
13    PARAMETER_MISSING_ERROR(1003,"确少必要请求参数!"),
14    /** 确少必要请求参数异常 */
15    REQUEST_MISSING_BODY_ERROR(1004,"缺少请求体!"),
16    /** 未知错误 */
17    SYSTEM_ERROR(9998,"未知的错误!"),
18    /** 系统错误 */
19    UNKNOWN_ERROR(9999,"未知的错误!");
20
21    private Integer code;
22    private String message;
23
24    ResultEnum(Integer code, String message) {
25        this.code = code;
26        this.message = message;
27    }
28
29    public Integer getCode() {
30        return code;
31    }
32    public String getMessage() {
33        return message;
34    }
35
36}

3、创建响应信息实体类

提前创建对接口调用放响应的实体类,在成功调用接口或者发生异常时,都应通过该响应实体类进行反馈信息,在此类中设置三个属性,分别为 code(返回码)、message(错误信息)和 result(返回结果)。

  • 如果发生异常就将异常返回码和异常信息存入 codemessage 属性中,进行响应。
  • 如果调用成功则将成功对应的返回码与返回消息存入 codemessage 属性中,之后将“响应数据”存入 Object 类型的 result 属性中,进行响应。

下面实体类使用了 Lombok 依赖和插件来减少代码量,其中 @Accessors(chain = true) 能让我们用链式方向对实体类象赋值,@Data 能让我们不用谢 Get 和 Set 方法。

 1import lombok.Data;
 2import lombok.experimental.Accessors;
 3
 4/**
 5 * 自定义返回实体类
 6 */
 7@Data
 8@Accessors(chain = true)
 9public class ResponseInfo {
10
11    /** 错误码 */
12    private Integer code;
13    /** 错误信息 */
14    private String message = "";
15    /** 返回结果 */
16    private Object data = "";
17
18}

4、创建异常类

这里创建一个自定义异常类,在平时很多时候 Java 自带的异常类就不足以处理我们的业务逻辑,所以为了方便测试,这里提前定义一下自定义异常类。

MyException 类

 1public class MyException extends RuntimeException{
 2
 3    public MyException() {
 4        super();
 5    }
 6
 7    public MyException(String message) {
 8        super(message);
 9    }
10
11    public MyException(String message, Throwable cause) {
12        super(message, cause);
13    }
14
15    public MyException(Throwable cause) {
16        super(cause);
17    }
18
19}

NotFountResourceException 类

 1public class NotFountResourceException extends RuntimeException{
 2
 3    public NotFountResourceException() {
 4        super();
 5    }
 6
 7    public NotFountResourceException(String message) {
 8        super(message);
 9    }
10
11    public NotFountResourceException(String message, Throwable cause) {
12        super(message, cause);
13    }
14
15    public NotFountResourceException(Throwable cause) {
16        super(cause);
17    }
18
19}

5、创建 Controller

创建一个 Controller 类并提供接接口,一个为正常调用不会产生异常的接口,其它几个接口中分别模拟创建不同的异常,并且将异常信息抛出给上级处理。

 1import club.mydlq.exceptionhandler.entity.ResponseInfo;
 2import club.mydlq.exceptionhandler.enums.ResultEnum;
 3import club.mydlq.exceptionhandler.exception.MyException;
 4import club.mydlq.exceptionhandler.exception.NotFountResourceException;
 5import org.springframework.web.bind.annotation.GetMapping;
 6import org.springframework.web.bind.annotation.RestController;
 7
 8/**
 9 * 测试 Controller
10 */
11@RestController
12public class TestController {
13
14    /**
15     * 正常接口
16     */
17    @GetMapping("/normal")
18    public ResponseInfo normal() {
19        String data = "模拟的响应数据";
20        return new ResponseInfo().setCode(ResultEnum.SUCCESS.getCode())
21                .setMessage(ResultEnum.SUCCESS.getMessage())
22                .setData(data);
23    }
24
25    /**
26     * 抛出自定义不能发行资源异常,测试自定义无法发现资源异常处理器
27     */
28    @GetMapping("/rsexception")
29    public String createException() throws NotFountResourceException {
30        throw new NotFountResourceException("NullPointerException 空指针异常信息");
31    }
32
33    /**
34     * 抛出自定义异常,测试自定义异常处理器
35     */
36    @GetMapping("/myexception")
37    public String createMyException() throws MyException {
38        throw new MyException("MyException 自定义异常信息");
39    }
40
41    /**
42     * 抛出空指针异常,测试全局异常处理器
43     */
44    @GetMapping("/exception")
45    public String createGlobleException() {
46        throw new NullPointerException("NullPointerException 空指针异常信息");
47    }
48
49}

6、创建异常处理器类

异常处理类

写一个异常处理器类加上 @RestControllerAdvice 注解,它的功能和 @ControllerAdvice 注解几乎一致,都是用于捕获异常信息,唯一的区别就是在该类中的所有方法不需要再单独添加 @ResponseBody 注解来标识将方法返回的信息转换成 JSON 字符串。

在类中创建几个方法并在方法上单独加上 @ExceptionHandler 注解,这样来表示该方法是用于处理指定异常的,当有异常信息从 Controller 层抛出被捕获后,将会根据异常类的不同而到不同的异常处理器中进行异常处理。

在异常发生且没有在方法中捕获往上级抛出时,Spring 会在此类中寻找有没有专门处理该抛出的异常的处理器,如果存在就交由此异常处理器处理,如果不存该异常的异常处理器,那么会检查是否存在指定处理 Exception 异常的异常处理器,因为 Exception 是除 Error 外的全部异常类的父类,所以它能处理在 Spring 中调用 Controller 层进而导致异常并抛出的全部异常信息。

 1import club.mydlq.exceptionhandler.enums.ResultEnum;
 2import club.mydlq.exceptionhandler.entity.ResponseInfo;
 3import club.mydlq.exceptionhandler.exception.MyException;
 4import club.mydlq.exceptionhandler.exception.NotFountResourceException;
 5import org.springframework.http.HttpStatus;
 6import org.springframework.http.converter.HttpMessageNotReadableException;
 7import org.springframework.web.bind.MethodArgumentNotValidException;
 8import org.springframework.web.bind.MissingServletRequestParameterException;
 9import org.springframework.web.bind.annotation.*;
10import javax.servlet.http.HttpServletResponse;
11
12/**
13 * 异常处理器
14 */
15//@RestControllerAdvice代替@ControllerAdvice,这样在方法上就可以不需要添加@ResponseBody
16@RestControllerAdvice(basePackages = "club.mydlq.exceptionhandler.controller")
17public class GlobalExceptionHandler {
18
19    /**
20     * 全局异常处理器
21     *
22     * @param e Exception
23     * @return ResponseInfo
24     */
25    @ResponseBody
26    @ExceptionHandler(Exception.class)
27    public ResponseInfo globleExceptionHandler(Exception e, HttpServletResponse response) {
28        // 判断是否为 MyException 异常
29        if (e instanceof MyException) {
30            // 设置 HTTP 状态码
31            response.setStatus(HttpStatus.BAD_REQUEST.value());
32            // 返回错误信息
33            return new ResponseInfo()
34                    .setMessage(e.getMessage())
35                    .setCode(ResultEnum.PARAMETER_ERROR.getCode());
36        }
37        // 判断是否为 NotFountResourceException 异常
38        else if (e instanceof NotFountResourceException) {
39            response.setStatus(HttpStatus.BAD_REQUEST.value());
40            return new ResponseInfo()
41                    .setCode(ResultEnum.NOT_FOUNT_RESOURCE.getCode())
42                    .setMessage(e.getMessage());
43        }
44        // 判断是否为丢失请求参数异常
45        else if (e instanceof MissingServletRequestParameterException){
46            response.setStatus(HttpStatus.BAD_REQUEST.value());
47            return new ResponseInfo()
48                    .setCode(ResultEnum.PARAMETER_MISSING_ERROR.getCode())
49                    .setMessage(ResultEnum.PARAMETER_MISSING_ERROR.getMessage());
50        }
51        // 判断是否为缺少请求体异常
52        else if (e instanceof HttpMessageNotReadableException){
53            response.setStatus(HttpStatus.BAD_REQUEST.value());
54            return new ResponseInfo()
55                    .setCode(ResultEnum.REQUEST_MISSING_BODY_ERROR.getCode())
56                    .setMessage(ResultEnum.REQUEST_MISSING_BODY_ERROR.getMessage());
57        }
58        // 判断是否为请求参数错误异常
59        else if (e instanceof MethodArgumentNotValidException){
60            response.setStatus(HttpStatus.BAD_REQUEST.value());
61            return new ResponseInfo()
62                    .setCode(ResultEnum.PARAMETER_ERROR.getCode())
63                    .setMessage(ResultEnum.PARAMETER_ERROR.getMessage());
64        }
65        // 不知道异常原因,默认返回未知异常
66        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
67        return new ResponseInfo()
68                .setCode(ResultEnum.UNKNOWN_ERROR.getCode())
69                .setMessage(ResultEnum.UNKNOWN_ERROR.getMessage());
70    }
71
72}

404错误处理类

由于在 SpringBoot 中已经对 404、500 这类错误进行了处理,这里我们想要对这些错误进行自己的处理就必须按下方式来进行改造,继承 ErrorController 类,重写其中的 getErrorPath() 方法,指定发送这类错误时跳转的路径。这里对这个路径进行重写,然后再写一个 Controller 处理器,返回错误信息。

 1import club.mydlq.exceptionhandler.entity.ResponseInfo;
 2import club.mydlq.exceptionhandler.enums.ResultEnum;
 3import org.springframework.boot.web.servlet.error.ErrorController;
 4import org.springframework.http.HttpStatus;
 5import org.springframework.web.bind.annotation.GetMapping;
 6import org.springframework.web.bind.annotation.ResponseStatus;
 7import org.springframework.web.bind.annotation.RestController;
 8import javax.servlet.http.HttpServletRequest;
 9
10/**
11 * 用于处理404、500错误
12 */
13@RestController
14public class ErrorHandler implements ErrorController {
15
16    @Override
17    public String getErrorPath() {
18        return "/error";
19    }
20
21    @GetMapping("/error")
22    @ResponseStatus(code = HttpStatus.NOT_FOUND)
23    public ResponseInfo handleError(HttpServletRequest request) {
24        Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
25        // 如果等于 404 错误,则抛出设定的枚举类中的错误信息
26        if (HttpStatus.NOT_FOUND.value() == statusCode) {
27            return new ResponseInfo()
28                    .setMessage(ResultEnum.NOT_FOUNT_RESOURCE.getMessage())
29                    .setCode(ResultEnum.NOT_FOUNT_RESOURCE.getCode());
30        }
31        // 如果非404,那就是500错误,则抛出设定的枚举类中的系统错误信息
32        return new ResponseInfo()
33                .setMessage(ResultEnum.SYSTEM_ERROR.getMessage())
34                .setCode(ResultEnum.SYSTEM_ERROR.getCode());
35    }
36
37}

7、创建启动类

 1import org.springframework.boot.SpringApplication;
 2import org.springframework.boot.autoconfigure.SpringBootApplication;
 3
 4@SpringBootApplication
 5public class Application {
 6
 7    public static void main(String[] args) {
 8        SpringApplication.run(Application.class, args);
 9    }
10
11}

8、对接口进行测试,观察结果分析

在上面 Controller 类中定义了四个接口,分别为:

  • /normal: 正常接口信息,不抛出异常。
  • /rsexception: 抛出一个自定义的异常,并在 ExcepitonHandler 类中设置了单独对此异常进行处理的异常处理器。
  • /myexception: 抛出一个自定义的异常,并在 ExcepitonHandler 类中设置了单独对此异常进行处理的异常处理器。
  • /exception: 抛出一个空指针异常,在 ExcepitonHandler 类中没有单独针对此异常做处理器,所以调用此接口用于测试全局异常捕捉。

接口:/normal:

1{
2    "code": 1000,
3    "message": "成功",
4    "data": "正常"
5}

接口:/rsexception:

1{
2    "code": 1001,
3    "message": "NullPointerException 空指针异常信息",
4    "data": ""
5}

接口:/myexception:

1{
2    "code": 1002,
3    "message": "MyException 自定义异常信息",
4    "data": ""
5}

接口:/exception:

1{
2    "code": 9999,
3    "message": "未知的错误!",
4    "data": ""
5}

调用上面的接口分别得到了异常处理器返回的 Json 格式的消息,根据测试结果可知,当外部访问 Controller 层接口时,如果发生异常则应设置将异常上抛给异常处理器处理,检测如果存在出现的异常类型监听的异常处理器则进行异常处理,如果不存在则继续查是否存在异常的父类 Exception 异常处理器,存在则进行处理,不存在则将异常信息丢弃。

到此 SpringBoot 中全局异常处理文章结束。

---END---


  !版权声明:本博客内容均为原创,每篇博文作为知识积累,写博不易,转载请注明出处。