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

2.@ServletComponentScan注解和@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视图。

一些测试代码: https://github.com/fangjian0423/springboot-analysis/tree/master/springboot-embedded-servlet-conatiner

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

推荐阅读更多精彩内容

  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,748评论 6 342
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,599评论 18 139
  • 上一篇文章中,我们分析了SpringBoot的启动过程:构造SpringApplication并调用它的run方法...
    丶Format阅读 6,755评论 6 24
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,184评论 11 349
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,581评论 18 399