- 关于 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 resolvers;
private int order = Ordered.LOWEST_PRECEDENCE;
public void setExceptionResolvers(List exceptionResolvers) {
this.resolvers = exceptionResolvers;
}
public List 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;
}
@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;
public DefaultErrorAttributes() {
this.includeException = null;
}
@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 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": "我是异常"}rntat com.template.TestController.test(TestController.java:45)rntat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)rntat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)rntat java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)rntat java.base/java.lang.reflect.Method.invoke(Method.java:566)rntat org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:197)rntat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:141)rntat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:106)rntat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)rntat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)rntat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)rntat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1064)rntat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963)rntat org.springframework.web.servlet.frameworkServlet.processRequest(frameworkServlet.java:1006)rntat org.springframework.web.servlet.frameworkServlet.doGet(frameworkServlet.java:898)rntat javax.servlet.http.HttpServlet.service(HttpServlet.java:497)rntat org.springframework.web.servlet.frameworkServlet.service(frameworkServlet.java:883)rntat javax.servlet.http.HttpServlet.service(HttpServlet.java:584)rntat io.undertow.servlet.handlers.ServletHandler.handleRequest(ServletHandler.java:74)rntat io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:129)rntat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)rntat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)rntat io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:61)rntat io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)rntat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)rntat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)rntat io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:61)rntat io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)rntat org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:97)rntat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)rntat io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:61)rntat io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)rntat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)rntat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)rntat io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:61)rntat io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)rntat io.undertow.servlet.handlers.FilterHandler.handleRequest(FilterHandler.java:84)rntat io.undertow.servlet.handlers.security.ServletSecurityRoleHandler.handleRequest(ServletSecurityRoleHandler.java:62)rntat io.undertow.servlet.handlers.ServletChain$1.handleRequest(ServletChain.java:68)rntat io.undertow.servlet.handlers.ServletDispatchingHandler.handleRequest(ServletDispatchingHandler.java:36)rntat io.undertow.servlet.handlers.RedirectDirHandler.handleRequest(RedirectDirHandler.java:68)rntat io.undertow.servlet.handlers.security.SSLInformationAssociationHandler.handleRequest(SSLInformationAssociationHandler.java:117)rntat io.undertow.servlet.handlers.security.ServletAuthenticationCallHandler.handleRequest(ServletAuthenticationCallHandler.java:57)rntat io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)rntat io.undertow.security.handlers.AbstractConfidentialityHandler.handleRequest(AbstractConfidentialityHandler.java:46)rntat io.undertow.servlet.handlers.security.ServletConfidentialityConstraintHandler.handleRequest(ServletConfidentialityConstraintHandler.java:64)rntat io.undertow.security.handlers.AuthenticationMechanismsHandler.handleRequest(AuthenticationMechanismsHandler.java:60)rntat io.undertow.servlet.handlers.security.CachedAuthenticatedSessionHandler.handleRequest(CachedAuthenticatedSessionHandler.java:77)rntat io.undertow.security.handlers.AbstractSecurityContextAssociationHandler.handleRequest(AbstractSecurityContextAssociationHandler.java:43)rntat io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)rntat io.undertow.servlet.handlers.SendErrorPageHandler.handleRequest(SendErrorPageHandler.java:52)rntat io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)rntat io.undertow.servlet.handlers.ServletInitialHandler.handleFirstRequest(ServletInitialHandler.java:280)rntat io.undertow.servlet.handlers.ServletInitialHandler.access$100(ServletInitialHandler.java:79)rntat io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:134)rntat io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:131)rntat io.undertow.servlet.core.ServletRequestContextThreadSetupAction$1.call(ServletRequestContextThreadSetupAction.java:48)rntat io.undertow.servlet.core.ContextClassLoaderSetupAction$1.call(ContextClassLoaderSetupAction.java:43)rntat io.undertow.servlet.handlers.ServletInitialHandler.dispatchRequest(ServletInitialHandler.java:260)rntat io.undertow.servlet.handlers.ServletInitialHandler.access$000(ServletInitialHandler.java:79)rntat io.undertow.servlet.handlers.ServletInitialHandler$1.handleRequest(ServletInitialHandler.java:100)rntat io.undertow.server.Connectors.executeRootHandler(Connectors.java:387)rntat io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:852)rntat org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)rntat org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:2019)rntat org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1558)rntat org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1423)rntat org.xnio.XnioWorker$WorkerThreadFactory$1$1.run(XnioWorker.java:1280)rntat java.base/java.lang.Thread.run(Thread.java:834)rn",
"message": "Request processing failed; nested exception is com.exception.ErrorException: 我是异常,
"path": "/test"
}
其中 exception(异常类路径)和trace (堆栈详细信息)是默认不显示的,是不是很神奇。可以通过配置进行配置:
public class ErrorProperties {
@Value("${error.path:/error}")
private String path = "/error";
private boolean includeException;
private IncludeStacktrace includeStacktrace = IncludeStacktrace.NEVER;
private IncludeAttribute includeMessage = IncludeAttribute.NEVER;
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;
}
public enum IncludeStacktrace {
NEVER,
ALWAYS,
ON_PARAM,
@Deprecated // since 2.3.0 in favor of {@link #ON_PARAM}
ON_TRACE_PARAM;
}
public enum IncludeAttribute {
NEVER,
ALWAYS,
ON_PARAM
}
public static class Whitelabel {
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,一步一步的查找原因,费了很大劲。但是总体感觉排查问题的方向是对的,还是值了。



