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
目录下。并且给这些错误页面命名为错误码.html
或4xx.html
、5xx.html
。Spring Boot就可以自动帮我们映射到错误页面。例如,处理404错误: 在/templates/error目录下放404.html


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改变需要返回的内容。
