springboot情操陶冶-web配置(二)

來源:互聯網
上載者:User

承接前文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));    }

對上述代碼的注釋作下簡單的解釋,協助讀者們理順下思路

  1. 首先其會調用所有實現了ErrorViewResolver介面的視圖解析器去找尋相應的錯誤視圖,並支援通過Order介面進行排序。所以此處預設情況下會調用DefaultErrorViewResolver來擷取view,具體的如果擷取可見上文的講解
  2. 如果上述找到了,那麼也就麼事了,但是如果還沒找到,則會預設指定名為error的視圖。
  3. 那麼如何去解析預設名為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所採用的ApplicationContextAnnotationConfigServletWebServerApplicationContext,其父類在重新整理上下文過程中的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檔案即可,具體可參照本文的講述。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.