关于 SpringBoot 默认异常信息返回问题梳理

关于 SpringBoot 默认异常信息返回问题整理
一、我的疑问

本文从如下两个问题开展讨论分析:

  1. 接口抛出 RuntimeException 后 Spring 给我们做了什么?

  2. 如何自定义默认异常信息返回?

二、具体问题具体分析

先来解释一下第一个问题,SpringMVC 在接口 throw RuntimeException 后通过 DispatcherServletprocessDispatchResult 处理异常,我想这个应该大家都知道,我想说的是大家进行断点的时候回发现会再次调用 /error 地址,这是因为 ErrorPageCustomize 注册了一个 ErrorPage ,所以出现错误以后来到 error 请求进行处理。

到了这里我们知道 SpringBoot 会调用 /error 请求,那么大家肯定都知道会有一个 Controller 来处理吧,对 Spring 给了一个默认的 Controller 来处理 /error 请求,它就是 BasicErrorControllerBasicErrorController 会将错误信息返回成一个 ResponseEntityModelAndView,这个根据我们的接口实现来确定到底是返回 ResponseEntityModelAndView

另外大家肯定有一个疑问,异常信息的堆栈信息是如何传递的?因为上面说到 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 帮我们过滤了除上述 timestampstatuserrormessagepath 外还可以返回 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,一步一步的查找原因,费了很大劲。但是总体感觉排查问题的方向是对的,还是值了。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lytao123

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值