先看个有意思的现象:
@RestControllerpublic 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 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的@RequestMappingpublic 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);}@Overridepublic 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:
@RequestMappingpublic 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.NEVERprotected 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 ResponseEntityString, Map<String, Object> body = new HashMap<>(); body.put("message", "服务端异常,请稍后重试"); return new ResponseEntity(body, HttpStatus.INTERNAL_SERVER_ERROR); } @Override public String getErrorPath() { return null; }}