1 基本思路

1.1 使用到的异常种类

image.png
Java的错误异常分为两个体系:Error、Exception,均继承自Throwable->Object
Error错误类体系:Java虚拟机无法解决的严重问题。如JVM系统内部错误、资源耗尽等严重情况
Exception异常类体系:其它因编程错误或偶然的外在因素导致的一般性问题。又分为编译时异常(checked)和运行时异常(unchecked)。

1.2 使用到的技术点

  1. 类:使用@ControllerAdvice+@ExceptionHandler处理全局抛出的异常
  2. 方法:上面定义的全局异常处理类,方法上标注不同的注解。可实现不同的异常进入到不同的处理模块。
  3. 方法返回值:根据不同的情况判断是返回的页面还是Json对象。

2 具体实现

2.1 通览:自定义全局异常类

  1. package com.efly.gulimall.coupon.helper.exception;
  2. import com.efly.gulimall.coupon.helper.base.AjaxResult;
  3. import lombok.extern.slf4j.Slf4j;
  4. import org.springframework.validation.BindException;
  5. import org.springframework.validation.BindingResult;
  6. import org.springframework.web.HttpRequestMethodNotSupportedException;
  7. import org.springframework.web.bind.MethodArgumentNotValidException;
  8. import org.springframework.web.bind.annotation.ExceptionHandler;
  9. import org.springframework.web.bind.annotation.RestControllerAdvice;
  10. import org.springframework.web.servlet.ModelAndView;
  11. import javax.servlet.http.HttpServletRequest;
  12. import javax.servlet.http.HttpServletResponse;
  13. import java.util.HashMap;
  14. import java.util.Map;
  15. /**
  16. * 面向切面的异常:自定义异常处理器
  17. *
  18. * @author zhuyf
  19. */
  20. @RestControllerAdvice
  21. @Slf4j
  22. public class DefaultExceptionHandler {
  23. /**
  24. * 自定义的业务异常(可在代码处手动抛出throw new BusinessPageException)
  25. *
  26. * @param request 请求对象,可不传
  27. * @param response 响应对象,可不传
  28. * @param ex 异常类(这个要和你当前捕获的异常类是同一个)
  29. */
  30. @ExceptionHandler(BusinessPageException.class) //也可以只对一个类进行捕获
  31. public Object errorHandler(BusinessPageException ex, HttpServletRequest request, HttpServletResponse response) {
  32. /*
  33. * 这里你拿到了request和response对象,你可以做任何你想做的事
  34. * 比如:
  35. * 1.用request从头信息中拿到Accept来判断是请求方可接收的类型从而进行第一个方法的判断
  36. * 2.如果你也想返回一个页面,使用response对象进行重定向到自己的错误页面就可以了
  37. */
  38. //异常写入数据库日志框架
  39. log.error(ex.getMessage(), ex);
  40. //异常协商:如果接受类型为html,就返回错误页面,否则返回json文本
  41. if (request.getHeader("accept").indexOf("text/html") != -1) {
  42. ModelAndView modelAndView = new ModelAndView();
  43. modelAndView.setViewName("/error/5xx.html"); // 指定错误跳转页面 需要在templates里面新建 一个error.html
  44. modelAndView.addObject("messages", ex.getMessage());
  45. modelAndView.addObject("code", ex.getCode());
  46. modelAndView.addObject("url", request.getRequestURL());
  47. return modelAndView;
  48. }
  49. return AjaxResult.error("服务器错误,请联系管理员" + ex.getMessage());
  50. }
  51. /**
  52. * Throwable->Exception->HttpRequestMethodNotSupportedException
  53. * 捕获请求方式不支持的异常
  54. */
  55. @ExceptionHandler({HttpRequestMethodNotSupportedException.class})
  56. public AjaxResult handleException(HttpRequestMethodNotSupportedException e) {
  57. log.error(e.getMessage(), e);
  58. return AjaxResult.error("不支持' " + e.getMethod() + "'请求");
  59. }
  60. @ExceptionHandler(value = {MethodArgumentNotValidException.class, BindException.class})
  61. public AjaxResult handleVaildException(BindException e) {
  62. log.error("数据校验出现问题{},异常类型:{}", e.getMessage(), e.getClass());
  63. BindingResult bindingResult = e.getBindingResult();
  64. Map<String, String> errorMap = new HashMap<>();
  65. bindingResult.getFieldErrors().forEach((fieldError) -> {
  66. errorMap.put(fieldError.getField(), fieldError.getDefaultMessage());
  67. });
  68. return AjaxResult.error("数据校验出现问题", errorMap);
  69. }
  70. /**
  71. * Throwable->Exception->RuntimeException
  72. * 捕获未知的运行时RuntimeException异常
  73. */
  74. @ExceptionHandler(RuntimeException.class)
  75. public AjaxResult notFount(RuntimeException e) {
  76. //内容可决定是返回AjaxResult还是回到404
  77. log.error("运行时异常:", e);
  78. return AjaxResult.error("运行时异常:" + e.getMessage());
  79. }
  80. /**
  81. * Throwable->Exception
  82. * 捕获Exception异常
  83. */
  84. @ExceptionHandler(Exception.class)
  85. public AjaxResult handleException(Exception e) {
  86. log.error(e.getMessage(), e);
  87. return AjaxResult.error("服务器错误,请联系管理员", e.getMessage());
  88. }
  89. /**
  90. * Throwable:最大的异常捕获类
  91. * 捕获异常体系最大的Throwable异常
  92. */
  93. @ExceptionHandler(value = Throwable.class)
  94. public AjaxResult handleException(Throwable throwable) {
  95. log.error("错误:", throwable);
  96. return AjaxResult.error("服务器错误,请联系管理员" + throwable.getMessage());
  97. }
  98. }

该类的各个方法使用不同注解捕获不同的异常类型。

2.2 拆解:自定义异常类及捕获自定义异常类的处理方法

上面的BussinessPageException是自定义的异常类,该方法识别请求的accept判断是网页发起的请求还是json对象发起的,如果是网页发起的则到模板引擎下的/error/5xx.html,如果是非text/html则返回json对象。

  1. /**
  2. * 自定义业务异常
  3. *
  4. * @param request 请求对象,可不传
  5. * @param response 响应对象,可不传
  6. * @param ex 异常类(这个要和你当前捕获的异常类是同一个)
  7. */
  8. @ExceptionHandler(BusinessPageException.class)
  9. public Object errorHandler(BusinessPageException ex,
  10. HttpServletRequest request,
  11. HttpServletResponse response) {
  12. /*
  13. * 这里你拿到了request和response对象,你可以做任何你想做的事
  14. * 比如:
  15. * 1.用request从头信息中拿到Accept来判断是请求方可接收的类型从而进行第一个方法的判断
  16. * 2.如果你也想返回一个页面,使用response对象进行重定向到自己的错误页面就可以了
  17. */
  18. //异常写入数据库日志框架
  19. log.error(ex.getMessage(), ex);
  20. //异常协商:如果接受类型为html,就返回错误页面,否则返回json文本
  21. if (request.getHeader("accept").indexOf("text/html") != -1) {
  22. ModelAndView modelAndView = new ModelAndView();
  23. // 指定错误跳转页面 需要在templates里面新建 一个error.html
  24. modelAndView.setViewName("/error/5xx.html");
  25. modelAndView.addObject("messages", ex.getMessage());
  26. modelAndView.addObject("code", ex.getCode());
  27. modelAndView.addObject("url", request.getRequestURL());
  28. return modelAndView;
  29. }
  30. return AjaxResult.error("服务器错误,请联系管理员" + ex.getMessage());
  31. }

自定义异常类

  1. /**
  2. * 自定义异常类
  3. *
  4. * @author zhuyf
  5. */
  6. public class BusinessPageException extends RuntimeException {
  7. private static final long serialVersionUID = 1L;
  8. protected final String message;
  9. protected final Integer code;
  10. public BusinessPageException(String message, Integer code) {
  11. this.message = message;
  12. this.code = code;
  13. }
  14. @Override
  15. public String getMessage() {
  16. return message;
  17. }
  18. public Integer getCode() {
  19. return code;
  20. }
  21. }

自定义返回的类型AjaxResult

  1. package com.efly.gulimall.coupon.helper.base;
  2. import java.util.HashMap;
  3. /**
  4. * 操作消息提醒
  5. *
  6. * @author zhuyf
  7. */
  8. public class AjaxResult extends HashMap<String, Object> {
  9. private static final long serialVersionUID = 1L;
  10. /**
  11. * 初始化一个新创建的 Message 对象
  12. */
  13. public AjaxResult() {
  14. }
  15. /**
  16. * 返回错误消息
  17. *
  18. * @return 错误消息
  19. */
  20. public static AjaxResult error() {
  21. return error(1, "操作失败", false, "");
  22. }
  23. /**
  24. * 返回错误消息
  25. *
  26. * @param msg 内容
  27. * @return 错误消息
  28. */
  29. public static AjaxResult error(String msg) {
  30. return error(500, msg, false, "");
  31. }
  32. public static AjaxResult error(String msg, Object data) {
  33. return error(500, msg, false, data);
  34. }
  35. /**
  36. * 返回错误消息
  37. *
  38. * @param code 错误码
  39. * @param msg 内容
  40. * @return 错误消息
  41. */
  42. public static AjaxResult error(int code, String msg, boolean isSuccess, Object data) {
  43. AjaxResult json = new AjaxResult();
  44. json.put("code", code);
  45. json.put("msg", msg);
  46. json.put("success", isSuccess);
  47. json.put("data", data);
  48. return json;
  49. }
  50. /**
  51. * 返回成功消息
  52. *
  53. * @param msg 内容
  54. * @return 成功消息
  55. */
  56. public static AjaxResult success(String msg) {
  57. AjaxResult json = new AjaxResult();
  58. json.put("msg", msg);
  59. json.put("code", 200);
  60. json.put("success", true);
  61. json.put("data", "");
  62. return json;
  63. }
  64. /**
  65. * 返回成功消息
  66. *
  67. * @return 成功消息
  68. */
  69. public static AjaxResult success() {
  70. return AjaxResult.success("操作成功");
  71. }
  72. /**
  73. * 返回成功消息
  74. *
  75. * @param key 键值
  76. * @param value 内容
  77. * @return 成功消息
  78. */
  79. @Override
  80. public AjaxResult put(String key, Object value) {
  81. super.put(key, value);
  82. return this;
  83. }
  84. }

2.3 拆解:thymeleaf引擎渲染的error/5xx.html页面

此处使用的是Thymeleaf。注意在实际生产环境里面,具体的错误信息不要显示在错误页面上,在测试环境下可以显示以提高排错效率。

  1. pom加载thymeleaf启动器

    1. <dependency>
    2. <groupId>org.springframework.boot</groupId>
    3. <artifactId>spring-boot-starter-thymeleaf</artifactId>
    4. </dependency>
  2. ThymeleafView默认路径为:classpath:/templates,加页面放入到templates/下

image.png
上面分发的代码

  1. ModelAndView modelAndView = new ModelAndView();
  2. // 指定错误跳转页面 需要在templates里面新建 一个error.html
  3. modelAndView.setViewName("/error/5xx.html");
  1. 5xx.html的具体错误内容 ``` <!DOCTYPE html>
    错误信息:
    错误状态码:
    失败API地址:
  1. <a name="xRKOU"></a>
  2. ## 2.4 拆解:SpringBoot默认的异常处理静态错误页面
  3. 除了像上面可以使用服务器端渲染的页面(可以动态捕获到具体的错误日志、错误代码、错误路径等信息),SpringBoot也提供了默认的异常信息静态页面的处理功能。<br />对于机器客户端,它将生成并返回JSON响应,其中包含错误,HTTP状态和异常消息的详细信息。<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/696107/1626495013380-4069a248-c45f-46b5-82b4-205b1b23fa07.png#clientId=u5b5b6f01-0bb3-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=142&id=ufed797bf&margin=%5Bobject%20Object%5D&name=image.png&originHeight=142&originWidth=356&originalType=binary&ratio=1&rotation=0&showTitle=false&size=19568&status=done&style=none&taskId=u6d23838d-97c9-4843-9268-2b001074dff&title=&width=356)<br />对于浏览器客户端,响应一个“ whitelabel”错误视图,以HTML格式呈现相同的数据。<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/696107/1626495005821-808e486d-2c7f-4072-a110-8713866d2082.png#clientId=u5b5b6f01-0bb3-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=126&id=ue95b3250&margin=%5Bobject%20Object%5D&name=image.png&originHeight=126&originWidth=482&originalType=binary&ratio=1&rotation=0&showTitle=false&size=33754&status=done&style=none&taskId=u7992a54c-919d-474f-a830-b6c892af16e&title=&width=482)<br />**具体的规则即为**:SpringMVC在执行过程中会捕获异常,然后去找异常解析器去解析异常,如果可以解析就拿到解析器返回的ModelAndView去指定的视图/JSON数据(就是我们上面定义的Thymeleaf引擎下的error/5xx、4xx.html),如果不能就由底层tomcat发送一个error错误,这个error错误会去SpringBoot默认的静态目录下的error/404或error/500页面,404或500是错误代码。
  4. ```java
  5. 扩展:springboot将从服务器此目录下读取静态资源数据,默认值为
  6. classpath:/META-INF/resources/
  7. classpath:/resources/,
  8. classpath:/static/,
  9. classpath:/public/
  10. 其中classpath为resources目录

所以我们可以将默认的5xx.html和4xx.html写入到如上默认规则目录里面
image.png
具体的页面内容
error/5xx.html

  1. <!DOCTYPE html>
  2. <html lang="zh">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>CGWANG - 500</title>
  7. <link href="/css/bootstrap.min.css" rel="stylesheet"/>
  8. <link href="/css/animate.css" rel="stylesheet"/>
  9. <link href="/css/style.css" rel="stylesheet"/>
  10. </head>
  11. <body class="gray-bg">
  12. <div class="middle-box text-center animated fadeInDown">
  13. <h1>500</h1>
  14. <h3 class="font-bold">内部服务器错误!</h3>
  15. <div class="error-desc">
  16. 服务器遇到意外事件,不允许完成请求。我们抱歉。您可以返回主页面。
  17. <a href="javascript:top.document.location.href='/'" class="btn btn-primary m-t">主页</a>
  18. </div>
  19. </div>
  20. <script src="/js/jquery.min.js?v=2.1.4"></script>
  21. <script src="/js/bootstrap.min.js?v=3.3.6"></script>
  22. </body>
  23. </html>

error/4xx.html页面

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>404</title>
  6. </head>
  7. <body>
  8. 404
  9. </body>
  10. </html>

2.6 测试

有如下的代码

  1. @RequestMapping("/list")
  2. public R list(@RequestParam Map<String, Object> params) {
  3. //手动抛出异常
  4. throw new BusinessPageException("进入了错误页面", 500);
  5. }

在网页端访问
image.png
image.png
如果使用PostMan由于没有像网页端一样传递了Accept:text/html,则返回的是Jason对象
image.png