SpringBoot从入门到精通—Spring Boot 错误处理机制

1、Spring Boot默认错误处理机制(现象)

        当我们使用Spring Boot发生错误的时候,如果我们没有配置错误的处理规则,那么Spring Boot就会启用内部的默认错误处理办法。比如当发生404错误的时候,网页端的效果如下:

而在别的客户端访问的时候如果出现了404错误,默认会给客户端发送一串错误消息的JSON数据

客户端的测试使用到了一个工具:Postman,感兴趣的小伙伴可以去Postman官网下载后来测试。

2、 Spring Boot默认错误处理机制(原理)

        看到这些现象我们不禁会有疑问,Spring Boot的底层是如何生成不同错误的默认错误页面的?还有他是如何区分浏览器和其他客户端的?带着疑问我们继续往下看。
        我们参照源码来分析一下(Spring Boot 2.1.7版本),具体在ErrorMvcAutoConfiguration这个错误处理自动配置类,下面是在这个类中注册的几个重要的组件的源码:

2.1 ErrorMvcAutoConfiguration源码片段
@Configuration
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
// Load before the main WebMvcAutoConfiguration so that the error View is available
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties({ ServerProperties.class, ResourceProperties.class, WebMvcProperties.class })
public class ErrorMvcAutoConfiguration {

    //注册DefaultErrorAttributs 
   	@Bean
	@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
	public DefaultErrorAttributes errorAttributes() {
		return new DefaultErrorAttributes(this.serverProperties.getError().isIncludeException());
	}
   
    //注册BaseErrorController
    @Bean
	@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
	public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
		return new BasicErrorController(errorAttributes, this.serverProperties.getError(), this.errorViewResolvers);
	}

    //注册ErrorPageCustomizer
	@Bean
	public ErrorPageCustomizer errorPageCustomizer() {
		return new ErrorPageCustomizer(this.serverProperties, this.dispatcherServletPath);
	}


   //配置DefaultErrorViewResolver内部类
   @Configuration
	static class DefaultErrorViewResolverConfiguration {

		private final ApplicationContext applicationContext;

		private final ResourceProperties resourceProperties;

		DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext,
				ResourceProperties resourceProperties) {
			this.applicationContext = applicationContext;
			this.resourceProperties = resourceProperties;
		}

        //在这个静态内部内中配置了DefaultErrorViewResolver
		@Bean
		@ConditionalOnBean(DispatcherServlet.class)
		@ConditionalOnMissingBean
		public DefaultErrorViewResolver conventionErrorViewResolver() {
			return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties);
		}

	}
}

可以看到,ErrorMvcAutoConfiguration这个错误处理类中配置了几个重要的组件:
* DefaultErrorAttributs :见名知意,这是SpringBoot定义的默认错误属性,他就是和错误信息的填充有关。
* BasicErrorController :他是Spring Boot中默认处理/error请求的Controller
* ErrorPageCustomizer :系统出现错误以后来到error请求进行处理
* DefaultErrorViewResolver:默认的出现错误后的视图解析器

继续跟踪源码

(1)DefaultErrorAttributs源码片段
@Order(Ordered.HIGHEST_PRECEDENCE)
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {

    //获得错误的属性信息,在页面上默认显示的错误信息都由这来的
    @Override
	public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
	    //new了一个LinkedHashMap
		Map<String, Object> errorAttributes = new LinkedHashMap<>();
		//产生错误放生的时间戳
		errorAttributes.put("timestamp", new Date());
		//产生错误的状态码
		addStatus(errorAttributes, webRequest);
		//错误的细节
		addErrorDetails(errorAttributes, webRequest, includeStackTrace);
		//产生错误URL路径
		addPath(errorAttributes, webRequest);
		return errorAttributes;
	}
}
(2)BasicErrorController源码片段

首选通过判断媒体的类型来选择不同的错误处理方法,核心就是下面两个方法

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {


    //浏览器的错误请求用这个处理方法来处理,产生HTML数据
    @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
	public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
	    //得到状态码
		HttpStatus status = getStatus(request);
		//把ErrorAttributs中的错误信息(上一个源码片段的返回值)填充到Model中
		Map<String, Object> model = Collections
				.unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
	    //设置响应码
		response.setStatus(status.value());
		//解析错误页面,将会在这个页面把错误信息显示出来,解析的内容包含页面地址和页面内容
		ModelAndView modelAndView = resolveErrorView(request, response, status, model);
		 //如果当前项目没有自定义错误页面(或命名没有按SpringBoot规范来做)就会去默认的错误页面(就是看到的那个白板页面) 
		return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
	}

    //其他终端返回JSON格式的数据
	@RequestMapping
	public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
		Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
		HttpStatus status = getStatus(request);
		//返回Json数据
		return new ResponseEntity<>(body, status);
	}

}

在BasicErrorController类的源码我们看到它调用了父类AbstractErrorControlle的方法resolveErrorView来处理ModelAndView,具体的实现细节如下:

AbstractErrorController源码片段
public abstract class AbstractErrorController implements ErrorController {
  protected ModelAndView resolveErrorView(HttpServletRequest request, 
                                          HttpServletResponse response, 
										  HttpStatus status,Map<String, 
										  Object> model) {
         //遍历所有的错误视图处理器,找到可用的视图解析器,而且如果我们自定义了视图解析器的话,那么SpringBoot会优先使用我们自定义的视图解析器
		for (ErrorViewResolver resolver : this.errorViewResolvers) {
			ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
			if (modelAndView != null) {
				return modelAndView;
			}
		}
		return null;
	}
}
(3)ErrorPageCustomizer源码片段
   /**
     *ErrorMvcAutoConfiguration的内部类
	 * {@link WebServerFactoryCustomizer} that configures the server's error pages.
	 */
	private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {

		private final ServerProperties properties;

		private final DispatcherServletPath dispatcherServletPath;

		protected ErrorPageCustomizer(ServerProperties properties, DispatcherServletPath dispatcherServletPath) {
			this.properties = properties;
			this.dispatcherServletPath = dispatcherServletPath;
		}

		@Override
		public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
			ErrorPage errorPage = new ErrorPage(
					this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
			errorPageRegistry.addErrorPages(errorPage);
		}

		@Override
		public int getOrder() {
			return 0;
		}
	}
(4)DefaultErrorViewResolver源码片段
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {

	private static final Map<Series, String> SERIES_VIEWS;
    //错误状态码
	static {
		Map<Series, String> views = new EnumMap<>(Series.class);
		views.put(Series.CLIENT_ERROR, "4xx");
		views.put(Series.SERVER_ERROR, "5xx");
		SERIES_VIEWS = Collections.unmodifiableMap(views);
	}

    @Override
	public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
	    // 这里如果没有拿到精确状态码(如404)的视图,则尝试拿4XX(或5XX)的视图
		ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
		if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
			modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
		}
		return modelAndView;
	}

 
  private ModelAndView resolve(String viewName, Map<String, Object> model) {
        //默认情况下Spring Boot会在error/目录下去找视图,比如error/404.html或error/4xx
		String errorViewName = "error/" + viewName;
		//如果模板引擎可以解析就有模板引擎来解析
		TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
				this.applicationContext);
		if (provider != null) {
		    //模板引擎可用的情况下返回到errorViewName指定的视图地址
			return new ModelAndView(errorViewName, model);
		}
		  //模板引擎不可用,就在静态资源文件夹下找errorViewName对应的页面   error/404.html
		return resolveResource(errorViewName, model);
	}

   //从静态资源文件夹下面找错误页面
   private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
        //遍历所有静态资源文件到的路径来看看有没有和viewName同名的视图名(网页名)
		for (String location : this.resourceProperties.getStaticLocations()) {
			try {
				Resource resource = this.applicationContext.getResource(location);
				resource = resource.createRelative(viewName + ".html");
				if (resource.exists()) {
				    //如果有就返回该视图的ModelAndView
					return new ModelAndView(new HtmlResourceView(resource), model);
				}
			}
			catch (Exception ex) {
			}
		}
	    //没有就返回null
		return null;
	}
}

        大致分析源码后可以总结Spring Boot对错误的处理流程如下:如果系统出现4xx或者5xx之类的错误,ErrorPageCustomizer就会生效(定制错误的响应规则),就会发出/error请求,然后就会被BasicErrorController处理并返回ModelAndView(网页)或者JSON(客户端)。

3、使用Spring Boot默认错误处理机制来处理我们程序中的异常

        通过分析源码我们可以发现,如果要使用Spring Boot默认的错误处理机制,我们可以把我们定制的错误页面放在/templates/error目录下的,交给模板引擎来处理;或者不使用模板引擎那就放在static/error目录下。并且给这些错误页面命名为错误码.html4xx.html5xx.html。Spring Boot就可以自动帮我们映射到错误页面。例如,处理404错误:
在/templates/error目录下放404.html


        访问浏览器,在地址栏中随便输入一个地址让他发生404错误,结果来到了我们定制的404错误页面,而不是Spring Boot默认的那个难看的白板页面。

4xx.html

<!doctype html>
<html lang="en">
 <head>
  <meta charset="UTF-8">
  <meta name="Generator" content="EditPlus®">
  <meta name="Author" content="">
  <meta name="Keywords" content="">
  <meta name="Description" content="">
  <title>Document</title>
 </head>
 <body>
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
	<h1>status:[[${status}]]</h1>
	<h2>timestamp:[[${timestamp}]]</h2>
	<h2>exception:[[${exception}]]</h2>
	<h2>message:[[${message}]]</h2>
	<h2>ext:[[${ext.code}]]</h2>
	<h2>ext:[[${ext.message}]]</h2>
</main>
 </body>
</html>
测试结果:

4、定制自己的错误信息

默认情况下,Spring Boot的错误页面中可以可得一下错误信息:

timestamp:时间戳
status:状态码
error:错误提示
exception:异常对象
message:异常消息
errors:JSR303数据校验的错误都在这里
4.1 第一种方式:使用Spring MVC的异常处理器
@ControllerAdvice
public class MyExceptionHandler {

    @ResuestBody 
    @ExceptionHandler(NullPointerException.class)
    public Map<String,Object> handleException(Exception e, HttpServletResponse response){
       Map<String,Object> map=new HashMap<>();
       map.put("code","");
       map.put("message",e.getMessage());
       map.put("exception",e.getClass());
       return map;
    } 
}

这样无论是浏览器还是别的客户端,只要出错了就全部返回的JSON数据。

4.2 第二种方式:转发到/error请求进行自适应效果处理
@ControllerAdvice
public class MyExceptionHandler {


    @ExceptionHandler(NullPointerException.class)
    public String handleException(Exception e,HttpServletResponse response, HttpServletRequest request){
       Map<String,Object> map=new HashMap<>();
       //设置状态码【必须】
       request.setAttribute("javax.servlet.error.status_code",500);
       map.put("code","null exception");
       map.put("message",e.getMessage());
       map.put("exception",e.getClass());
       //转发到/error
       return "forward:/error";
    }

}
4.3 第三种方式:编写一个MyErrorAttributes继承DefaultErrorAttributes并重写其getErrorAttributes方法

        前两种虽然都可以解决错误,但是当我们自己定义一个错误属性(比如上面的code属性)就没办法带到页面,因此我们设置的信息也就无法被带到页面显示。我们可以编写一个MyErrorAttributes继承自DefaultErrorAttributes重写其getErrorAttributes方法将我们的错误数据添加进去。

@Component   //使用我们的ErrorAttributes
public class MyErrorAttributes extends DefaultErrorAttributes {

    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        //得到原有的errorAttributes
        Map<String,Object> errorAttributes=super.getErrorAttributes(webRequest,includeStackTrace);
        errorAttributes.put("code","MyError");
        errorAttributes.remove("exception");
        errorAttributes.put("path",webRequest.getContextPath());
        return errorAttributes;
     }
}

最终的效果:响应是自适应的,以后可以通过定制ErrorAttributes改变需要返回的内容。

留言区

还能输入500个字符