springboot情操陶冶-web配置(二)
承接前文springboot情操陶冶-web配置(一),在分析mvc的配置之前先了解下其默认的错误界面是如何显示的
404界面
springboot有个比较有趣的配置server.error.whitelabel.enabled,可用来管理404界面的显示方式,是简单的显示还是详细的显示。
指定为false的时候,则会简简单单的显示视图找不到的错误信息,如下
指定为true的时候(默认配置),则会显示前文样例中的错误信息,如下
源码层分析
springboot安排了ErrorMvcAutoConfiguration自动配置类来处理错误页面的相关信息,笔者分几个步骤来进行分析
No.1 脑壳上的注解看一发
@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 })public class ErrorMvcAutoConfiguration { }
可以看出其是排在WebMvcAutoConfiguration配置类之前的,那么为什么需要排在前面呢?看注释是说这样才可以使error视图有效,那怎么实现的呢?笔者带着问题继续往下探索
No.2 DefaultErrorViewResolverConfiguration内部类-错误视图解析器注册
@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); } }
DefaultErrorViewResolver这个默认的错误视图解析器很有意思,里面包含了一些默认的处理,也分几个小步骤来吧,这样会显得清晰
静态方法了解
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); }
应该是对HTTP状态码的映射处理,以4开头的是客户端错误,5开头的为服务端错误
构造函数了解
public DefaultErrorViewResolver(ApplicationContext applicationContext, ResourceProperties resourceProperties) { Assert.notNull(applicationContext, "ApplicationContext must not be null"); Assert.notNull(resourceProperties, "ResourceProperties must not be null"); this.applicationContext = applicationContext; this.resourceProperties = resourceProperties; // 模板加载器 this.templateAvailabilityProviders = new TemplateAvailabilityProviders( applicationContext); }
上述的模板加载器主要是读取所有spring.factories中的org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider
对应的属性值,本质也就是模板的渲染器,比如我们常用的freemarker、velocity、jsp等等
视图对象获取了解
@Override public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) { // 优先根据状态码来查找view静态资源,比如404则会查找error/404视图 ModelAndView modelAndView = resolve(String.valueOf(status), model); if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) { // 上述不存在则再查找error/4xx或者error/5xx视图 modelAndView = resolve(SERIES_VIEWS.get(status.series()), model); } return modelAndView; } private ModelAndView resolve(String viewName, Map<String, Object> model) { String errorViewName = "error/" + viewName; // 通过模板加载器查找是否含有符合要求的视图资源 TemplateAvailabilityProvider provider = this.templateAvailabilityProviders .getProvider(errorViewName, this.applicationContext); if (provider != null) { return new ModelAndView(errorViewName, model); } return resolveResource(errorViewName, model); } // 默认查找staticLocation指定路径的资源,比如classpath:/static/error/404.html private ModelAndView resolveResource(String viewName, Map<String, Object> model) { for (String location : this.resourceProperties.getStaticLocations()) { try { Resource resource = this.applicationContext.getResource(location); resource = resource.createRelative(viewName + ".html"); if (resource.exists()) { // view类型为HtmlResourceView,直接将html资源输出到response对象中 return new ModelAndView(new HtmlResourceView(resource), model); } } catch (Exception ex) { } } return null; }
通过上述的代码注释,基本可以得知错误视图的查找规则,所以用户可以简单的在static目录下配置对应状态码的页面比如error/404.html或者error/500.html;当然也可以配置统一的页面error/4xx.html或者error/5xx.html
那如果我们啥也不指定,那上述的错误提示信息是如何展示的呢?
No.3 WhitelabelErrorViewConfiguration-白板错误视图配置
// server.error.whitelabel.enabled开关,默认是打开的 @Configuration @ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true) @Conditional(ErrorTemplateMissingCondition.class) protected static class WhitelabelErrorViewConfiguration { // 熟悉的打印信息 private final SpelView defaultErrorView = new SpelView( "<html><body><h1>Whitelabel Error Page</h1>" + "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>" + "<div id='created'>${timestamp}</div>" + "<div>There was an unexpected error (type=${error}, status=${status}).</div>" + "<div>${message}</div></body></html>"); // 创建了名为error的视图对象 @Bean(name = "error") @ConditionalOnMissingBean(name = "error") public View defaultErrorView() { return this.defaultErrorView; } // 与上面的View对象搭配使用 @Bean @ConditionalOnMissingBean public BeanNameViewResolver beanNameViewResolver() { BeanNameViewResolver resolver = new BeanNameViewResolver(); resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10); return resolver; } }
上述就是我们开头可见的错误信息的处理处,详细的用户可自行查阅代码
No.4 构造函数了解
public ErrorMvcAutoConfiguration(ServerProperties serverProperties, ObjectProvider<List<ErrorViewResolver>> errorViewResolversProvider) { this.serverProperties = serverProperties; this.errorViewResolvers = errorViewResolversProvider.getIfAvailable(); }
上述的errorViewResolverProvider便会加载第二步骤的DefaultViewResolver,当然用户也可以自定义去实现ErrorViewResolver接口。这些错误的视图解析器将会在下一步骤的controller层被调用
No.5 error控制器注册
@Bean @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT) public DefaultErrorAttributes errorAttributes() { return new DefaultErrorAttributes( this.serverProperties.getError().isIncludeException()); } // 创建BasicErrorController控制器用于响应server.error.path指定的路径,默认为/error @Bean @ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT) public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) { return new BasicErrorController(errorAttributes, this.serverProperties.getError(), this.errorViewResolvers); }
此处的BasicErrorController对象则会默认响应/error的请求,其内部写了一个返回html页面的响应方法
@RequestMapping(produces = "text/html") 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()); // 调用errorViewResolvers集合去获取对应的错误视图 ModelAndView modelAndView = resolveErrorView(request, response, status, model); // 如果没指定相应的视图,则会采用默认的名为error的视图 return (modelAndView != null ? modelAndView : new ModelAndView("error", model)); }
对上述代码的注释作下简单的解释,帮助读者们理顺下思路
首先其会调用所有实现了ErrorViewResolver接口的视图解析器去找寻相应的错误视图,并支持通过Order接口进行排序。所以此处默认情况下会调用DefaultErrorViewResolver来获取view,具体的如果获取可见上文的讲解
如果上述找到了,那么也就么事了,但是如果还没找到,则会默认指定名为error的视图。
那么如何去解析默认名为error的视图呢?答案在DispatcherServlet在最终确定渲染视图的时候,会统一调用所有实现了ViewResolver接口的视图解析器去获取视图对象,那么第三步骤中的BeanNameViewResolver对象便会找寻到对应的SpelView视图,由其来进行相应的渲染
在此处笔者回答下开头的问题,为什么ErrorMvcAutoConfiguration需要放在DispatcherServletAutoConfiguration之前,其实最主要的是后者并没有去注册BeanViewResolver,此处上了一份保险,好让能正确的找到SpelView对象
error请求问题
经过上文的分析,我们知道了BasicErrorController用来处理访问方式为GET [/error]的请求并处理得到相应的错误视图,那么最重要的问题来了,到底怎么在出现资源找不到的时候去路由至此路径上呢?笔者继续带着这个问题去探索
No.1 ErrorPageCustomizer-错误页面配置
@Bean public ErrorPageCustomizer errorPageCustomizer() { return new ErrorPageCustomizer(this.serverProperties); } private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered { private final ServerProperties properties; protected ErrorPageCustomizer(ServerProperties properties) { this.properties = properties; } @Override public void registerErrorPages(ErrorPageRegistry errorPageRegistry) { // 默认路径为/error ErrorPage errorPage = new ErrorPage( this.properties.getServlet().getServletPrefix() + this.properties.getError().getPath()); // 注册 errorPageRegistry.addErrorPages(errorPage); } @Override public int getOrder() { return 0; } }
上述的errorPage貌似展示了一点信息,可能是会去访问/error的源头,那么ErrorPageCustomizer#registerErrorPages()是如何被调用的呢?继续往下
No.2 ServletWebAutoConfiguration引入的时候还注册了一个BeanPostProcessor
@Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { if (this.beanFactory == null) { return; } registerSyntheticBeanIfMissing(registry, "webServerFactoryCustomizerBeanPostProcessor", WebServerFactoryCustomizerBeanPostProcessor.class); // 就是这个 registerSyntheticBeanIfMissing(registry, "errorPageRegistrarBeanPostProcessor", ErrorPageRegistrarBeanPostProcessor.class); }
我们直接去关注其主要的方法
// 注册了相应的错误界面 private void postProcessBeforeInitialization(ErrorPageRegistry registry) { for (ErrorPageRegistrar registrar : getRegistrars()) { registrar.registerErrorPages(registry); } } private Collection<ErrorPageRegistrar> getRegistrars() { if (this.registrars == null) { // Look up does not include the parent context this.registrars = new ArrayList<>(this.beanFactory .getBeansOfType(ErrorPageRegistrar.class, false, false).values()); this.registrars.sort(AnnotationAwareOrderComparator.INSTANCE); this.registrars = Collections.unmodifiableList(this.registrars); } return this.registrars; }
至于为什么在该类中去注册这个processor去执行注册错误页面,看来这个路径的转发应该与web容器有关。其实追踪源头其实将错误界面注册到了相应的web容器中(Tomcat),具体的读者可自行去分析。
No.4 web容器加载(插曲,顺带提一下)
我们都知道springboot对环境为Servlet所采用的ApplicationContext为AnnotationConfigServletWebServerApplicationContext,其父类在刷新上下文过程中的onRefresh()方法便去启动了web容器
@Override protected void onRefresh() { super.onRefresh(); try { // 创建web服务器 createWebServer(); } catch (Throwable ex) { throw new ApplicationContextException("Unable to start web server", ex); } } private void createWebServer() { WebServer webServer = this.webServer; ServletContext servletContext = getServletContext(); if (webServer == null && servletContext == null) { // 默认为TomcatServletWebServerFactory ServletWebServerFactory factory = getWebServerFactory(); // 初始化servlet/filter等 this.webServer = factory.getWebServer(getSelfInitializer()); } else if (servletContext != null) { try { getSelfInitializer().onStartup(servletContext); } catch (ServletException ex) { throw new ApplicationContextException("Cannot initialize servlet context", ex); } } initPropertySources(); }
上述的代码主要会在ServletContext上注册Filters和Servlets集合并且注册ErrorPages,限于代码过长,读者可自行分析。而具体的去启动web容器则是在finishRefresh()方法中
@Override protected void finishRefresh() { super.finishRefresh(); // 启动 WebServer webServer = startWebServer(); if (webServer != null) { publishEvent(new ServletWebServerInitializedEvent(webServer, this)); } }
No.5 StandardHostValve-错误界面应用
private void status(Request request, Response response) { int statusCode = response.getStatus(); .... // 优先查找404对应的ErrorPage ErrorPage errorPage = context.findErrorPage(statusCode); if (errorPage == null) { // 0-默认的ErrorPage,此处便是上文注册的 errorPage = context.findErrorPage(0); } if (errorPage != null && response.isErrorReportRequired()) { .... } }
此源码来源于tomcat,这让笔者想起了针对状态码的page配置
<!--404 error page specified based on Tomcat--> <error-page> <error-code>404</error-code> <location>/404.html</location> </error-page>
小结
本文的内容较多,需要耐心阅读,读者只需要了解View视图的解析加载便可通读全文,如果想要自定义状态码视图则直接在classpath:/static/error目录下新建相应的状态码HTML文件即可,具体可参照本文的讲述。
作者:南柯问天 出处:http://www.cnblogs.com/question-sky/ 本文版权归本人和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
共同学习,写下你的评论
评论加载中...
作者其他优质文章