SpringBoot 2.2和2.3异常处理的一个小变化

37 篇文章 5 订阅

先看个有意思的现象:

@RestController
public class DemoController {
    @GetMapping("/hello")
    public String hello(){
        throw new RuntimeException("服务端异常,请稍后再试!");
    }
}

在springboot2.2.0的时候,浏览器访问http://localhost:8080/hello,输出结果如下:

在这里插入图片描述
换成springboot2.3.0的时候,输出结果如下:
在这里插入图片描述
对比一下,能看出来在2.3.0的时候,异常message没有打印出来。

以上是在返回html的时候,返回json的时候,我们再来看下:

public static void main(String[] args)throws Exception {
  String url = "http://localhost:8080/hello";
  HttpURLConnection conn = (HttpURLConnection)new URL(url).openConnection();
  conn.addRequestProperty("Accept", "application/json");
  int code = conn.getResponseCode();
  System.out.println("responseCode:" + code);
  InputStream inputStream = conn.getErrorStream();
  String response = readInputStream(inputStream);
  System.out.println(response);
}

在2.2.0的时候输出结果是:

{"timestamp":"2020-08-16T04:18:30.778+0000",
"status":500,
"error":"Internal Server Error",
"message":"服务端异常,请稍后再试!",
"path":"/hello"}

在2.3.0的时候输出结果是:

{"timestamp":"2020-08-16T04:16:25.738+00:00",
"status":500,
"error":"Internal Server Error",
"message":"",
"path":"/hello"}

这回看的更清晰了,message字段变成了空。

看下源码的处理,springboot异常处理的入口配置类在这里:ErrorMvcAutoConfiguration,先看2.2.0,它里面配置了处理error的BasicErrorController:

@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
    ObjectProvider<ErrorViewResolver> errorViewResolvers) {
  return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
      errorViewResolvers.orderedStream().collect(Collectors.toList()));
}

看下BasicErrorController里面是如何响应error的:

//这个就是生成html页面额
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
  HttpStatus status = getStatus(request);
  Map<String, Object> model = Collections
      .unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
  response.setStatus(status.value());
  ModelAndView modelAndView = resolveErrorView(request, response, status, model);
  return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
//这个是输出json的
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
  HttpStatus status = getStatus(request);
  if (status == HttpStatus.NO_CONTENT) {
    return new ResponseEntity<Map<String, Object>>(status);
  }
  Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
  return new ResponseEntity<>(body, status);
}

我们主要看下页面上的数据是怎么生成的,也就是getErrorAttributes():

在这之前首先判断了是否要包含异常堆栈isIncludeStackTrace():

protected boolean isIncludeStackTrace(HttpServletRequest request, MediaType produces) {
  IncludeStacktrace include = getErrorProperties().getIncludeStacktrace();
  if (include == IncludeStacktrace.ALWAYS) {
    return true;
  }
  if (include == IncludeStacktrace.ON_TRACE_PARAM) {
    return getTraceParameter(request);
  }
  return false;
}

是根据配置项server.error.includeStacktrace来决定的,默认是IncludeStacktrace.NEVER。如果配置了是ALWAYS那就输出堆栈,如果配置了ON_TRACE_PARAM,那么就从请求参数中获取trace参数,如果是true那么也会打印堆栈。

protected Map<String, Object> getErrorAttributes(HttpServletRequest request, boolean includeStackTrace) {
  WebRequest webRequest = new ServletWebRequest(request);
  return this.errorAttributes.getErrorAttributes(webRequest, includeStackTrace);
}
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
  Map<String, Object> errorAttributes = new LinkedHashMap<>();
  //timestamp
  errorAttributes.put("timestamp", new Date());
  //status
  addStatus(errorAttributes, webRequest);
  //error和message
  addErrorDetails(errorAttributes, webRequest, includeStackTrace);
  //path
  addPath(errorAttributes, webRequest);
  return errorAttributes;
}

这里看到了,这里面包含的内容首先是timestamp,然后是status,最后一行是path,看下addErrorDetails():

private void addErrorDetails(Map<String, Object> errorAttributes, WebRequest webRequest,
    boolean includeStackTrace) {
    //获取异常,上例中就是RuntimeException
  Throwable error = getError(webRequest);
  if (error != null) {
    while (error instanceof ServletException && error.getCause() != null) {
      error = error.getCause();
    }
    //这个includeException也是个配置项server.error.includeException
    // 如果打开就输出exception,值是异常的类名
    // 默认是没有打开的,可以看下DefaultErrorAttributes
    if (this.includeException) {
      errorAttributes.put("exception", error.getClass().getName());
    }
    // 添加message属性
    addErrorMessage(errorAttributes, error);
    //是否包含异常堆栈,默认也是没有的
    if (includeStackTrace) {
      // 添加异常堆栈
      addStackTrace(errorAttributes, error);
    }
  }
  Object message = getAttribute(webRequest, "javax.servlet.error.message");
  if ((!StringUtils.isEmpty(message) || errorAttributes.get("message") == null)
      && !(error instanceof BindingResult)) {
    errorAttributes.put("message", StringUtils.isEmpty(message) ? "No message available" : message);
  }
}
private void addErrorMessage(Map<String, Object> errorAttributes, Throwable error) {
  BindingResult result = extractBindingResult(error);
  //显然这里是null,因此直接直接添加error.getMessage()
  if (result == null) {
    errorAttributes.put("message", error.getMessage());
    return;
  }
  if (result.hasErrors()) {
    errorAttributes.put("errors", result.getAllErrors());
    errorAttributes.put("message", "Validation failed for object='" + result.getObjectName()
        + "'. Error count: " + result.getErrorCount());
  }
  else {
    errorAttributes.put("message", "No errors");
  }
}

现在我们试着添加下那几个属性,看下输出结果,首先添加server.error.includeStacktrace= IncludeStacktrace.ALWAYS:
在这里插入图片描述
再添加下server.error.includeException=true:

{"timestamp":"2020-08-16T04:56:26.275+0000",
"status":500,
"error":"Internal Server Error",
"exception":"java.lang.RuntimeException",//多了一个这个
"message":"服务端异常,请稍后再试!",
"trace":"java.lang.RuntimeException: 服务端异常,请稍后再试!\r\n\tat com.github.xjs.error.controller.DemoController.hello(DemoController.java:10)\r\n\tat sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\r\n\tat sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)\r\n\tat sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\r\n\tat java.lang.reflect.Method.invoke(Method.java:498)\r\n\tat org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190)\r\n\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:106)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:888)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:793)\r\n\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)\r\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040)\r\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)\r\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)\r\n\tat org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)\r\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:634)\r\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)\r\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:741)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\r\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\r\n\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\r\n\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\r\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\r\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)\r\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)\r\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:526)\r\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)\r\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)\r\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)\r\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)\r\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:408)\r\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)\r\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:861)\r\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1579)\r\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)\r\n\tat java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)\r\n\tat java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)\r\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\r\n\tat java.lang.Thread.run(Thread.java:748)\r\n",
"path":"/hello"}

我们再来看下2.3.0:

@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
  HttpStatus status = getStatus(request);
  if (status == HttpStatus.NO_CONTENT) {
    return new ResponseEntity<>(status);
  }
  Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
  return new ResponseEntity<>(body, status);
}

这里的getErrorAttributes之前,首先是getErrorAttributeOptions():

protected ErrorAttributeOptions getErrorAttributeOptions(HttpServletRequest request, MediaType mediaType) {
  ErrorAttributeOptions options = ErrorAttributeOptions.defaults();
  //server.error.includeException,是否输出异常类名
  if (this.errorProperties.isIncludeException()) {
    options = options.including(Include.EXCEPTION);
  }
  //是否打印堆栈,server.error.includeStacktrace= IncludeStacktrace.NEVER
  if (isIncludeStackTrace(request, mediaType)) {
    options = options.including(Include.STACK_TRACE);
  }
  //是否输出message
  if (isIncludeMessage(request, mediaType)) {
    options = options.including(Include.MESSAGE);
  }
  //和上面是使用的同一个配置项
  if (isIncludeBindingErrors(request, mediaType)) {
    options = options.including(Include.BINDING_ERRORS);
  }
  return options;
}

是否输出堆栈,跟2.2.0一样的逻辑:

//server.error.includeStacktrace= IncludeStacktrace.NEVER
protected boolean isIncludeStackTrace(HttpServletRequest request, MediaType produces) {
  switch (getErrorProperties().getIncludeStacktrace()) {
  case ALWAYS:
    return true;
  case ON_PARAM:
  case ON_TRACE_PARAM:
    return getTraceParameter(request);
  default:
    return false;
  }
}

是否输出message:

//添加了一个新的配置项,2.2.0是直接输出的
//server.error.includeMessage=IncludeAttribute.NEVER;
protected boolean isIncludeMessage(HttpServletRequest request, MediaType produces) {
  switch (getErrorProperties().getIncludeMessage()) {
  case ALWAYS:
    return true;
  case ON_PARAM:
    return getMessageParameter(request);
  default:
    return false;
  }
}

2.3.0新增了一个配置项server.error.includeMessage,默认是NEVER,因此默认是不是输出message的,只要开启就可以了。

当开启以下参数的时候:

server:
  error:
    includeException: true
    includeStacktrace: ALWAYS
    includeMessage: ALWAYS

输出结果:

{"timestamp":"2020-08-16T05:09:23.983+00:00",
"status":500,
"error":"Internal Server Error",
"exception":"java.lang.RuntimeException",
"trace":"java.lang.RuntimeException: 服务端异常,请稍后再试!\r\n\tat com.github.xjs.error.controller.DemoController.hello(DemoController.java:10)\r\n\tat sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\r\n\tat sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)\r\n\tat sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\r\n\tat java.lang.reflect.Method.invoke(Method.java:498)\r\n\tat org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190)\r\n\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:879)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:793)\r\n\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)\r\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040)\r\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)\r\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)\r\n\tat org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)\r\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:634)\r\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)\r\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:741)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\r\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\r\n\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\r\n\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\r\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\r\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)\r\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)\r\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541)\r\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)\r\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)\r\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)\r\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)\r\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:373)\r\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)\r\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868)\r\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1590)\r\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)\r\n\tat java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)\r\n\tat java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)\r\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\r\n\tat java.lang.Thread.run(Thread.java:748)\r\n",
"message":"服务端异常,请稍后再试!",
"path":"/hello"}

我们可以模仿BasicErrorController自定义一下异常的处理:

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class MyErrorController implements ErrorController {
    @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response)throws Exception {
        response.setContentType("text/html;charset=UTF-8");
        response.getWriter().append("服务端异常,请稍后重试");
        return null;
    }
    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        Map<String, Object> body = new HashMap<>();
        body.put("message", "服务端异常,请稍后重试");
        return new ResponseEntity(body, HttpStatus.INTERNAL_SERVER_ERROR);
    }
    @Override
    public String getErrorPath() {
        return null;
    }
}

参考代码下载:https://github.com/xjs1919/enumdemo 下面的error。

欢迎扫码加关注查看更多文章:

在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值