关于 SpringBoot 默认异常信息返回问题整理
关于 SpringBoot 默认异常信息返回问题整理
一、我的疑问
本文从如下两个问题开展讨论分析:
-
接口抛出
RuntimeException
后 Spring 给我们做了什么? -
如何自定义默认异常信息返回?
二、具体问题具体分析
先来解释一下第一个问题,SpringMVC 在接口 throw RuntimeException
后通过 DispatcherServlet
的 processDispatchResult
处理异常,我想这个应该大家都知道,我想说的是大家进行断点的时候回发现会再次调用 /error
地址,这是因为 ErrorPageCustomize 注册了一个 ErrorPage ,所以出现错误以后来到 error 请求进行处理。
到了这里我们知道 SpringBoot 会调用 /error
请求,那么大家肯定都知道会有一个 Controller 来处理吧,对 Spring 给了一个默认的 Controller 来处理 /error
请求,它就是 BasicErrorController
; BasicErrorController
会将错误信息返回成一个 ResponseEntity
或 ModelAndView
,这个根据我们的接口实现来确定到底是返回 ResponseEntity
或 ModelAndView
。
另外大家肯定有一个疑问,异常信息的堆栈信息是如何传递的?因为上面说到 SpringBoot 接口异常后会再次请求 /error
,这里解释一下,请求我们业务接口的 request 对象和 /error
是同一个对象,既然是同一个对象那异常信息传递还难吗?我下面复制了部分源码:
public class HandlerExceptionResolverComposite implements HandlerExceptionResolver, Ordered {
@Nullable
private List<HandlerExceptionResolver> resolvers;
private int order = Ordered.LOWEST_PRECEDENCE;
/**
* Set the list of exception resolvers to delegate to.
*/
public void setExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
this.resolvers = exceptionResolvers;
}
/**
* Return the list of exception resolvers to delegate to.
*/
public List<HandlerExceptionResolver> getExceptionResolvers() {
return (this.resolvers != null ? Collections.unmodifiableList(this.resolvers) : Collections.emptyList());
}
public void setOrder(int order) {
this.order = order;
}
@Override
public int getOrder() {
return this.order;
}
/**
* Resolve the exception by iterating over the list of configured exception resolvers.
* <p>The first one to return a {@link ModelAndView} wins. Otherwise {@code null} is returned.
*/
@Override
@Nullable
public ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
if (this.resolvers != null) {
for (HandlerExceptionResolver handlerExceptionResolver : this.resolvers) {
ModelAndView mav = handlerExceptionResolver.resolveException(request, response, handler, ex);
if (mav != null) {
return mav;
}
}
}
return null;
}
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {
private static final String ERROR_ATTRIBUTE = DefaultErrorAttributes.class.getName() + ".ERROR";
private final Boolean includeException;
/**
* Create a new {@link DefaultErrorAttributes} instance.
*/
public DefaultErrorAttributes() {
this.includeException = null;
}
/**
* Create a new {@link DefaultErrorAttributes} instance.
* @param includeException whether to include the "exception" attribute
* @deprecated since 2.3.0 for removal in 2.5.0 in favor of
* {@link ErrorAttributeOptions#including(Include...)}
*/
@Deprecated
public DefaultErrorAttributes(boolean includeException) {
this.includeException = includeException;
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) {
storeErrorAttributes(request, ex);
return null;
}
private void storeErrorAttributes(HttpServletRequest request, Exception ex) {
request.setAttribute(ERROR_ATTRIBUTE, ex);
}
@Override
public Throwable getError(WebRequest webRequest) {
Throwable exception = getAttribute(webRequest, ERROR_ATTRIBUTE);
return (exception != null) ? exception : getAttribute(webRequest, RequestDispatcher.ERROR_EXCEPTION);
}
@SuppressWarnings("unchecked")
private <T> T getAttribute(RequestAttributes requestAttributes, String name) {
return (T) requestAttributes.getAttribute(name, RequestAttributes.SCOPE_REQUEST);
}
}
请求接口后调用链:
DispatcherServlet.doDispatch -> DispatcherServlet.processDispatchResult -> DispatcherServlet.processHandlerException -> HandlerExceptionResolverComposite.resolveException -> DefaultErrorAttributes.resolveException,一层套一层。
从上述源码不难看出, request.setAttribute(ERROR_ATTRIBUTE, ex);
将异常信息存入,最后通过
requestAttributes.getAttribute(name, RequestAttributes.SCOPE_REQUEST);
获取异常信息,这样就实现了异常信息的传递。
到这第一个问题应该是解释清楚了。
第二个问题我们如何自定义默认异常信息返回?我想大家都比较好奇为什么是自定义默认异常信息的返回,而不是像大部分博客写得一样增加一个统一异常处理机制? 对,我们的统一异常处理机制不在服务中,而是在其他的地方,这样我们只需要自定义 SpringBoot 的默认异常信息即可。重点来了,我想大家应该知道 SpringBoot 在异常后会返回类似的如下信息:
{
"timestamp": "2021-10-14T05:31:59.613+00:00",
"status": 500,
"error": "Internal Server Error",
"message": "Request processing failed; nested exception is com.exception.ErrorException: 我是异常",
"path": "/test"
}
其实 SpringBoot 帮我们过滤了除上述 timestamp
、 status
、error
、message
、path
外还可以返回 exception
(异常类路径)、trace
(堆栈详细信息),类似这样:
{
"timestamp": "2021-10-14T06:25:56.226+00:00",
"status": 500,
"error": "Internal Server Error",
"exception": "com.exception.ErrorException",
"trace": "com.exception.ErrorException: {\"errorCode\": 30001, \"systemId\": \"Not Definition\", \"message\": \"我是异常\"}\r\n\tat com.template.TestController.test(TestController.java:45)\r\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\r\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)\r\n\tat java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\r\n\tat java.base/java.lang.reflect.Method.invoke(Method.java:566)\r\n\tat org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:197)\r\n\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:141)\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:895)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)\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:1064)\r\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963)\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:497)\r\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)\r\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:584)\r\n\tat io.undertow.servlet.handlers.ServletHandler.handleRequest(ServletHandler.java:74)\r\n\tat io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:129)\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 io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:61)\r\n\tat io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)\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 io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:61)\r\n\tat io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)\r\n\tat org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:97)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\r\n\tat io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:61)\r\n\tat io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)\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 io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:61)\r\n\tat io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)\r\n\tat io.undertow.servlet.handlers.FilterHandler.handleRequest(FilterHandler.java:84)\r\n\tat io.undertow.servlet.handlers.security.ServletSecurityRoleHandler.handleRequest(ServletSecurityRoleHandler.java:62)\r\n\tat io.undertow.servlet.handlers.ServletChain$1.handleRequest(ServletChain.java:68)\r\n\tat io.undertow.servlet.handlers.ServletDispatchingHandler.handleRequest(ServletDispatchingHandler.java:36)\r\n\tat io.undertow.servlet.handlers.RedirectDirHandler.handleRequest(RedirectDirHandler.java:68)\r\n\tat io.undertow.servlet.handlers.security.SSLInformationAssociationHandler.handleRequest(SSLInformationAssociationHandler.java:117)\r\n\tat io.undertow.servlet.handlers.security.ServletAuthenticationCallHandler.handleRequest(ServletAuthenticationCallHandler.java:57)\r\n\tat io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)\r\n\tat io.undertow.security.handlers.AbstractConfidentialityHandler.handleRequest(AbstractConfidentialityHandler.java:46)\r\n\tat io.undertow.servlet.handlers.security.ServletConfidentialityConstraintHandler.handleRequest(ServletConfidentialityConstraintHandler.java:64)\r\n\tat io.undertow.security.handlers.AuthenticationMechanismsHandler.handleRequest(AuthenticationMechanismsHandler.java:60)\r\n\tat io.undertow.servlet.handlers.security.CachedAuthenticatedSessionHandler.handleRequest(CachedAuthenticatedSessionHandler.java:77)\r\n\tat io.undertow.security.handlers.AbstractSecurityContextAssociationHandler.handleRequest(AbstractSecurityContextAssociationHandler.java:43)\r\n\tat io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)\r\n\tat io.undertow.servlet.handlers.SendErrorPageHandler.handleRequest(SendErrorPageHandler.java:52)\r\n\tat io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)\r\n\tat io.undertow.servlet.handlers.ServletInitialHandler.handleFirstRequest(ServletInitialHandler.java:280)\r\n\tat io.undertow.servlet.handlers.ServletInitialHandler.access$100(ServletInitialHandler.java:79)\r\n\tat io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:134)\r\n\tat io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:131)\r\n\tat io.undertow.servlet.core.ServletRequestContextThreadSetupAction$1.call(ServletRequestContextThreadSetupAction.java:48)\r\n\tat io.undertow.servlet.core.ContextClassLoaderSetupAction$1.call(ContextClassLoaderSetupAction.java:43)\r\n\tat io.undertow.servlet.handlers.ServletInitialHandler.dispatchRequest(ServletInitialHandler.java:260)\r\n\tat io.undertow.servlet.handlers.ServletInitialHandler.access$000(ServletInitialHandler.java:79)\r\n\tat io.undertow.servlet.handlers.ServletInitialHandler$1.handleRequest(ServletInitialHandler.java:100)\r\n\tat io.undertow.server.Connectors.executeRootHandler(Connectors.java:387)\r\n\tat io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:852)\r\n\tat org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)\r\n\tat org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:2019)\r\n\tat org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1558)\r\n\tat org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1423)\r\n\tat org.xnio.XnioWorker$WorkerThreadFactory$1$1.run(XnioWorker.java:1280)\r\n\tat java.base/java.lang.Thread.run(Thread.java:834)\r\n",
"message": "Request processing failed; nested exception is com.exception.ErrorException: 我是异常,
"path": "/test"
}
其中 exception
(异常类路径)和trace
(堆栈详细信息)是默认不显示的,是不是很神奇。可以通过配置进行配置:
public class ErrorProperties {
/**
* Path of the error controller.
*/
@Value("${error.path:/error}")
private String path = "/error";
/**
* Include the "exception" attribute.
*/
private boolean includeException;
/**
* When to include the "trace" attribute.
*/
private IncludeStacktrace includeStacktrace = IncludeStacktrace.NEVER;
/**
* When to include "message" attribute.
*/
private IncludeAttribute includeMessage = IncludeAttribute.NEVER;
/**
* When to include "errors" attribute.
*/
private IncludeAttribute includeBindingErrors = IncludeAttribute.NEVER;
private final Whitelabel whitelabel = new Whitelabel();
public String getPath() {
return this.path;
}
public void setPath(String path) {
this.path = path;
}
public boolean isIncludeException() {
return this.includeException;
}
public void setIncludeException(boolean includeException) {
this.includeException = includeException;
}
public IncludeStacktrace getIncludeStacktrace() {
return this.includeStacktrace;
}
public void setIncludeStacktrace(IncludeStacktrace includeStacktrace) {
this.includeStacktrace = includeStacktrace;
}
public IncludeAttribute getIncludeMessage() {
return this.includeMessage;
}
public void setIncludeMessage(IncludeAttribute includeMessage) {
this.includeMessage = includeMessage;
}
public IncludeAttribute getIncludeBindingErrors() {
return this.includeBindingErrors;
}
public void setIncludeBindingErrors(IncludeAttribute includeBindingErrors) {
this.includeBindingErrors = includeBindingErrors;
}
public Whitelabel getWhitelabel() {
return this.whitelabel;
}
/**
* Include Stacktrace attribute options.
*/
public enum IncludeStacktrace {
/**
* Never add stacktrace information.
*/
NEVER,
/**
* Always add stacktrace information.
*/
ALWAYS,
/**
* Add stacktrace attribute when the appropriate request parameter is not "false".
*/
ON_PARAM,
/**
* Add stacktrace information when the "trace" request parameter is "true".
*/
@Deprecated // since 2.3.0 in favor of {@link #ON_PARAM}
ON_TRACE_PARAM;
}
/**
* Include error attributes options.
*/
public enum IncludeAttribute {
/**
* Never add error attribute.
*/
NEVER,
/**
* Always add error attribute.
*/
ALWAYS,
/**
* Add error attribute when the appropriate request parameter is not "false".
*/
ON_PARAM
}
public static class Whitelabel {
/**
* Whether to enable the default error page displayed in browsers in case of a
* server error.
*/
private boolean enabled = true;
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}
}
以上是 SpringBoot 2.4.X 的 ErrorProperties
配置文件,在配置文件中做如下配置:
server:
port: 8083
error:
include-message: always
最后提一句: SpringBoot 2.3.X 后 message 默认不返回,如下所示
:
{
"timestamp": "2021-10-14T05:31:59.613+00:00",
"status": 500,
"error": "Internal Server Error",
"message": "",
"path": "/test"
}
三、温馨提示
最后提醒一句:SpringBoot 2.3.X 后 message 默认不返回通过配置 server.error.include-message=always
可返回。
最后提醒一句:SpringBoot 2.3.X 后 message 默认不返回通过配置 server.error.include-message=always
可返回。
最后提醒一句:SpringBoot 2.3.X 后 message 默认不返回通过配置 server.error.include-message=always
可返回。
四、我的总结
刚开始排查 message 无法返回信息的时候发现是升级了 SpringBoot 后导致的,之前 SpringBoot 2.2.5 版本是没问题的,但是升级到 SpringBoot 2.4.11 后就出现问题了,所以第一反应是去查询官网,但是官网只是写了个大概,下图:
If an exception occurs during request mapping or is thrown from a request handler (such as a @Controller), the DispatcherServlet delegates to a chain of HandlerExceptionResolver beans to resolve the exception and provide alternative handling, which is typically an error response.
被这句话点醒了,所以排查问题时只能进行 debug,一步一步的查找原因,费了很大劲。但是总体感觉排查问题的方向是对的,还是值了。