Author:Gorit
date:2020/6/26
SpringBoot 版本: 2.3.1
开发工具:idea 2019 3

一、全局异常处理介绍

在项目开发的过程中,总是会有各种我们意想不到的异常发生,和数据库操作,业务层操作,控制层操作。都有可能遇到异常需要处理,如果每个异常都要需要我们单独处理,那么我们岂不是遇到一个问题,就要自己手动处理一个异常,这样到最后我们的程序之间的耦合还是增大了不少,这也是我们不希望看到的。所有就有了 全局异常处理 的概念。

使用全局异常处理 就能够拦截我们项目中出现得异常,这样也避免了大量的 try catch,也实现了 异常信息的统一处理和维护,同时,我们不希望把异常抛给给用户,应该对异常的错误信息进行封装,然后返回一个友好的信息给用户。

二、全局异常处理之定义统一的 JSON 结构

在 3.3.2 中,我们讲过如何返回一个 JSON 格式的数据,大家不会的可以回到上一节去看。

下面就是我定义好的一个全局异常处理的类

  1. package cn.gorit.common.lang;
  2. /**
  3. * 封装统一结果集返回数据
  4. * */
  5. public class Result {
  6. private Object data;
  7. private String msg;
  8. private int code;
  9. public Object getData() {
  10. return data;
  11. }
  12. public void setData(Object data) {
  13. this.data = data;
  14. }
  15. public String getMsg() {
  16. return msg;
  17. }
  18. public void setMsg(String msg) {
  19. this.msg = msg;
  20. }
  21. public int getCode() {
  22. return code;
  23. }
  24. public void setCode(int code) {
  25. this.code = code;
  26. }
  27. // 操作成功返回数据
  28. public static Result succ(Object data) {
  29. return succ(200, "操作成功", data);
  30. }
  31. public static Result succ(int code, String msg, Object data) {
  32. Result r = new Result();
  33. r.setCode(code);
  34. r.setMsg(msg);
  35. r.setData(data);
  36. return r;
  37. }
  38. public static Result succ(String msg, Object data) {
  39. return succ(200,msg,data);
  40. }
  41. // 操作异常返回
  42. public static Result fail(int code, String msg, Object data) {
  43. Result r = new Result();
  44. r.setCode(code);
  45. r.setMsg(msg);
  46. r.setData(data);
  47. return r;
  48. }
  49. public static Result fail(String msg) {
  50. return fail(400,msg,null);
  51. }
  52. public static Result fail(int code, String msg) {
  53. return fail(code,msg,"null");
  54. }
  55. public static Result fail(String msg, Object data) {
  56. return fail(400,msg,data);
  57. }
  58. }

三、编写全局异常处理类 (GlobalExceptionHandler)

在这里要讲一下一个用到的注解:@RestControllerAdvice

写过 Controller 的我们知道, RestController 注解会将我们返回的内容转换成 JSON 格式返回回去。

接下来我们看看这个注解包含了哪些内容
image.png

我们发现这个注解包含了 @ControllerAdvice 和 @ResponseBody

然后接着看 @ControllerAdvice 包含了哪些
image.png
我们看到了 Compoent 注解,因此这个注解是被 Spring 所管理的。接着看它的属性,有个叫做 basePackages 的属性,在网上查阅资料得知,这个属性是用来来接收来自哪个包的异常信息,一般我们不指定这个属性,我们拦截整个工程当中的所有异常信息。

具体怎么使用呢?
在方法上添加 @ExceptionHandler 注解来指定具体的异常,然后在方法中处理该异常信息,然后通过统一结果集,返回 JSON 数据给调用者。

四、全局异常处理使用示例

4.1 处理参数缺失异常

前后端分离的项目架构当中,前端请求后台的接口是通过 rest 风格调用的,我们一般发起网络请求,使用 get 或者 post 通常都会携带一些参数。但是这些参数往往会出现漏掉的情况,此时我们需要定义一个参数缺失异常的方法,来给前端调用者返回一个友好信息。

参数缺失会抛出 HttpMessageNotReadableException 异常,我们可以拦截这个异常,并做一个友好的处理返回给前端。

我们接下来定义一个全局异常处理的类 GlobalExceptionHandler

  1. package cn.gorit.common.exception;
  2. import cn.gorit.common.lang.Result;
  3. import org.slf4j.Logger;
  4. import org.slf4j.LoggerFactory;
  5. import org.springframework.http.HttpStatus;
  6. import org.springframework.web.bind.MissingServletRequestParameterException;
  7. import org.springframework.web.bind.annotation.*;
  8. /**
  9. * ControllerAdvice 注解的功能,能拦截所有的异常
  10. * */
  11. @RestControllerAdvice
  12. public class GlobalExceptionHandler {
  13. // 打印 log
  14. //记得加上这个哇,这是告诉全世界,你要开始在这类里面使用日志
  15. protected static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
  16. // 参数缺失异常处理
  17. @ExceptionHandler(MissingServletRequestParameterException.class)
  18. @ResponseStatus(value = HttpStatus.BAD_REQUEST)
  19. public Result handleHttpMessageNotReadableException (MissingServletRequestParameterException ex) {
  20. logger.error("缺少请求参数, {}",ex.getMessage());
  21. return Result.fail("缺少必要的请求参数");
  22. }
  23. }

编写一个测试用的 Controller,用于接收前端的请求。

  1. package cn.gorit.controller;
  2. import cn.gorit.common.exception.GlobalExceptionHandler;
  3. import cn.gorit.common.lang.Result;
  4. import cn.gorit.pojo.entity.User;
  5. import org.slf4j.Logger;
  6. import org.slf4j.LoggerFactory;
  7. import org.springframework.web.bind.annotation.PostMapping;
  8. import org.springframework.web.bind.annotation.RequestMapping;
  9. import org.springframework.web.bind.annotation.RequestParam;
  10. import org.springframework.web.bind.annotation.RestController;
  11. @RestController
  12. public class TestController {
  13. protected static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
  14. @RequestMapping("/")
  15. public String index() {
  16. return "Hello World";
  17. }
  18. @PostMapping("/test")
  19. public Result test(@RequestParam("name") String name, @RequestParam("pass") String pass) {
  20. // 日志打印
  21. logger.info("name {}",name);
  22. logger.info("pass {}",pass);
  23. User user = new User(name,pass);
  24. return Result.succ("操作成功",user);
  25. }
  26. }

启动项目,使用 Postman 测试项目的运行结果,发送一个 post 请求,并且携带参数,正常返回结果集
image.png

控制台的日志
image.png

接下来看看异常,少一个参数的情况
image.png

控制台也打印出了对应的错误
image.png

但是程序并没有报错。是不是很神奇

4.2 处理空指针异常

空指针异常就很常见了,我们创建 bean ,就经常会引用一些以为创建,实际上并没有的 bean,这样就会导致出现控制真异常,这个问题其实用的多了就司空见惯了。

接下来我们就模拟一个空指针异常,首先编写全局异常处理的拦截方法

  1. GlobalExceptionHandler 的处理方法
  2. // 空指针异常
  3. @ExceptionHandler(NullPointerException.class)
  4. @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
  5. public Result handleTypeMismatchException(NullPointerException ex) {
  6. logger.error("空指针异常,{}",ex.getMessage());
  7. return Result.fail(500,"空指针异常",null);
  8. }

在控制层中,我们模拟一个 空指针异常

    @RequestMapping("/test1")
    public void handleNullPointerException() {
        User u = null;
        u.getUsername();
    }

然后测试
image.png

日志报错打印
image.png

4.3 处理所有异常

异常会有很多种,还有编译期异常、运行期异常。这个方法就是一劳永逸的方法,因为 Exception 是所有异常的父类,所有的异常都会继承该异常,所以直接拦截 Exception 异常,就可以做到一劳永逸了。

编写异常处理一劳永逸的方法

    //    一劳永逸
    @ExceptionHandler(Exception.class)
    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    public Result handleUnexpectedServer(Exception e) {
        logger.error("系统异常");
        return  Result.fail(500,"系统内部出现异常");
    }

使用这一个方法,就要把前面的异常处理的方法注释掉,这样所有的异常都会变成统一的一个样子
image.png

但是在正式的开发中,不可能只有一种异常,这样的话我们也不好对项目出问题的地方进行定位。所以一般都会把全局异常处理放在最后一个地方

五、自定义错误页面

在新建的 SpringBoot 项目中的 resource/static/ 目录新建一个 404.html 文件