SpringCloud 统一异常处理
在启动应用时会发现在控制台打印的日志中出现了两个路径为 {[/error]} 的访问地址,当系统中发送异常错误时,Spring Boot 会根据请求方式分别跳转到以 JSON 格式或以界面显示的 /error 地址中显示错误信息。

  1. 2018-12-18 09:36:24.627 INFO 19040 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" ...
  2. 2018-12-18 09:36:24.632 INFO 19040 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" ...

默认异常处理

使用 AJAX 方式请求时返回的 JSON 格式错误信息。

  1. {
  2. "timestamp": "2018-12-18T01:50:51.196+0000",
  3. "status": 404,
  4. "error": "Not Found",
  5. "message": "No handler found for GET /err404",
  6. "path": "/err404"
  7. }

使用浏览器请求时返回的错误信息界面。

2021-05-13-23-35-38-993132.png

自定义异常处理

引入依赖

  1. <dependency>
  2. <groupId>com.alibaba</groupId>
  3. <artifactId>fastjson</artifactId>
  4. <version>1.2.54</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.springframework.boot</groupId>
  8. <artifactId>spring-boot-starter-freemarker</artifactId>
  9. </dependency>

fastjson 是 JSON 序列化依赖, spring-boot-starter-freemarker 是一个模板引擎,用于我们设置错误输出模板。

增加配置

properties

  1. # 出现错误时, 直接抛出异常(便于异常统一处理,否则捕获不到404)
  2. spring.mvc.throw-exception-if-no-handler-found=true
  3. # 不要为工程中的资源文件建立映射
  4. spring.resources.add-mappings=false

yml

  1. spring:
  2. # 出现错误时, 直接抛出异常(便于异常统一处理,否则捕获不到404)
  3. mvc:
  4. throw-exception-if-no-handler-found: true
  5. # 不要为工程中的资源文件建立映射
  6. resources:
  7. add-mappings: false

新建错误信息实体

  1. /**
  2. * 信息实体
  3. */
  4. public class ExceptionEntity implements Serializable {
  5. private static final long serialVersionUID = 1L;
  6. private String message;
  7. private int code;
  8. private String error;
  9. private String path;
  10. @JSONField(format = "yyyy-MM-dd hh:mm:ss")
  11. private Date timestamp = new Date();
  12. public static long getSerialVersionUID() {
  13. return serialVersionUID;
  14. }
  15. public String getMessage() {
  16. return message;
  17. }
  18. public void setMessage(String message) {
  19. this.message = message;
  20. }
  21. public int getCode() {
  22. return code;
  23. }
  24. public void setCode(int code) {
  25. this.code = code;
  26. }
  27. public String getError() {
  28. return error;
  29. }
  30. public void setError(String error) {
  31. this.error = error;
  32. }
  33. public String getPath() {
  34. return path;
  35. }
  36. public void setPath(String path) {
  37. this.path = path;
  38. }
  39. public Date getTimestamp() {
  40. return timestamp;
  41. }
  42. public void setTimestamp(Date timestamp) {
  43. this.timestamp = timestamp;
  44. }
  45. }

新建自定义异常

  1. /**
  2. * 自定义异常
  3. */
  4. public class BasicException extends RuntimeException {
  5. private static final long serialVersionUID = 1L;
  6. private int code = 0;
  7. public BasicException(int code, String message) {
  8. super(message);
  9. this.code = code;
  10. }
  11. public int getCode() {
  12. return this.code;
  13. }
  14. }
  15. /**
  16. * 业务异常
  17. */
  18. public class BusinessException extends BasicException {
  19. private static final long serialVersionUID = 1L;
  20. public BusinessException(int code, String message) {
  21. super(code, message);
  22. }
  23. }

BasicException 继承了 RuntimeException ,并在原有的 Message 基础上增加了错误码 code 的内容。而 BusinessException 则是在业务中具体使用的自定义异常类,起到了对不同的异常信息进行分类的作用。

新建 error.ftl 模板文件

位置:/src/main/resources/templates/ 用于显示错误信息

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta name="robots" content="noindex,nofollow" />
  5. <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
  6. <style>
  7. h2{
  8. color: #4288ce;
  9. font-weight: 400;
  10. padding: 6px 0;
  11. margin: 6px 0 0;
  12. font-size: 18px;
  13. border-bottom: 1px solid #eee;
  14. }
  15. /* Exception Variables */
  16. .exception-var table{
  17. width: 100%;
  18. max-width: 500px;
  19. margin: 12px 0;
  20. box-sizing: border-box;
  21. table-layout:fixed;
  22. word-wrap:break-word;
  23. }
  24. .exception-var table caption{
  25. text-align: left;
  26. font-size: 16px;
  27. font-weight: bold;
  28. padding: 6px 0;
  29. }
  30. .exception-var table caption small{
  31. font-weight: 300;
  32. display: inline-block;
  33. margin-left: 10px;
  34. color: #ccc;
  35. }
  36. .exception-var table tbody{
  37. font-size: 13px;
  38. font-family: Consolas,"Liberation Mono",Courier,"微软雅黑";
  39. }
  40. .exception-var table td{
  41. padding: 0 6px;
  42. vertical-align: top;
  43. word-break: break-all;
  44. }
  45. .exception-var table td:first-child{
  46. width: 28%;
  47. font-weight: bold;
  48. white-space: nowrap;
  49. }
  50. .exception-var table td pre{
  51. margin: 0;
  52. }
  53. </style>
  54. </head>
  55. <body>
  56. <div class="exception-var">
  57. <h2>Exception Datas</h2>
  58. <table>
  59. <tbody>
  60. <tr>
  61. <td>Code</td>
  62. <td>
  63. ${(exception.code)!}
  64. </td>
  65. </tr>
  66. <tr>
  67. <td>Time</td>
  68. <td>
  69. ${(exception.timestamp?datetime)!}
  70. </td>
  71. </tr>
  72. <tr>
  73. <td>Path</td>
  74. <td>
  75. ${(exception.path)!}
  76. </td>
  77. </tr>
  78. <tr>
  79. <td>Exception</td>
  80. <td>
  81. ${(exception.error)!}
  82. </td>
  83. </tr>
  84. <tr>
  85. <td>Message</td>
  86. <td>
  87. ${(exception.message)!}
  88. </td>
  89. </tr>
  90. </tbody>
  91. </table>
  92. </div>
  93. </body>
  94. </html>

编写全局异常控制类

  1. /**
  2. * 全局异常控制类
  3. */
  4. @ControllerAdvice
  5. public class GlobalExceptionHandler {
  6. /**
  7. * 404异常处理
  8. */
  9. @ExceptionHandler(value = NoHandlerFoundException.class)
  10. @ResponseStatus(HttpStatus.NOT_FOUND)
  11. public ModelAndView errorHandler(HttpServletRequest request, NoHandlerFoundException exception, HttpServletResponse response) {
  12. return commonHandler(request, response,
  13. exception.getClass().getSimpleName(),
  14. HttpStatus.NOT_FOUND.value(),
  15. exception.getMessage());
  16. }
  17. /**
  18. * 405异常处理
  19. */
  20. @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
  21. public ModelAndView errorHandler(HttpServletRequest request, HttpRequestMethodNotSupportedException exception, HttpServletResponse response) {
  22. return commonHandler(request, response,
  23. exception.getClass().getSimpleName(),
  24. HttpStatus.METHOD_NOT_ALLOWED.value(),
  25. exception.getMessage());
  26. }
  27. /**
  28. * 415异常处理
  29. */
  30. @ExceptionHandler(HttpMediaTypeNotSupportedException.class)
  31. public ModelAndView errorHandler(HttpServletRequest request, HttpMediaTypeNotSupportedException exception, HttpServletResponse response) {
  32. return commonHandler(request, response,
  33. exception.getClass().getSimpleName(),
  34. HttpStatus.UNSUPPORTED_MEDIA_TYPE.value(),
  35. exception.getMessage());
  36. }
  37. /**
  38. * 500异常处理
  39. */
  40. @ExceptionHandler(value = Exception.class)
  41. public ModelAndView errorHandler (HttpServletRequest request, Exception exception, HttpServletResponse response) {
  42. return commonHandler(request, response,
  43. exception.getClass().getSimpleName(),
  44. HttpStatus.INTERNAL_SERVER_ERROR.value(),
  45. exception.getMessage());
  46. }
  47. /**
  48. * 业务异常处理
  49. */
  50. @ExceptionHandler(value = BasicException.class)
  51. private ModelAndView errorHandler (HttpServletRequest request, BasicException exception, HttpServletResponse response) {
  52. return commonHandler(request, response,
  53. exception.getClass().getSimpleName(),
  54. exception.getCode(),
  55. exception.getMessage());
  56. }
  57. /**
  58. * 表单验证异常处理
  59. */
  60. @ExceptionHandler(value = BindException.class)
  61. @ResponseBody
  62. public ExceptionEntity validExceptionHandler(BindException exception, HttpServletRequest request, HttpServletResponse response) {
  63. List<FieldError> fieldErrors = exception.getBindingResult().getFieldErrors();
  64. Map<String,String> errors = new HashMap<>();
  65. for (FieldError error:fieldErrors) {
  66. errors.put(error.getField(), error.getDefaultMessage());
  67. }
  68. ExceptionEntity entity = new ExceptionEntity();
  69. entity.setMessage(JSON.toJSONString(errors));
  70. entity.setPath(request.getRequestURI());
  71. entity.setCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
  72. entity.setError(exception.getClass().getSimpleName());
  73. response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
  74. return entity;
  75. }
  76. /**
  77. * 异常处理数据处理
  78. */
  79. private ModelAndView commonHandler (HttpServletRequest request, HttpServletResponse response,
  80. String error, int httpCode, String message) {
  81. ExceptionEntity entity = new ExceptionEntity();
  82. entity.setPath(request.getRequestURI());
  83. entity.setError(error);
  84. entity.setCode(httpCode);
  85. entity.setMessage(message);
  86. return determineOutput(request, response, entity);
  87. }
  88. /**
  89. * 异常输出处理
  90. */
  91. private ModelAndView determineOutput(HttpServletRequest request, HttpServletResponse response, ExceptionEntity entity) {
  92. if (!(
  93. request.getHeader("accept").contains("application/json")
  94. || (request.getHeader("X-Requested-With") != null && request.getHeader("X-Requested-With").contains("XMLHttpRequest"))
  95. )) {
  96. ModelAndView modelAndView = new ModelAndView("error");
  97. modelAndView.addObject("exception", entity);
  98. return modelAndView;
  99. } else {
  100. response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
  101. response.setCharacterEncoding("UTF8");
  102. response.setHeader("Content-Type", "application/json");
  103. try {
  104. response.getWriter().write(ResultJsonTools.build(
  105. ResponseCodeConstant.SYSTEM_ERROR,
  106. ResponseMessageConstant.APP_EXCEPTION,
  107. JSONObject.parseObject(JSON.toJSONString(entity))
  108. ));
  109. } catch (IOException e) {
  110. e.printStackTrace();
  111. }
  112. return null;
  113. }
  114. }
  115. }

@ControllerAdvice

作用于类上,用于标识该类用于处理全局异常。

@ExceptionHandler

作用于方法上,用于对拦截的异常类型进行处理。value 属性用于指定具体的拦截异常类型,如果有多个 ExceptionHandler 存在,则需要指定不同的 value 类型,由于异常类拥有继承关系,所以 ExceptionHandler 会首先执行在继承树中靠前的异常类型。

BindException

该异常来自于表单验证框架 Hibernate validation,当字段验证未通过时会抛出此异常。

编写测试 Controller

  1. @RestController
  2. public class TestController {
  3. @RequestMapping(value = "err")
  4. public void error(){
  5. throw new BusinessException(400, "业务异常错误信息");
  6. }
  7. @RequestMapping(value = "err2")
  8. public void error2(){
  9. throw new NullPointerException("手动抛出异常信息");
  10. }
  11. @RequestMapping(value = "err3")
  12. public int error3(){
  13. int a = 10 / 0;
  14. return a;
  15. }
  16. }

使用 AJAX 方式请求时返回的 JSON 格式错误信息。

  1. # /err
  2. {
  3. "msg": "应用程序异常",
  4. "code": -1,
  5. "status_code": 0,
  6. "data": {
  7. "path": "/err",
  8. "code": 400,
  9. "error": "BusinessException",
  10. "message": "业务异常错误信息",
  11. "timestamp": "2018-12-18 11:09:00"
  12. }
  13. }
  14. # /err2
  15. {
  16. "msg": "应用程序异常",
  17. "code": -1,
  18. "status_code": 0,
  19. "data": {
  20. "path": "/err2",
  21. "code": 500,
  22. "error": "NullPointerException",
  23. "message": "手动抛出异常信息",
  24. "timestamp": "2018-12-18 11:15:15"
  25. }
  26. }
  27. # /err3
  28. {
  29. "msg": "应用程序异常",
  30. "code": -1,
  31. "status_code": 0,
  32. "data": {
  33. "path": "/err3",
  34. "code": 500,
  35. "error": "ArithmeticException",
  36. "message": "/ by zero",
  37. "timestamp": "2018-12-18 11:15:46"
  38. }
  39. }
  40. # /err404
  41. {
  42. "msg": "应用程序异常",
  43. "code": -1,
  44. "status_code": 0,
  45. "data": {
  46. "path": "/err404",
  47. "code": 404,
  48. "error": "NoHandlerFoundException",
  49. "message": "No handler found for GET /err404",
  50. "timestamp": "2018-12-18 11:16:11"
  51. }
  52. }

使用浏览器请求时返回的错误信息界面。

2021-05-13-23-35-39-133753.png
2021-05-13-23-35-39-228495.png
2021-05-13-23-35-39-329228.png
2021-05-13-23-35-39-423972.png