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内置容器并且容

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一个内部类BeanPostProcessorsRegistrarpublic 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方法做一些定制化:

@Overridepublic 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方法的一部分代码如下:

@Overridepublic 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自动化配置里定义,是个内部静态类:


@Beanpublic 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方法代码:

@Overridepublic 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容器:

@Overrideprotected 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方法获得ServletContextInitializerBeansprotected 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方法中会去启动:

@Overrideprotected 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的构造章节中已经说明

@Beanpublic 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");    }}


2.@ServletComponentScan注解和@WebServlet、@WebFilter以及@WebListener注解配合使用。@ServletComponentScan注解启用ImportServletComponentScanRegistrar类,是个ImportBeanDefinitionRegistrar接口的实现类,会被Spring容器所解析。ServletComponentScanRegistrar内部会解析@ServletComponentScan注解,然后会在Spring容器中注册ServletComponentRegisteringPostProcessor,是个BeanFactoryPostProcessor,会去解析扫描出来的类是不是有@WebServlet、@WebListener、@WebFilter这3种注解,有的话把这3种类型的类转换成ServletRegistrationBean、FilterRegistrationBean或者ServletListenerRegistrationBean,然后让Spring容器去解析:

@SpringBootApplication@ServletComponentScanpublic 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方法:

@Overridepublic 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视图。

所谓技多不压身,我们所读过的每一本书,所学过的每一门语言,在未来指不定都能给我们意想不到的回馈呢。其实做为一个开发者,有一个学习的氛围跟一个交流圈子特别重要这里我推荐一个Java学习交流群342016322,不管你是小白还是大牛欢迎入驻,大家一起交流成长。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容