1. 程式人生 > >SpringBoot原始碼分析之內建Servlet容器

SpringBoot原始碼分析之內建Servlet容器

SpringBoot內建了Servlet容器,這樣專案的釋出、部署就不需要額外的Servlet容器,直接啟動jar包即可。SpringBoot官方文件上有一個小章節內建servlet容器支援用於說明內建Servlet的相關問題。

SpringBoot原始碼分析之SpringBoot的啟動過程文章中我們瞭解到如果是Web程式,那麼會構造AnnotationConfigEmbeddedWebApplicationContext型別的Spring容器,在SpringBoot原始碼分析之Spring容器的refresh過程文章中我們知道AnnotationConfigEmbeddedWebApplicationContext型別的Spring容器在refresh的過程中會在onRefresh方法中建立內建的Servlet容器。

接下來,我們分析一下內建的Servlet容器相關的知識點。

內建Servlet容器相關的介面和類

SpringBoot對內建的Servlet容器做了一層封裝:

public interface EmbeddedServletContainer {
    // 啟動內建的Servlet容器,如果容器已經啟動,則不影響
    void start() throws EmbeddedServletContainerException;
    // 關閉內建的Servlet容器,如果容器已經關係,則不影響
    void stop() throws EmbeddedServletContainerException;
    // 內建的Servlet容器監聽的埠
    int getPort();
}

它目前有3個實現類,分別是JettyEmbeddedServletContainer、TomcatEmbeddedServletContainer和UndertowEmbeddedServletContainer,分別對應Jetty、Tomcat和Undertow這3個Servlet容器。

EmbeddedServletContainerFactory介面是一個工廠介面,用於生產EmbeddedServletContainer:

public interface EmbeddedServletContainerFactory {
    // 獲得一個已經配置好的內建Servlet容器,但是這個容器還沒有監聽埠。需要手動呼叫內建Servlet容器的start方法監聽埠
    // 引數是一群ServletContextInitializer,Servlet容器啟動的時候會遍歷這些ServletContextInitializer,並呼叫onStartup方法
    EmbeddedServletContainer getEmbeddedServletContainer(
            ServletContextInitializer... initializers);
}

ServletContextInitializer表示Servlet初始化器,用於設定ServletContext中的一些配置,在使用EmbeddedServletContainerFactory介面的getEmbeddedServletContainer方法獲取Servlet內建容器並且容器啟動的時候呼叫onStartup方法:

public interface ServletContextInitializer {
    void onStartup(ServletContext servletContext) throws ServletException;
}

EmbeddedServletContainerFactory是在EmbeddedServletContainerAutoConfiguration這個自動化配置類中被註冊到Spring容器中的(前期是Spring容器中不存在EmbeddedServletContainerFactory型別的bean,可以自己定義EmbeddedServletContainerFactory型別的bean):

@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Configuration
@ConditionalOnWebApplication // 在Web環境下才會起作用
@Import(BeanPostProcessorsRegistrar.class) // 會Import一個內部類BeanPostProcessorsRegistrar
public class EmbeddedServletContainerAutoConfiguration {

    @Configuration
    // Tomcat類和Servlet類必須在classloader中存在
    @ConditionalOnClass({ Servlet.class, Tomcat.class })
    // 當前Spring容器中不存在EmbeddedServletContainerFactory型別的例項
    @ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT)
    public static class EmbeddedTomcat {

        @Bean
        public TomcatEmbeddedServletContainerFactory tomcatEmbeddedServletContainerFactory() {
          // 上述條件註解成立的話就會構造TomcatEmbeddedServletContainerFactory這個EmbeddedServletContainerFactory
            return new TomcatEmbeddedServletContainerFactory();
        }

    }

    @Configuration
    // Server類、Servlet類、Loader類以及WebAppContext類必須在classloader中存在
    @ConditionalOnClass({ Servlet.class, Server.class, Loader.class,
            WebAppContext.class })
    // 當前Spring容器中不存在EmbeddedServletContainerFactory型別的例項
    @ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT)
    public static class EmbeddedJetty {

        @Bean
        public JettyEmbeddedServletContainerFactory jettyEmbeddedServletContainerFactory() {
            // 上述條件註解成立的話就會構造JettyEmbeddedServletContainerFactory這個EmbeddedServletContainerFactory
            return new JettyEmbeddedServletContainerFactory();
        }

    }

    @Configuration
    // Undertow類、Servlet類、以及SslClientAuthMode類必須在classloader中存在
    @ConditionalOnClass({ Servlet.class, Undertow.class, SslClientAuthMode.class })
    // 當前Spring容器中不存在EmbeddedServletContainerFactory型別的例項
    @ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT)
    public static class EmbeddedUndertow {

        @Bean
        public UndertowEmbeddedServletContainerFactory undertowEmbeddedServletContainerFactory() {
            // 上述條件註解成立的話就會構造JettyEmbeddedServletContainerFactory這個EmbeddedServletContainerFactory
            return new UndertowEmbeddedServletContainerFactory();
        }

    }
    // 在EmbeddedServletContainerAutoConfiguration自動化配置類中被匯入,實現了BeanFactoryAware介面(BeanFactory會被自動注入進來)和ImportBeanDefinitionRegistrar介面(會被ConfigurationClassBeanDefinitionReader解析並註冊到Spring容器中)
    public static class EmbeddedServletContainerCustomizerBeanPostProcessorRegistrar
              implements ImportBeanDefinitionRegistrar, BeanFactoryAware {

          private ConfigurableListableBeanFactory beanFactory;

          @Override
          public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
              if (beanFactory instanceof ConfigurableListableBeanFactory) {
                  this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
              }
          }

          @Override
          public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
                  BeanDefinitionRegistry registry) {
              if (this.beanFactory == null) {
                  return;
              }
              // 如果Spring容器中不存在EmbeddedServletContainerCustomizerBeanPostProcessor型別的bean
              if (ObjectUtils.isEmpty(this.beanFactory.getBeanNamesForType(
                      EmbeddedServletContainerCustomizerBeanPostProcessor.class, true,
                      false))) {
                  // 註冊一個EmbeddedServletContainerCustomizerBeanPostProcessor
                  registry.registerBeanDefinition(
                          "embeddedServletContainerCustomizerBeanPostProcessor",
                          new RootBeanDefinition(
                                  EmbeddedServletContainerCustomizerBeanPostProcessor.class));

              }
          }

      }

}

EmbeddedServletContainerCustomizerBeanPostProcessor是一個BeanPostProcessor,它在postProcessBeforeInitialization過程中去尋找Spring容器中EmbeddedServletContainerCustomizer型別的bean,並依次呼叫EmbeddedServletContainerCustomizer介面的customize方法做一些定製化:

@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
    throws BeansException {
  // 在Spring容器中尋找ConfigurableEmbeddedServletContainer型別的bean,SpringBoot內部的3種內建Servlet容器工廠都實現了這個介面,該介面的作用就是進行Servlet容器的配置
  // 比如新增Servlet初始化器addInitializers、新增錯誤頁addErrorPages、設定session超時時間setSessionTimeout、設定埠setPort等等
  if (bean instanceof ConfigurableEmbeddedServletContainer) {
    postProcessBeforeInitialization((ConfigurableEmbeddedServletContainer) bean);
  }
  return bean;
}

private void postProcessBeforeInitialization(
    ConfigurableEmbeddedServletContainer bean) {
  for (EmbeddedServletContainerCustomizer customizer : getCustomizers()) {
    // 遍歷獲取的每個定製化器,並呼叫customize方法進行一些定製
    customizer.customize(bean);
  }
}

private Collection<EmbeddedServletContainerCustomizer> getCustomizers() {
  if (this.customizers == null) {
    this.customizers = new ArrayList<EmbeddedServletContainerCustomizer>(
        // 找出Spring容器中EmbeddedServletContainerCustomizer型別的bean
        this.applicationContext
            .getBeansOfType(EmbeddedServletContainerCustomizer.class,
                false, false)
            .values());
    // 定製化器做排序
    Collections.sort(this.customizers, AnnotationAwareOrderComparator.INSTANCE);
    // 設定定製化器到屬性中
    this.customizers = Collections.unmodifiableList(this.customizers);
  }
  return this.customizers;
}

SpringBoot內建了一些EmbeddedServletContainerCustomizer,比如ErrorPageCustomizer、ServerProperties、TomcatWebSocketContainerCustomizer等。

定製器比如ServerProperties表示服務端的一些配置,以server為字首,比如有server.port、server.contextPath、server.displayName等,它同時也實現了EmbeddedServletContainerCustomizer介面,其中customize方法的一部分程式碼如下:

@Override
public void customize(ConfigurableEmbeddedServletContainer container) {
  // 3種ServletContainerFactory都實現了ConfigurableEmbeddedServletContainer介面,所以下面的這些設定相當於對ServletContainerFactory進行設定
  // 如果配置了埠資訊
  if (getPort() != null) {
    container.setPort(getPort());
  }
  ...
  // 如果配置了displayName
  if (getDisplayName() != null) {
    container.setDisplayName(getDisplayName());
  }
  // 如果配置了server.session.timeout,session超時時間。注意:這裡的Session指的是ServerProperties的內部靜態類Session
  if (getSession().getTimeout() != null) {
    container.setSessionTimeout(getSession().getTimeout());
  }
  ...
  // 如果使用的是Tomcat內建Servlet容器,設定對應的Tomcat配置
  if (container instanceof TomcatEmbeddedServletContainerFactory) {
    getTomcat().customizeTomcat(this,
        (TomcatEmbeddedServletContainerFactory) container);
  }
  // 如果使用的是Jetty內建Servlet容器,設定對應的Tomcat配置
  if (container instanceof JettyEmbeddedServletContainerFactory) {
    getJetty().customizeJetty(this,
        (JettyEmbeddedServletContainerFactory) container);
  }
  // 如果使用的是Undertow內建Servlet容器,設定對應的Tomcat配置
  if (container instanceof UndertowEmbeddedServletContainerFactory) {
    getUndertow().customizeUndertow(this,
        (UndertowEmbeddedServletContainerFactory) container);
  }
  // 新增SessionConfiguringInitializer這個Servlet初始化器
  // SessionConfiguringInitializer初始化器的作用是基於ServerProperties的內部靜態類Session設定Servlet中session和cookie的配置
  container.addInitializers(new SessionConfiguringInitializer(this.session));
  // 新增InitParameterConfiguringServletContextInitializer初始化器
  // InitParameterConfiguringServletContextInitializer初始化器的作用是基於ServerProperties的contextParameters配置設定到ServletContext的init param中
  container.addInitializers(new InitParameterConfiguringServletContextInitializer(
      getContextParameters()));
}

ErrorPageCustomizer在ErrorMvcAutoConfiguration自動化配置裡定義,是個內部靜態類:

@Bean
public ErrorPageCustomizer errorPageCustomizer() {
    return new ErrorPageCustomizer(this.properties);
}

private static class ErrorPageCustomizer
          implements EmbeddedServletContainerCustomizer, Ordered {

        private final ServerProperties properties;

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

        @Override
        public void customize(ConfigurableEmbeddedServletContainer container) {
            // 新增錯誤頁ErrorPage,這個ErrorPage對應的路徑是 /error
            // 可以通過配置修改 ${servletPath} + ${error.path}
            container.addErrorPages(new ErrorPage(this.properties.getServletPrefix()
                    + this.properties.getError().getPath()));
        }

        @Override
        public int getOrder() {
            return 0;
        }

   }

DispatcherServlet的構造

DispatcherServlet是SpringMVC中的核心分發器。它是在DispatcherServletAutoConfiguration這個自動化配置類裡構造的(如果Spring容器內沒有自定義的DispatcherServlet),並且還會被加到Servlet容器中(通過ServletRegistrationBean完成)。

DispatcherServletAutoConfiguration這個自動化配置類存在2個條件註解@ConditionalOnWebApplication和@ConditionalOnClass(DispatcherServlet.class)都滿足條件,所以會被構造(存在@AutoConfigureAfter(EmbeddedServletContainerAutoConfiguration.class)註解,會在EmbeddedServletContainerAutoConfiguration自動化配置類構造後構造):

@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass(DispatcherServlet.class)
@AutoConfigureAfter(EmbeddedServletContainerAutoConfiguration.class)
public class DispatcherServletAutoConfiguration ...

DispatcherServletAutoConfiguration有個內部類DispatcherServletConfiguration,它會構造DispatcherServlet(使用了條件類DefaultDispatcherServletCondition,如果Spring容器已經存在自定義的DispatcherServlet型別的bean,該類就不會被構造,會直接使用自定義的DispatcherServlet):

@Configuration
// 條件類DefaultDispatcherServletCondition,是EmbeddedServletContainerAutoConfiguration的內部類
// DefaultDispatcherServletCondition條件類會去Spring容器中找DispatcherServlet型別的例項,如果找到了不會構造DispatcherServletConfiguration,否則就是構造DispatcherServletConfiguration,該類內部會構造DispatcherServlet
// 所以如果我們要自定義DispatcherServlet的話只需要自定義DispatcherServlet即可,這樣DispatcherServletConfiguration內部就不會構造DispatcherServlet
@Conditional(DefaultDispatcherServletCondition.class)
// Servlet3.0開始才有的類,支援以編碼的形式註冊Servlet
@ConditionalOnClass(ServletRegistration.class)
// spring.mvc 為字首的配置
@EnableConfigurationProperties(WebMvcProperties.class)
protected static class DispatcherServletConfiguration {

  @Autowired
  private ServerProperties server;

  @Autowired
  private WebMvcProperties webMvcProperties;

  @Autowired(required = false)
  private MultipartConfigElement multipartConfig;

  // Spring容器註冊DispatcherServlet
  @Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
  public DispatcherServlet dispatcherServlet() {
    // 直接構造DispatcherServlet,並設定WebMvcProperties中的一些配置
    DispatcherServlet dispatcherServlet = new DispatcherServlet();
    dispatcherServlet.setDispatchOptionsRequest(
        this.webMvcProperties.isDispatchOptionsRequest());
    dispatcherServlet.setDispatchTraceRequest(
        this.webMvcProperties.isDispatchTraceRequest());
    dispatcherServlet.setThrowExceptionIfNoHandlerFound(
        this.webMvcProperties.isThrowExceptionIfNoHandlerFound());
    return dispatcherServlet;
  }

  @Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
  public ServletRegistrationBean dispatcherServletRegistration() {
    // 直接使用DispatcherServlet和server配置中的servletPath路徑構造ServletRegistrationBean
    // ServletRegistrationBean實現了ServletContextInitializer介面,在onStartup方法中對應的Servlet註冊到Servlet容器中
    // 所以這裡DispatcherServlet會被註冊到Servlet容器中,對應的urlMapping為server.servletPath配置
    ServletRegistrationBean registration = new ServletRegistrationBean(
        dispatcherServlet(), this.server.getServletMapping());
    registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
    if (this.multipartConfig != null) {
      registration.setMultipartConfig(this.multipartConfig);
    }
    return registration;
  }

  @Bean // 構造檔案上傳相關的bean
  @ConditionalOnBean(MultipartResolver.class)
  @ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
  public MultipartResolver multipartResolver(MultipartResolver resolver) {
    return resolver;
  }

}

ServletRegistrationBean實現了ServletContextInitializer介面,是個Servlet初始化器,onStartup方法程式碼:

@Override
public void onStartup(ServletContext servletContext) throws ServletException {
  Assert.notNull(this.servlet, "Servlet must not be null");
  String name = getServletName();
  if (!isEnabled()) {
    logger.info("Servlet " + name + " was not registered (disabled)");
    return;
  }
  logger.info("Mapping servlet: '" + name + "' to " + this.urlMappings);
  // 把servlet新增到Servlet容器中,Servlet容器啟動的時候會載入這個Servlet
  Dynamic added = servletContext.addServlet(name, this.servlet);
  if (added == null) {
    logger.info("Servlet " + name + " was not registered "
        + "(possibly already registered?)");
    return;
  }
  // 進行Servlet的一些配置,比如urlMapping,loadOnStartup等
  configure(added);
}

類似ServletRegistrationBean的還有ServletListenerRegistrationBean和FilterRegistrationBean,它們都是Servlet初始化器,分別都是在Servlet容器中新增Listener和Filter。

1個小漏洞:如果定義了一個名字為dispatcherServlet的bean,但是它不是DispatcherServlet型別,那麼DispatcherServlet就不會被構造,@RestController和@Controller註解的控制器就沒辦法生效:

@Bean(name = "dispatcherServlet")
public Object test() {
    return new Object();
}

內建Servlet容器的建立和啟動

web程式對應的Spring容器是AnnotationConfigEmbeddedWebApplicationContext,繼承自EmbeddedWebApplicationContext。在onRefresh方法中會去建立內建Servlet容器:

@Override
protected void onRefresh() {
  super.onRefresh();
  try {
    // 建立內建Servlet容器
    createEmbeddedServletContainer();
  }
  catch (Throwable ex) {
    throw new ApplicationContextException("Unable to start embedded container",
        ex);
  }
}

private void createEmbeddedServletContainer() {
      EmbeddedServletContainer localContainer = this.embeddedServletContainer;
      ServletContext localServletContext = getServletContext();
      // 內建Servlet容器和ServletContext都還沒初始化的時候執行
      if (localContainer == null && localServletContext == null) {
          // 從Spring容器中獲取EmbeddedServletContainerFactory,如果EmbeddedServletContainerFactory不存在或者有多個的話會丟擲異常中止程式
          EmbeddedServletContainerFactory containerFactory = getEmbeddedServletContainerFactory();
          // 獲取Servlet初始化器並建立Servlet容器,依次呼叫Servlet初始化器中的onStartup方法
          this.embeddedServletContainer = containerFactory
                  .getEmbeddedServletContainer(getSelfInitializer());
      }
      // 內建Servlet容器已經初始化但是ServletContext還沒初始化的時候執行
      else if (localServletContext != null) {
          try {
      // 對已經存在的Servlet
      容器依次呼叫Servlet初始化器中的onStartup方法
              getSelfInitializer().onStartup(localServletContext);
          }
          catch (ServletException ex) {
              throw new ApplicationContextException("Cannot initialize servlet context",
                      ex);
          }
      }
      initPropertySources();
  }

getSelfInitializer方法獲得的Servlet初始化器內部會去構造一個ServletContextInitializerBeans(Servlet初始化器的集合),ServletContextInitializerBeans構造的時候會去Spring容器中查詢ServletContextInitializer型別的bean,其中ServletRegistrationBean、FilterRegistrationBean、ServletListenerRegistrationBean會被找出(如果有定義),這3種ServletContextInitializer會在onStartup方法中將Servlet、Filter、Listener新增到Servlet容器中(如果我們只定義了Servlet、Filter或者Listener,ServletContextInitializerBeans內部會呼叫addAdaptableBeans方法把它們包裝成RegistrationBean):

// selfInitialize方法內部呼叫的getServletContextInitializerBeans方法獲得ServletContextInitializerBeans
protected Collection<ServletContextInitializer> getServletContextInitializerBeans() {
  return new ServletContextInitializerBeans(getBeanFactory());
}

private void addServletContextInitializerBean(String beanName,
          ServletContextInitializer initializer, ListableBeanFactory beanFactory) {
      if (initializer instanceof ServletRegistrationBean) {
          Servlet source = ((ServletRegistrationBean) initializer).getServlet();
          addServletContextInitializerBean(Servlet.class, beanName, initializer,
                  beanFactory, source);
      }
      else if (initializer instanceof FilterRegistrationBean) {
          Filter source = ((FilterRegistrationBean) initializer).getFilter();
          addServletContextInitializerBean(Filter.class, beanName, initializer,
                  beanFactory, source);
      }
      else if (initializer instanceof DelegatingFilterProxyRegistrationBean) {
          String source = ((DelegatingFilterProxyRegistrationBean) initializer)
                  .getTargetBeanName();
          addServletContextInitializerBean(Filter.class, beanName, initializer,
                  beanFactory, source);
      }
      else if (initializer instanceof ServletListenerRegistrationBean) {
          EventListener source = ((ServletListenerRegistrationBean<?>) initializer)
                  .getListener();
          addServletContextInitializerBean(EventListener.class, beanName, initializer,
                  beanFactory, source);
      }
      else {
          addServletContextInitializerBean(ServletContextInitializer.class, beanName,
                  initializer, beanFactory, null);
      }
  }

Servlet容器建立完畢之後在finishRefresh方法中會去啟動:

@Override
protected void finishRefresh() {
  super.finishRefresh();
  // 呼叫startEmbeddedServletContainer方法
  EmbeddedServletContainer localContainer = startEmbeddedServletContainer();
  if (localContainer != null) {
    // 釋出EmbeddedServletContainerInitializedEvent事件
    publishEvent(
        new EmbeddedServletContainerInitializedEvent(this, localContainer));
  }
}

private EmbeddedServletContainer startEmbeddedServletContainer() {
      // 先得到在onRefresh方法中構造的Servlet容器embeddedServletContainer
      EmbeddedServletContainer localContainer = this.embeddedServletContainer;
      if (localContainer != null) {
          // 啟動
          localContainer.start();
      }
      return localContainer;
  }

自定義Servlet、Filter、Listener

SpringBoot預設只會新增一個Servlet,也就是DispatcherServlet,如果我們想新增自定義的Servlet或者是Filter還是Listener,有以下幾種方法。

1.在Spring容器中宣告ServletRegistrationBean、FilterRegistrationBean或者ServletListenerRegistrationBean。原理在DispatcherServlet的構造章節中已經說明

@Bean
public ServletRegistrationBean customServlet() {
    return new ServletRegistrationBean(new CustomServlet(), "/custom");
}

private static class CustomServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("receive by custom servlet");
    }
}

[email protected]註解和@WebServlet、@WebFilter以及@WebListener註解配合使用。@ServletComponentScan註解啟用ImportServletComponentScanRegistrar類,是個ImportBeanDefinitionRegistrar介面的實現類,會被Spring容器所解析。ServletComponentScanRegistrar內部會解析@ServletComponentScan註解,然後會在Spring容器中註冊ServletComponentRegisteringPostProcessor,是個BeanFactoryPostProcessor,會去解析掃描出來的類是不是有@WebServlet、@WebListener、@WebFilter這3種註解,有的話把這3種類型的類轉換成ServletRegistrationBean、FilterRegistrationBean或者ServletListenerRegistrationBean,然後讓Spring容器去解析:

@SpringBootApplication
@ServletComponentScan
public class EmbeddedServletApplication { ... }

@WebServlet(urlPatterns = "/simple")
public class SimpleServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("receive by SimpleServlet");
    }

}

3.在Spring容器中宣告Servlet、Filter或者Listener。因為在ServletContextInitializerBeans內部會去呼叫addAdaptableBeans方法把它們包裝成ServletRegistrationBean:

@Bean(name = "dispatcherServlet")
public DispatcherServlet myDispatcherServlet() {
    return new DispatcherServlet();
}

Whitelabel Error Page原理

為什麼SpringBoot的程式裡Controller發生了錯誤,我們沒有進行異常的捕捉,會跳轉到Whitelabel Error Page頁面,這是如何實現的?

SpringBoot內部提供了一個ErrorController叫做BasicErrorController,對應的@RequestMapping地址為 “server.error.path” 配置 或者 “error.path” 配置,這2個配置沒配的話預設是/error,之前分析過ErrorPageCustomizer這個定製化器會把ErrorPage新增到Servlet容器中(這個ErrorPage的path就是上面說的那2個配置),這樣Servlet容器發生錯誤的時候就會訪問ErrorPage配置的path,所以程式發生異常且沒有被catch的話,就會走Servlet容器配置的ErrorPage。下面這段程式碼是BasicErrorController對應的處理請求方法:

@RequestMapping(produces = "text/html")
public ModelAndView errorHtml(HttpServletRequest request,
  HttpServletResponse response) {
    // 設定響應碼
    response.setStatus(getStatus(request).value());
    // 設定一些資訊,比如timestamp、statusCode、錯誤message等
    Map<String, Object> model = getErrorAttributes(request,
        isIncludeStackTrace(request, MediaType.TEXT_HTML));
    // 返回error檢視
    return new ModelAndView("error", model);
}

這裡名字為error檢視會被BeanNameViewResolver這個檢視解析器解析,它會去Spring容器中找出name為error的View,error這個bean在ErrorMvcAutoConfiguration自動化配置類裡定義,它返回了一個SpelView檢視,也就是剛才見到的Whitelabel Error Page(error.whitelabel.enabled配置需要是true,否則WhitelabelErrorViewConfiguration自動化配置類不會被註冊):

@Configuration
@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
@Conditional(ErrorTemplateMissingCondition.class)
protected static class WhitelabelErrorViewConfiguration {

  // Whitelabel Error Page
  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>");

  @Bean(name = "error") // bean的名字是error
  @ConditionalOnMissingBean(name = "error") // 名字為error的bean不存在才會構造
  public View defaultErrorView() {
    return this.defaultErrorView;
  }

  @Bean
  @ConditionalOnMissingBean(BeanNameViewResolver.class)
  public BeanNameViewResolver beanNameViewResolver() {
    // BeanNameViewResolver會去Spring容器找對應bean的檢視
    BeanNameViewResolver resolver = new BeanNameViewResolver();
    resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
    return resolver;
  }

}

如果自定義了error頁面,比如使用freemarker模板的話存在/templates/error.ftl頁面,使用thymeleaf模板的話存在/templates/error.html頁面。那麼Whitelabel Error Page就不會生效了,而是會跳到這些error頁面。這又是如何實現的呢?

這是因為ErrorMvcAutoConfiguration自動化配置類裡的內部類 WhitelabelErrorViewConfiguration自動化配置類裡有個條件類ErrorTemplateMissingCondition,它的getMatchOutcome方法:

@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
    AnnotatedTypeMetadata metadata) {
  // 從spring.factories檔案中找出key為TemplateAvailabilityProvider為類,TemplateAvailabilityProvider用來查詢檢視是否可用
  List<TemplateAvailabilityProvider> availabilityProviders = SpringFactoriesLoader
      .loadFactories(TemplateAvailabilityProvider.class,
          context.getClassLoader());
  // 遍歷各個TemplateAvailabilityProvider
  for (TemplateAvailabilityProvider availabilityProvider : availabilityProviders)
    // 如果error檢視可用
    if (availabilityProvider.isTemplateAvailable("error",
        context.getEnvironment(), context.getClassLoader(),
        context.getResourceLoader())) {
      // 條件不生效。WhitelabelErrorViewConfiguration不會被構造
      return ConditionOutcome.noMatch("Template from "
          + availabilityProvider + " found for error view");
    }
  }
  // 條件生效。WhitelabelErrorViewConfiguration被構造
  return ConditionOutcome.match("No error template view detected");
}

比如FreeMarkerTemplateAvailabilityProvider這個TemplateAvailabilityProvider的邏輯如下:

public class FreeMarkerTemplateAvailabilityProvider
        implements TemplateAvailabilityProvider {

    @Override
    public boolean isTemplateAvailable(String view, Environment environment,
            ClassLoader classLoader, ResourceLoader resourceLoader) {
        // 判斷是否存在freemarker包中的Configuration類,存在的話才會繼續
        if (ClassUtils.isPresent("freemarker.template.Configuration", classLoader)) {
            // 構造屬性解析器
            RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(environment,
                    "spring.freemarker.");
            // 設定一些配置
            String loaderPath = resolver.getProperty("template-loader-path",
                    FreeMarkerProperties.DEFAULT_TEMPLATE_LOADER_PATH);
            String prefix = resolver.getProperty("prefix",
                    FreeMarkerProperties.DEFAULT_PREFIX);
            String suffix = resolver.getProperty("suffix",
                    FreeMarkerProperties.DEFAULT_SUFFIX);
            // 查詢對應的資原始檔是否存在
            return resourceLoader.getResource(loaderPath + prefix + view + suffix)
                    .exists();
        }
        return false;
    }

}

所以BeanNameViewResolver不會被構造,Whitelabel Error Page也不會構造,而是直接去找自定義的error檢視。

備用:法國隊4-2克羅埃西亞 2018-07-16

ConfigurationClassPostProcessor這個processor是優先順序最高的被執行的processor(實現了PriorityOrdered介面)。
這個ConfigurationClassPostProcessor會去BeanFactory中找出所有有@Configuration註解的bean
【找出所有有@Configuration註解的bean,EmbeddedServletContainerAutoConfiguration會被找到  --
http://fangjian0423.github.io/2017/05/10/springboot-context-refresh/ - 
http://7x2wh6.com1.z0.glb.clouddn.com/configuration-annotation-process.png 圖】


EmbeddedServletContainerAutoConfiguration  @Import(BeanPostProcessorsRegistrar.class)


SpringApplication refreshContext(context);  -AbstractApplicationContext refresh()  invokeBeanFactoryPostProcessors(beanFactory);
PostProcessorRegistrationDelegate  invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry); postProcessor.postProcessBeanDefinitionRegistry(registry);
-- ConfigurationClassPostProcessor類 -- (分叉1 parser.parse(candidates);-- 
 BeanPostProcessorsRegistrar -setBeanFactory)-this.reader.loadBeanDefinitions(configClasses); 
--ConfigurationClassBeanDefinitionReader loadBeanDefinitions   loadBeanDefinitionsForConfigurationClass --loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
--BeanPostProcessorsRegistrar registerBeanDefinitions EmbeddedServletContainerCustomizerBeanPostProcessor 
--postProcessBeforeInitialization (自動執行,因為BeanPostProcessor, BeanFactoryAware) customizer.customize(bean); ServerProperties




spring onfresh時  EmbeddedServletContainerAutoConfiguration裡的 EmbeddedTomcat new TomcatEmbeddedServletContainerFactory();

相關推薦

SpringBoot原始碼分析之內Servlet容器

SpringBoot內建了Servlet容器,這樣專案的釋出、部署就不需要額外的Servlet容器,直接啟動jar包即可。SpringBoot官方文件上有一個小章節內建servlet容器支援用於說明內建Servlet的相關問題。在SpringBoot原始碼分析之SpringB

SpringBoot 原始碼解析 (六)----- Spring Boot的核心能力 - 內Servlet容器原始碼分析(Tomcat)

Spring Boot預設使用Tomcat作為嵌入式的Servlet容器,只要引入了spring-boot-start-web依賴,則預設是用Tomcat作為Servlet容器: <dependency> <groupId>org.springframework.boot<

SpringBoot原始碼分析 配置檔案的載入原理和優先順序

從SpringBoot原始碼分析 配置檔案的載入原理和優先順序 本文從SpringBoot原始碼分析 配置檔案的載入原理和配置檔案的優先順序     跟入原始碼之前,先提一個問題:   SpringBoot 既可以載入指定目錄下的配置檔案獲取配置項,也可

STL原始碼分析之hashtable關聯容器

前言 前面我們分析過RB-tree關聯容器, RB-tree在插入(可重複和不可重複), 刪除等操作時間複雜度都是O(nlngn), 以及滿足5個規則, 以及以他為底層的配接器; 本節就來分析hashtable另個關聯容器, 他在插入, 刪除等操作都可以做到O(1)的時間複雜度.

STL原始碼分析之slist有序容器

前言 上節我們對slist的基本構成, 構造析構做了分析, 本節 我們就來分析關於slist的基本元素操作. slist分析 基本屬性資訊 slist是隻有正向迭代, 所以只能直接獲取頭部的資料. template <class T, class Alloc =

STL原始碼分析之slist有序容器

前言 不同於list是Forward Iterator型別的雙向連結串列, slist是單向連結串列, 也是Bidirectional Iterator型別. slist主要耗費的空間小, 操作一些特定的操作更加的快, 同樣類似與slist, 在執行插入和刪除操作迭代器都不會像vec

STL原始碼分析之deque有序容器

前言 前一節我們分析了deque的基本使用, 本節我們來分析一下deque的對map的操作, 即插入, 刪除等. 但是本節只分析push, pop和刪除操作, 而insert操作有點複雜還是放到下節來分析. push, pop 因為deque的是能夠雙向操作, 所以其push

STL原始碼分析之deque有序容器

前言 deque的功能很強大, 其複雜度也比list, vector複雜很多. deque是一個random_access_iterator_tag型別. 前面分析過vector是儲存在連續的線性空間, 頭插入和刪除其代價都很大, 當陣列滿了還要重新尋找更大的空間; deque也是一

STL原始碼分析之list有序容器

前言 前兩節對list的push, pop, insert等操作做了分析, 本節準備探討list怎麼實現sort功能. list是一個迴圈雙向連結串列, 不是一個連續地址空間, 所以sort功能需要特殊的演算法單獨實現, 而不能用演算法中的sort. 當然還可以將list的元素插

STL原始碼分析RB-tree關聯容器

前言 上節我們分析了關於RB-tree的迭代器實現, 其中最重要的功能都會在rb-tree結構中呼叫. 本節我們就來分析RB-tree結構. 再來複習一下紅黑樹的規則: 每個節點的顏色是黑色或者紅色 根節點必須是黑色的 每個葉節點(NULL)必須是黑色的

[SpringBoot之一入門] 08-配置Servlet容器

概要 1.嵌入式Servlet容器 2.註冊Servlet三大元件(Servlet、Filter、Listener) 3.替換為其他嵌入式Servlet容器 4.使用外接的Servlet容器 一、嵌入式Servlet容器引數修改 如何定製和修改Ser

四、原始碼分析 Spring 之IOC 容器的高階特性

高階特性介紹 通過前面對 Spring IOC 容器的原始碼分析,我們已經基本上了解了 Spring IOC 容器對 Bean 定義資源的定位、讀入和解析過程,同時也清楚了當使用者通過 getBean 方法向 IOC 容器獲取被管理的 Bean 時,IOC 容器對 Bean 進行的初始化

SpringBoot使用外接的Servlet容器——Tomcat

##一、引言 對於SpringBoot,它提供嵌入式的Servlet容器(Tomcat),可以很方便的把應用打成可執行的jar包。這種方式:簡單、便攜。但這種方式的缺點就是預設不支援JSP,優化定製也比

springboot原始碼分析4-springboot之SpringFactoriesLoader使用

摘要:本文我們重點分析一下Spring框架中的SpringFactoriesLoader類以及META-INF/spring.factories的使用。在詳細分析之前,我們可以思考一個問題?在我們設計一套API供別人呼叫的時候,如果同一個功能的要求特別多,或者同一個介面要面對

springboot原始碼分析5-springboot之命令列引數以及原理

摘要:本文我們重點分析一下Springboot框架中的命令列引數的使用以及框架內部處理的命令列引數的原理。眾所周知,springboot專案可以有兩種方式啟動,第一種使用jar包;第二種使用war包。在使用jar方式的時候,我們可以在啟動jar包的時候設定一些命令引數。1.1

springboot原始碼分析7-環境屬性構造過程(上)

使用springboot的目的就是在專案開發中,快速出東西,因此springboot對於配置檔案的格式支援是非常豐富的,最常見的配置檔案字尾有如下四種:properties、xml、yml、yaml,比

springboot原始碼分析16-spring boot監聽器使用

摘要:spring boot提供了一系列的監聽器,方便我們開發人員使用和擴充套件。本文咱們詳細講解一下spring boot中的監聽器。spring boot中支援的事件型別定在org.springfr

springboot原始碼分析6-springboot之PropertySource類初探

在springboot原始碼分析5-springboot之命令列引數以及原理一文中,我們看到了例項化Source類的時候,會去先例項化其父類SimpleCommandLinePropertySource。SimpleCommandLinePropertySource類的建構函

SpringBoot原始碼分析之---SpringBoot專案啟動類SpringApplication淺析

原始碼版本 本文原始碼採用版本為SpringBoot 2.1.0BUILD,對應的SpringFramework 5.1.0.RC1 注意:本文只是從整體上梳理流程,不做具體深入分析 SpringBoot入口類 @SpringBootAp

SpringBoot原始碼解析 內Tomcat啟動流程

開啟原始碼過程略去不談,找到入口方法之後發現有兩次呼叫,而我們實際需要開始關注的是下面這個方法。 public static ConfigurableApplicationContext run(Object[] sources, String[] arg