绑定完请刷新页面
取消
刷新

分享好友

×
取消 复制
全局异常处理教程
2020-03-06 17:15:37

 一.SpringBoot全局异常处理方式

  正常的Web应用开发时,需要考虑到应用运行发生异常时或出现错误时如何来被处理,例如捕获必要的异常信息,记录日志方便日后排错,友好的用户响应输出等等.而应用程序发生错误,有可能是应用自身的问题,也有可能是客户端操作的问题.在我们的项目中全局异常处理非常重要.

  大概有三种异常情况:

  1️⃣.在进入Controller之前,譬如请求一个不存在的地址404错误;

  2️⃣.在执行@RequestMapping时,进入逻辑处理阶段前,譬如传的参数类型错误;

  3️⃣.以上都正常时,在controller里执行逻辑代码时出的异常,比如出现NullPointerException.

  二.Spring Boot默认的错误处理机制。

  1. 默认错误处理机制

  默认情况下,Spring Boot为两种情况提供了不同的响应方式。

  一种是浏览器客户端访问应用发生错误时,一般情况下浏览器默认发送的请求头中Accept: text/html(当然你更改了就另当别论了),所以Spring Boot默认会响应一个html文档内容,称作“Whitelabel Error Page”。

​.



  2. 第三方工具请求接口时

  另一种是机器客户端访问应用发送错误时,Spring Boot会响应Json格式内容。这种情况更常见于利用第三方的Http工具请求接口时。


  如果看过源码,会发现两种方式输出用到的内容项是一样的,只不过种方式用html格式显示,第二种方式使用了Json格式。

  Spring Boot提供这个错误处理机制靠自动配置的BasicErrorController类。如果好奇Spring Boot是如何实现这种机制的。可以参看下BasicErrorController源码以及SpringMVC请求映射匹配规则。

  三.自定义多种错误页面

  上述的默认错误处理机制是一种通用的做法,你可能更期望细化一些处理,对于某些错误你可能想特殊对待。

  Spring Boot提供了一种方式,笔者认为这种方式是“容器级别”的操作。

  如下:

  @Configuration

  public class ContainerConfig {

  @Bean

  public EmbeddedServletContainerCustomizer containerCustomizer(){

  return new MyCustomizer();

  }

  private static class MyCustomizer implements EmbeddedServletContainerCustomizer {

  @Override

  public void customize(ConfigurableEmbeddedServletContainer container) {

  container.addErrorPages(new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/500"));

  }

  }

  }

  笔者新增了一个配置类,里面定义了一个bean,主要目的在于针对响应码为500的错误,采用笔者自定义的方式处理。如下:

  @RestController

  public class ExceptionController {

  @RequestMapping("/exception")

  public void catchException() {

  throw new RuntimeException("error occur");

  }

  @RequestMapping("/500")

  public String showServerError() {

  return "server error";

  }

  }

  那么浏览器中结果如下:



  很明显这种方式依赖于响应状态码进行定制。看到这里你应该会眼熟,还记得web.xml文件里的配置吗?

java.lang.Throwable

/error/500

500

/error/500

401

/error/401

403

/error/403

  笔者认为,Spring Boot的默认错误处理机制,包括自定义的错误页面,与原来的web.xml中的error-page配置是相通的,只不过Spring Boot应用默认使用内嵌Servlet容器,web.xml不见了而已。查看源码你会发现,Spring Boot在应用发生错误时会转向"/error"请求,即交由BasicErrorController处理。

  四.覆盖默认的错误处理方式

  默认错误处理机制的响应内容格式不一定是你相中的,理由可能如下:

  1️⃣.“Whitelabel Error Page”页面的样式太单调,用户体验不好;

  2️⃣.Json格式的结果字符串不统一,与你配合的前端人员更希望统一格式,好做统一的显示处理。

  Spring Boot开发指南上给出了几种方法:

  1️⃣.自定义一个bean,实现ErrorController接口,那么默认的错误处理机制将不再生效。

  2️⃣.自定义一个bean,继承BasicErrorController类,使用一部分现成的功能,自己也可以添加新的public方法,使用@RequestMapping及其produces属性指定新的地址映射。

  3️⃣.自定义一个ErrorAttribute类型的bean,那么还是默认的两种响应方式,只不过改变了内容项而已。

  先说下第三种方法,其实查看BasicErrorController源码,响应结果不论是html还是json,内容源都是ErrorAttribute。第三种方法只能改变内容,却改变不了格式,特别是html页面的样式。

  其实指南上只是轻描淡写了几种方法,没有很好的示例,不知道是不是指南的作者认为Spring Boot默认的错误处理机制已经很适用了。无论种还是第二种办法,都需要你看下BasicErrorController的继承体系及实现,因为BasicErrorController也是实现了ErrorController。

  采用种方式你可以具有完全的控制权,你可以摒弃默认的“Whitelabel Error Page”,指定自己的视图及视图样式,你可以指定响应的Json格式内容等等,因为BasicErrorController不再起作用,可以参考这里。

  由于笔者参照了BasicErrorController的源码,感觉第二种方法可能更简便些,所以实现了第二种方法。第二种方法的思路其实就是你可以通过继承,利用BasicErrorController已有的功能,或者进行扩展。

  那么如何覆盖默认的处理行为呢(虽然是自定义bean,但因为是继承,没有覆盖的话还是会采用默认的处理行为)?大致有两种思路。

  按照指南上所述,你可以新建public方法,使用@RequestMapping及其produces属性,例如@RequestMapping(produces="application/json"),那么只要请求头中包含“Accept: application/json”,则会映射到你的方法进行处理。看到这儿你可能会疑惑,why?还是建议你先看下BasicErrorController源码。

  因为是继承,所以对BasicErrorController默认两种错误处理方式的方法进行override。

  笔者希望完全覆盖及可控,所以选择了第二种思路。笔者自定义了MyErrorController,继承于BasicErrorController,注意一定要添加@Controller,不然Spring无法感知自定义的bean,继承于BasicErrorController还是会起作用!

  其他

  Spring Boot提供的ErrorController是一种全局性的容错机制。你还可以使用SpringMVC提供的@ControllerAdvice。

  如字面意思,@ControllerAdvice是切面技术的应用,允许你对Controller中抛出的某个或某些异常进行捕获并响应输出。用法如下:

  @ControllerAdvice

  public class DefaultExceptionHandler {

  private static final Logger LOGGER = LoggerFactory.getLogger(DefaultExceptionHandler.class); //日志记录器

  @ExceptionHandler({MissingServletRequestParameterException.class, TypeMismatchException.class, IllegalArgumentException.class, IllegalStateException.class})

  @ResponseStatus(value = HttpStatus.BAD_REQUEST)

  @ResponseBody

  public JsonResult conversionErrorHandler(Exception ex) {

  //记录日志

  LOGGER.error("参数异常捕获", ex);

  return new JsonResult(false, ErrorCode2Msg.getMessage(10004));

  }

  }

  笔者以往的开发习惯,使用@ControllerAdvice捕获应用级别的异常,使用web.xml中的error-page配置处理容器级别的报错。假设定义的过滤器抛出的异常,@ControllerAdvice是无法处理的(假设定义的过滤器抛出的异常,@ControllerAdvice是无法处理的)。

  改用Spring Boot后,@ControllerAdvice没有捕获的异常,ErrorController会帮你“捡起来”。

  五.实现步骤

  package com.syc.boot.handler;

  import lombok.extern.slf4j.Slf4j;

  import org.springframework.http.converter.HttpMessageNotReadableException;

  import org.springframework.validation.FieldError;

  import org.springframework.web.HttpRequestMethodNotSupportedException;

  import org.springframework.web.bind.MethodArgumentNotValidException;

  import org.springframework.web.bind.MissingServletRequestParameterException;

  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.context.request.WebRequest;

  import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;

  import org.springframework.web.servlet.NoHandlerFoundException;

  import javax.servlet.http.HttpServletRequest;

  import java.util.ArrayList;

  import java.util.HashMap;

  import java.util.List;

  import java.util.Map;

  /**

  * ControllerAdvice注解:控制器增强,

  * 用来使@ExceptionHandler、@InitBinder、@ModelAttribute注解的方法应用到

  * 所有的 @RequestMapping注解的方法。

  */

  @Slf4j

  @ControllerAdvice

  public class GlobalExceptionHandler {

  /**

  * 拦截所有Exception类的异常

  */

  @ExceptionHandler(Exception.class)

  @ResponseBody

  public Map exceptionHandler(Exception e) {

  Map result = new HashMap<>();

  result.put("respCode", "1000");

  result.put("respMsg", e.getMessage());

  return result;

  }

  /**

  * 400 请求参数封装到bean时 类型转换错误

  * 400 (Bad Request)

  */

  @ExceptionHandler({MethodArgumentTypeMismatchException.class,

  HttpMessageNotReadableException.class,

  MissingServletRequestParameterException.class})

  @ResponseBody

  public Map handleRequestParamFormatError(Exception ex) {

  log.error("handleRequestParamFormatError() bad request ex:{}", ex.getLocalizedMessage());

  Map result = new HashMap<>();

  result.put("respCode", "400");

  result.put("respMsg", ex.getLocalizedMessage());

  return result;

  }

  /**

  * 404 (Not Found)

  */

  @ExceptionHandler(NoHandlerFoundException.class)

  @ResponseBody

  public Map handleNoHandlerFoundException(NoHandlerFoundException ex, WebRequest request) {

  log.error("handleNoHandlerFoundException() {}{}{} ", request, " exception message:", ex.getLocalizedMessage());

  Map result = new HashMap<>();

  result.put("respCode", "404");

  result.put("respMsg", ex.getLocalizedMessage());

  return result;

  }

  /**

  * 405 (Method Not Allowed)

  */

  @ExceptionHandler(HttpRequestMethodNotSupportedException.class)

  @ResponseBody

  public Map handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException ex) {

  log.error("handleHttpRequestMethodNotSupportedException() request Method Not Allowed(405) exception message:{}", ex.getLocalizedMessage());

  Map result = new HashMap<>();

  result.put("respCode", "405");

  result.put("respMsg", ex.getLocalizedMessage());

  return result;

  }

  //跳转到自定义的错误处理页面

  //由于工作中都是才有前后端分离开发模式,所以一般上都没有直接返回资源页的需求了,

  // 一般上都是返回固定的响应格式,如respCode、respMsg、data,

  // 前端通过判断respCode的值进行业务判断,是弹窗还是跳转页面。

  //在校验不通过时,返回的异常信息是不友好的,此时可利用统一异常处理,

  // 对校验异常进行特殊处理,特别说明下,对于异常处理类,

  // 共有以下几种情况(被@RequestBody和@RequestParam注解的请求实体,校验异常类是不同的)

  // @ExceptionHandler(MethodArgumentNotValidException.class)

  // public Map handleBindException(MethodArgumentNotValidException ex) {

  // FieldError fieldError = ex.getBindingResult().getFieldError();

  // log.info("参数校验异常:{}({})", fieldError.getDefaultMessage(), fieldError.getField());

  // Map result = new HashMap<>();

  // result.put("respCode", "1001");

  // result.put("respMsg", fieldError.getDefaultMessage());

  // return result;

  // }

  }

分享好友

分享这个小栈给你的朋友们,一起进步吧。

Spring Boot
创建时间:2020-06-22 17:22:00
SpringBoot是由Pivotal团队在2013年开始研发、2014年4月发布个版本的全新开源的轻量级框架。它基于Spring4.0设计,不仅继承了Spring框架原有的特性,而且还通过简化配置来进一步简化了Spring应用的整个搭建和开发过程。另外SpringBoot通过集成大量的框架使得依赖包的版本冲突,以及引用的不稳定性等问题得到了很好的解决。
展开
订阅须知

• 所有用户可根据关注领域订阅专区或所有专区

• 付费订阅:虚拟交易,一经交易不退款;若特殊情况,可3日内客服咨询

• 专区发布评论属默认订阅所评论专区(除付费小栈外)

栈主、嘉宾

查看更多
  • duanhao
    栈主

小栈成员

查看更多
  • ?
  • zander
  • 凉茶cooltea
戳我,来吐槽~