1、前言
在此之前也提及过这个问题,可见SpringBoot系列记录(十一)——Springboot默认异常处理机制。当时主要是做统一异常处理,所以对于Springboot默认处理错误的自动配置记录的不够详细。好了,进入主题吧。
2、错误处理
当我们在浏览器发送一个不存在的请求时,会返回一个如下的视图:
当用postman等客户端发送请求时,则返回json数据
为什么会产生这样的效果?
这就和Springboot系列记录(十九)里说到的Springboot自动配置有关系了
我们打开mvc的错误处理自动配置类一探究竟,给容器中添加了以下组件:
- DefaultErrorAttributes
- BasicErrorController
- ErrorPageCustomizer
- DefaultErrorViewResolver
3、错误处理步骤
当4XX或者5xx等错误发生时,ErrorPageCustomizer就会生效,而它是定制错误的响应规则的,该组件会注册一个错误页面
源码:
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
ErrorPage errorPage = new ErrorPage(this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
errorPageRegistry.addErrorPages(new ErrorPage[]{errorPage});
}
可以看出,通过this.properties.getError().getPath() 来获取这个错误页面的路径,这个properties是ServerProperties
getError()方法返回的是ErrorProperties, 从下图可以看到这个路径默认是/error
此时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);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
//去哪个页面作为错误页面
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
//产生json数据
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
}
对错误页面的视图解析resolveErrorView 遍历所有的ErrorViewResolver来得到ModelAndView
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
Map<String, Object> model) {
for (ErrorViewResolver resolver : this.errorViewResolvers) {
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
而这个ErrorViewResolver的实现类其实就是前文提到的DefaultErrorViewResolver,去哪个响应页面也就是它解析得到的。
源码:
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
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) {
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) {
//默认可以找到一个页面,是在error下的状态码页面 例如:error/404
String errorViewName = "error/" + viewName;
//如果模板引擎可以解析
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
this.applicationContext);
if (provider != null) {
//模板引擎可用
return new ModelAndView(errorViewName, model);
}
return resolveResource(errorViewName, model);
}
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
//找静态资源文件夹下的 404.html
for (String location : this.resourceProperties.getStaticLocations()) {
try {
Resource resource = this.applicationContext.getResource(location);
resource = resource.createRelative(viewName + ".html");
if (resource.exists()) {
return new ModelAndView(new HtmlResourceView(resource), model);
}
}
catch (Exception ex) {
}
}
return null;
}
}
有模板引擎:error/状态码【把错误页面命名为状态码.html 放在模板引擎的error文件夹下,所有以4开头或者以5开头的状态码,可以直接写成4XX.html或5XX.html,优先寻找精确的状态码页面】,则去静态资源文件夹下找对应的页面:
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/",
"classpath:/resources/", "classpath:/static/", "classpath:/public/" };