java框架之SpringBoot(8)-嵌入式Servlet容器

前言

SpringBoot 默认使用的嵌入式 Servlet 容器为 Tomcat,通过依赖关系就可以看到:

java框架之SpringBoot(8)-嵌入式Servlet容器

问题:

  1. 如何定制和修改 Servlet 容器相关配置?
  2. SpringBoot 能否支持其它 Servlet 容器?

相关配置

方式一:配置文件

在普通 web 程序中我们如果需要修改 Tomcat 配置则可通过 Tomcat 目录下 conf/server.xml 修改,而在 SpringBoot 中我们只需要在项目配置文件中通过 server 节下的相关属性即可修改容器相关配置,如:

# 通用 Servlet 容器配置
server.port=8080
server.context-path=/
# Tomcat 配置
server.tomcat.uri-encoding=utf-8

方式二:容器的定制器

除了上述配置的方式,SpringBoot 还为我们提供了嵌入式 Servlet 容器的定制器来定制相关配置,例:

@Bean
public EmbeddedServletContainerCustomizer embeddedServletContainerCustomizer(){
    return new EmbeddedServletContainerCustomizer() {
        // 定制嵌入式 Servlet 容器相关规则
        @Override
        public void customize(ConfigurableEmbeddedServletContainer container) {
            // container 即为容器对象
            container.setPort(8088);
        }
    };
}

配置文件中 server 节对应的配置属性类内部也是通过该方式定义容器配置的。

注册三大组件

如果是在一个标准的 web 工程中,我们可以通过 WEB-INF/web.xml 来注册三大组件,即 Servlet、Filter、Listener。而 SpringBoot 项目默认是以 jar 包的方式通过嵌入式的 Servlet 容器来启动 web 应用,没有 web.xml,注册三大组件则需要按照以下方式:

注册Servlet-ServletRegistrationBean

@Bean
public ServletRegistrationBean myServlet(){
    return new ServletRegistrationBean(new MyServlet(), "/servlet/hello");
}

注册Filter-FilterRegistrationBean

@Bean
public FilterRegistrationBean myFilter(){
    FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
    filterRegistrationBean.setFilter(new MyFilter());
    filterRegistrationBean.setUrlPatterns(Arrays.asList("/servlet/hello","/login"));
    return filterRegistrationBean;
}

注册Listener-ListenerRegistrationBean

@Bean
public ServletListenerRegistrationBean myListener(){
    return new ServletListenerRegistrationBean(new MyListener());
}

SpringMVC 的核心 Servlet 在 SpringBoot 中就是以此种方式注册,如下:

 @Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
 public DispatcherServlet dispatcherServlet() {
     DispatcherServlet dispatcherServlet = new DispatcherServlet();
     dispatcherServlet.setDispatchOptionsRequest(
             this.webMvcProperties.isDispatchOptionsRequest());
     dispatcherServlet.setDispatchTraceRequest(
             this.webMvcProperties.isDispatchTraceRequest());
     dispatcherServlet.setThrowExceptionIfNoHandlerFound(
             this.webMvcProperties.isThrowExceptionIfNoHandlerFound());
     return dispatcherServlet;
 }

     @Configuration
     @Conditional(DispatcherServletRegistrationCondition.class)
     @ConditionalOnClass(ServletRegistration.class)
     @EnableConfigurationProperties(WebMvcProperties.class)
     @Import(DispatcherServletConfiguration.class)
     protected static class DispatcherServletRegistrationConfiguration {

         private final ServerProperties serverProperties;

         private final WebMvcProperties webMvcProperties;

         private final MultipartConfigElement multipartConfig;

         public DispatcherServletRegistrationConfiguration(
                 ServerProperties serverProperties, WebMvcProperties webMvcProperties,
                 ObjectProvider<MultipartConfigElement> multipartConfigProvider) {
             this.serverProperties = serverProperties;
             this.webMvcProperties = webMvcProperties;
             this.multipartConfig = multipartConfigProvider.getIfAvailable();
         }

         @Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
         @ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
         public ServletRegistrationBean dispatcherServletRegistration(
                 DispatcherServlet dispatcherServlet) {
             ServletRegistrationBean registration = new ServletRegistrationBean(
                     dispatcherServlet, this.serverProperties.getServletMapping());
             registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
             registration.setLoadOnStartup(
                     this.webMvcProperties.getServlet().getLoadOnStartup());
             if (this.multipartConfig != null) {
                 registration.setMultipartConfig(this.multipartConfig);
             }
             return registration;
         }

     }

org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration

切换容器

我们知道,SpringBoot 默认使用的嵌入式容器为 Tomcat,SpringBoot 默认还支持将其切换为 Jetty(对长连接有更好的支持) 或 Undertow(不支持 JSP)。

其实通过容器接口的实现类我们就可以看到其它两个容器工厂的实现:

java框架之SpringBoot(8)-嵌入式Servlet容器

切换为Jetty

首先我们要排除默认依赖的 Tomcat 场景启动器:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>

再引入 Jetty 的场景启动器即可:

 <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jetty</artifactId>
</dependency>

切换为Undertow

同 Jetty 先排除 Tomcat 的场景启动器,接着引入 Undertow 的场景启动器:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-undertow</artifactId>
</dependency>

自动配置原理

依旧是从嵌入式 Servlet 容器的自动配置类看起:

 @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
 @Configuration
 @ConditionalOnWebApplication
 @Import(BeanPostProcessorsRegistrar.class)
 public class EmbeddedServletContainerAutoConfiguration {
     @Configuration
     @ConditionalOnClass({ Servlet.class, Tomcat.class }) // 判断当前环境是否引入了嵌入式 Tomcat 依赖
     @ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT) // 当 IoC 容器中没有自定义的嵌入式 Servlet 容器工厂下方代码才生效
     public static class EmbeddedTomcat {
         @Bean
         public TomcatEmbeddedServletContainerFactory tomcatEmbeddedServletContainerFactory() {
             return new TomcatEmbeddedServletContainerFactory();
         }
     }

     @Configuration
     @ConditionalOnClass({ Servlet.class, Server.class, Loader.class,
             WebAppContext.class })  // 判断当前环境是否引入了嵌入式 Jetty 依赖
     @ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT) // 当 IoC 容器中没有自定义的嵌入式 Servlet 容器工厂下方代码才生效
     public static class EmbeddedJetty {
         @Bean
         public JettyEmbeddedServletContainerFactory jettyEmbeddedServletContainerFactory() {
             return new JettyEmbeddedServletContainerFactory();
         }
     }

     @Configuration
     @ConditionalOnClass({ Servlet.class, Undertow.class, SslClientAuthMode.class }) // 判断当前环境是否引入了嵌入式 Undertow 依赖
     @ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT) // 当 IoC 容器中没有自定义的嵌入式 Servlet 容器工厂下方代码才生效
     public static class EmbeddedUndertow {
         @Bean
         public UndertowEmbeddedServletContainerFactory undertowEmbeddedServletContainerFactory() {
             return new UndertowEmbeddedServletContainerFactory();
         }
     }

     public static class BeanPostProcessorsRegistrar
             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;
             }
             registerSyntheticBeanIfMissing(registry,
                     "embeddedServletContainerCustomizerBeanPostProcessor",
                     EmbeddedServletContainerCustomizerBeanPostProcessor.class);
             registerSyntheticBeanIfMissing(registry,
                     "errorPageRegistrarBeanPostProcessor",
                     ErrorPageRegistrarBeanPostProcessor.class);
         }

         private void registerSyntheticBeanIfMissing(BeanDefinitionRegistry registry,
                 String name, Class<?> beanClass) {
             if (ObjectUtils.isEmpty(
                     this.beanFactory.getBeanNamesForType(beanClass, true, false))) {
                 RootBeanDefinition beanDefinition = new RootBeanDefinition(beanClass);
                 beanDefinition.setSynthetic(true);
                 registry.registerBeanDefinition(name, beanDefinition);
             }
         }

     }
 }

org.springframework.boot.autoconfigure.web.EmbeddedServletContainerAutoConfiguration

切换容器

很明了,自动配置类中使用三个静态内部类来分别注册不同的嵌入式的 Servlet 容器工厂,按顺序从上到下分别为 Tomcat、Jetty、Undertow。具体要注册哪个 Servlet 容器工厂需要根据当前环境的依赖来决定,如果当前环境只引入了 Tomcat 场景依赖,那么就仅仅会注册 Tomcat 的容器工厂,其它两个 Servlet 容器工厂就不会被注册。这就是我们引入依赖便能切换 Servlet 容器的原因。

容器创建与启动

现在可以知道的是,在自动配置类中只是注册了一个嵌入式 Servlet 容器的工厂 bean,而并不是注册了嵌入式 Servlet 容器的实例 bean,顾名思义,嵌入式 Servlet 容器的工厂肯定是用来创建嵌入式 Servlet 容器的实例,那么这个嵌入式 Servlet 容器的实例是在何时被创建和启动的呢?

以默认的 Tomcat 容器为例,当我们启动一个 SpringBoot 程序时,我们会发现嵌入式 Tomcat 会随之启动,我们直接从 SpringBoot 程序的入口 run 方法开始看起:

 public static ConfigurableApplicationContext run(Object source, String... args) {
     return run(new Object[] { source }, args);
 }

 public static ConfigurableApplicationContext run(Object[] sources, String[] args) {
     return new SpringApplication(sources).run(args);
 }

 public ConfigurableApplicationContext run(String... args) {
     StopWatch stopWatch = new StopWatch();
     stopWatch.start();
     ConfigurableApplicationContext context = null;
     FailureAnalyzers analyzers = null;
     configureHeadlessProperty();
     SpringApplicationRunListeners listeners = getRunListeners(args);
     listeners.starting();
     try {
         ApplicationArguments applicationArguments = new DefaultApplicationArguments(
                 args);
         ConfigurableEnvironment environment = prepareEnvironment(listeners,
                 applicationArguments);
         Banner printedBanner = printBanner(environment);
         context = createApplicationContext();
         analyzers = new FailureAnalyzers(context);
         prepareContext(context, environment, listeners, applicationArguments,
                 printedBanner);
         refreshContext(context);
         afterRefresh(context, applicationArguments);
         listeners.finished(context, null);
         stopWatch.stop();
         if (this.logStartupInfo) {
             new StartupInfoLogger(this.mainApplicationClass)
                     .logStarted(getApplicationLog(), stopWatch);
         }
         return context;
     }
     catch (Throwable ex) {
         handleRunFailure(context, listeners, analyzers, ex);
         throw new IllegalStateException(ex);
     }
 }

org.springframework.boot.SpringApplication#run

依次执行这几个 run 方法,接着进到 27 行的 refreshContext(context) 方法:

 private void refreshContext(ConfigurableApplicationContext context) {
     refresh(context);
     if (this.registerShutdownHook) {
         try {
             context.registerShutdownHook();
         }
         catch (AccessControlException ex) {
             // Not allowed in some environments.
         }
     }
 }

org.springframework.boot.SpringApplication#refreshContext

接着执行第 2 行的 refresh(context) 方法:

 protected void refresh(ApplicationContext applicationContext) {
     Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
     ((AbstractApplicationContext) applicationContext).refresh();
 }

org.springframework.boot.SpringApplication#refresh

继续到第 3 行的 ((AbstractApplicationContext) applicationContext).refresh() 方法:

 @Override
 public final void refresh() throws BeansException, IllegalStateException {
     try {
         super.refresh();
     }
     catch (RuntimeException ex) {
         stopAndReleaseEmbeddedServletContainer();
         throw ex;
     }
 }

org.springframework.boot.context.embedded.EmbeddedWebApplicationContext#refresh

调用父类的 refresh() 方法:

 public void refresh() throws BeansException, IllegalStateException {
     synchronized(this.startupShutdownMonitor) {
         this.prepareRefresh();
         ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
         this.prepareBeanFactory(beanFactory);

         try {
             this.postProcessBeanFactory(beanFactory);
             this.invokeBeanFactoryPostProcessors(beanFactory);
             this.registerBeanPostProcessors(beanFactory);
             this.initMessageSource();
             this.initApplicationEventMulticaster();
             this.onRefresh();
             this.registerListeners();
             this.finishBeanFactoryInitialization(beanFactory);
             this.finishRefresh();
         } catch (BeansException var9) {
             if (this.logger.isWarnEnabled()) {
                 this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var9);
             }

             this.destroyBeans();
             this.cancelRefresh(var9);
             throw var9;
         } finally {
             this.resetCommonCaches();
         }

     }
 }

org.springframework.context.support.AbstractApplicationContext#refresh

在第 13 行又调用了子类的 onRefresh() 方法:

 @Override
 protected void onRefresh() {
     super.onRefresh();
     try {
         createEmbeddedServletContainer();
     }
     catch (Throwable ex) {
         throw new ApplicationContextException("Unable to start embedded container",
                 ex);
     }
 }

org.springframework.boot.context.embedded.EmbeddedWebApplicationContext#onRefresh

根据方法名可以看出,接着会通过 createEmbeddedServletContainer() 方法创建嵌入式 Servlet 容器:

 private void createEmbeddedServletContainer() {
     EmbeddedServletContainer localContainer = this.embeddedServletContainer;
     ServletContext localServletContext = getServletContext();
     if (localContainer == null && localServletContext == null) {
         EmbeddedServletContainerFactory containerFactory = getEmbeddedServletContainerFactory();
         this.embeddedServletContainer = containerFactory
                 .getEmbeddedServletContainer(getSelfInitializer());
     }
     else if (localServletContext != null) {
         try {
             getSelfInitializer().onStartup(localServletContext);
         }
         catch (ServletException ex) {
             throw new ApplicationContextException("Cannot initialize servlet context",
                     ex);
         }
     }
     initPropertySources();
 }

org.springframework.boot.context.embedded.EmbeddedWebApplicationContext#createEmbeddedServletContainer

在第 5 行通过 getEmbeddedServletContainerFactory() 获取到嵌入式 Servlet 容器工厂 bean,该 bean 在嵌入式 Servlet 容器自动配置类中就已经被注册,此时为 Tomcat 的工厂即: org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory ,然后在第 7 行通过 containerFactory.getEmbeddedServletContainer(getSelfInitializer()) 获取嵌入式 Servlet 容器的实例即 Tomcat 容器实例:

 @Override
 public EmbeddedServletContainer getEmbeddedServletContainer(
         ServletContextInitializer... initializers) {
     // 创建一个 Tomcat 实例
     Tomcat tomcat = new Tomcat();
     // Tomcat 的基本配置
     File baseDir = (this.baseDirectory != null) ? this.baseDirectory
             : createTempDir("tomcat");
     tomcat.setBaseDir(baseDir.getAbsolutePath());
     Connector connector = new Connector(this.protocol);
     tomcat.getService().addConnector(connector);
     customizeConnector(connector);
     tomcat.setConnector(connector);
     tomcat.getHost().setAutoDeploy(false);
     configureEngine(tomcat.getEngine());
     for (Connector additionalConnector : this.additionalTomcatConnectors) {
         tomcat.getService().addConnector(additionalConnector);
     }
     prepareContext(tomcat.getHost(), initializers);
     // 传入配置完成的 Tomcat 实例
     return getTomcatEmbeddedServletContainer(tomcat);
 }

org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory#getEmbeddedServletContainer

在第 21 行将完成基本配置的 Tomcat 实例传递给了 getTomcatEmbeddedServletContainer(tomcat) 方法:

 protected TomcatEmbeddedServletContainer getTomcatEmbeddedServletContainer(
         Tomcat tomcat) {
     );
 }

org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory#getTomcatEmbeddedServletContainer

接着通过第 3 行将指定的端口不小于 0 的判断结果作为是否自动启动的参数传递给了 Tomcat 容器类的构造方法,创建 Tomcat 容器实例:

 public TomcatEmbeddedServletContainer(Tomcat tomcat, boolean autoStart) {
     Assert.notNull(tomcat, "Tomcat Server must not be null");
     this.tomcat = tomcat;
     this.autoStart = autoStart;
     initialize();
 }

org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainer#TomcatEmbeddedServletContainer(org.apache.catalina.startup.Tomcat, boolean)

又在第 5 行执行了 initialize() :

 private void initialize() throws EmbeddedServletContainerException {
     TomcatEmbeddedServletContainer.logger
             .info("Tomcat initialized with port(s): " + getPortsDescription(false));
     synchronized (this.monitor) {
         try {
             addInstanceIdToEngineName();
             try {
                 final Context context = findContext();
                 context.addLifecycleListener(new LifecycleListener() {

                     @Override
                     public void lifecycleEvent(LifecycleEvent event) {
                         if (context.equals(event.getSource())
                                 && Lifecycle.START_EVENT.equals(event.getType())) {
                             removeServiceConnectors();
                         }
                     }

                 });

                 this.tomcat.start();

                 rethrowDeferredStartupExceptions();

                 try {
                     ContextBindings.bindClassLoader(context, getNamingToken(context),
                             getClass().getClassLoader());
                 }
                 catch (NamingException ex) {
                 }

                 startDaemonAwaitThread();
             }
             catch (Exception ex) {
                 containerCounter.decrementAndGet();
                 throw ex;
             }
         }
         catch (Exception ex) {
             stopSilently();
             throw new EmbeddedServletContainerException(
                     "Unable to start embedded Tomcat", ex);
         }
     }
 }

org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainer#initialize

在第 21 行通过 Tomcat 实例的 start() 方法启动了 Tomcat。

至此我们可以得出结论,随着 SpringBoot 程序的启动,SpringBoot 会使用注册的嵌入式 Servlet 容器工厂 bean 来创建嵌入式 Servlet 容器,接着会随着容器的创建来启动嵌入式 Servlet 容器。

容器配置的生效

通过上面的学习我们已经知道,可以通过修改配置文件及编写容器的定制器来修改嵌入式 Servlet 容器的配置,这些配置是如何生效的呢?

回到嵌入式 Servlet 容器的自动配置类中,我们会发现在该类上有一个 @Import(BeanPostProcessorsRegistrar.class) 注解,该注解是用来快速注册指定组件的,具体使用可参考【Import-快速注册】。查看该类的 registerBeanDefinitions 方法:

 @Override
 public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
         BeanDefinitionRegistry registry) {
     if (this.beanFactory == null) {
         return;
     }
     registerSyntheticBeanIfMissing(registry,
             "embeddedServletContainerCustomizerBeanPostProcessor",
             EmbeddedServletContainerCustomizerBeanPostProcessor.class);
     registerSyntheticBeanIfMissing(registry,
             "errorPageRegistrarBeanPostProcessor",
             ErrorPageRegistrarBeanPostProcessor.class);
 }

org.springframework.boot.autoconfigure.web.EmbeddedServletContainerAutoConfiguration.BeanPostProcessorsRegistrar#registerBeanDefinitions

在第 7 行可以看到注册了一个 embeddedServletContainerCustomizerBeanPostProcessor 即后置处理器组件,后置处理器使用可参考:【实现BeanPostProcessor接口】,查看该组件:

 package org.springframework.boot.context.embedded;

 public class EmbeddedServletContainerCustomizerBeanPostProcessor
         implements BeanPostProcessor, BeanFactoryAware {

     private ListableBeanFactory beanFactory;

     private List<EmbeddedServletContainerCustomizer> customizers;

     @Override
     public void setBeanFactory(BeanFactory beanFactory) {
         Assert.isInstanceOf(ListableBeanFactory.class, beanFactory,
                 "EmbeddedServletContainerCustomizerBeanPostProcessor can only be used "
                         + "with a ListableBeanFactory");
         this.beanFactory = (ListableBeanFactory) beanFactory;
     }

     // 初始化之前执行
     @Override
     public Object postProcessBeforeInitialization(Object bean, String beanName)
             throws BeansException {
         if (bean instanceof ConfigurableEmbeddedServletContainer) {
             postProcessBeforeInitialization((ConfigurableEmbeddedServletContainer) bean);
         }
         return bean;
     }

     @Override
     public Object postProcessAfterInitialization(Object bean, String beanName)
             throws BeansException {
         return bean;
     }

     private void postProcessBeforeInitialization(
             ConfigurableEmbeddedServletContainer bean) {
         // 遍历所有的容器定制器
         for (EmbeddedServletContainerCustomizer customizer : getCustomizers()) {
             customizer.customize(bean);
         }
     }
     // 获取所有的容器定制器
     private Collection<EmbeddedServletContainerCustomizer> getCustomizers() {
         if (this.customizers == null) {
             // Look up does not include the parent context
             this.customizers = new ArrayList<EmbeddedServletContainerCustomizer>(
                     this.beanFactory
                             .getBeansOfType(EmbeddedServletContainerCustomizer.class,
                                     false, false)
                             .values());
             Collections.sort(this.customizers, AnnotationAwareOrderComparator.INSTANCE);
             this.customizers = Collections.unmodifiableList(this.customizers);
         }
         return this.customizers;
     }

 }

org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizerBeanPostProcessor

查看第 20-26 行的 postProcessBeforeInitialization 方法,该方法会在 IoC 容器创建一个 bean 实例后、初始化之前执行。在该方法中判断了当前创建的 bean 是不是嵌入式 Servlet 容器,如果是,则通过 34 行的 postProcessBeforeInitialization 方法,在该方法中遍历所有的容器定制器,通过容器定制器的 customize 方法来配置当前创建的 bean 即当前创建的嵌入式 Servlet 容器。因为在配置文件中做容器相关配置实际也是通过容器定制器来配置容器,所以修改配置文件及编写容器的定制器来修改容器配置会对当前 IoC 容器中所有的嵌入式 Servlet 容器生效。

使用外部Servlet容器

使用

这里以使用本地的 Tomcat 为例。

1、创建一个 SpringBoot 项目,打包方式为 war。

2、将嵌入式 Tomcat 依赖的 scope 指定为 provided:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
    <scope>provided</scope>
</dependency>

3、编写一个类继承 org.springframework.boot.web.support.SpringBootServletInitializer ,重写 configure 方法,例如:

package com.springboot.webdev3;

import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.support.SpringBootServletInitializer;

public class ServletInitializer extends SpringBootServletInitializer {
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(Webdev3Application.class);
    }
}

4、创建 web 资源目录:

java框架之SpringBoot(8)-嵌入式Servlet容器

java框架之SpringBoot(8)-嵌入式Servlet容器

5、最终目录结构如下:

java框架之SpringBoot(8)-嵌入式Servlet容器

6、将其部署到本地 Tomcat 容器,启动 Tomcat,SpringBoot 项目会随之启动:

java框架之SpringBoot(8)-嵌入式Servlet容器

原理

之前我们启动打包方式为 jar 的 SpringBoot 项目时,首先是执行 SpringBoot 入口类的 main 方法,随之启动了 IoC 容器,嵌入式 Servlet 容器也随之创建并启动了。

而我们现在是启动打包方式为 war 的Spring项目,直接启动服务器,SpringBoot 应用就随之启动了。

问题来了,为什么 SpringBoot 程序会随着外部 Servlet 容器启动而启动?

Servlet 3.0后有一个新规范:

  1. 服务器启动(web 应用启动)时会当前 web 应用中(包含所有依赖 jar 中)寻找目录 WEB-INF/services 下名为 javax.servlet.ServletContainerInitializer 的文件。
  2. 在 javax.servlet.ServletContainerInitializer 文件中可指定全类名,对应类为 javax.servlet.ServletContainerInitializer 的实现类,这些实现类会随服务器的启动而创建实例并会执行类中的 onStartup 方法。
  3. 还可以通过 @HandlesTypes 注解加载我们需要的类,通过被标注类的构造方法注入。

在 SpringBoot 的 web 场景启动器依赖中就有一个 javax.servlet.ServletContainerInitializer 文件:

org.springframework.web.SpringServletContainerInitializer

spring-web-4.3.22.RELEASE.jar!/META-INF/services/javax.servlet.ServletContainerInitializer

查看该类:

 package org.springframework.web;

 import java.lang.reflect.Modifier;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.ServiceLoader;
 import java.util.Set;
 import javax.servlet.ServletContainerInitializer;
 import javax.servlet.ServletContext;
 import javax.servlet.ServletException;
 import javax.servlet.annotation.HandlesTypes;

 import org.springframework.core.annotation.AnnotationAwareOrderComparator;

 @HandlesTypes(WebApplicationInitializer.class)
 public class SpringServletContainerInitializer implements ServletContainerInitializer {

     @Override
     public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
             throws ServletException {

         List<WebApplicationInitializer> initializers = new LinkedList<WebApplicationInitializer>();

         if (webAppInitializerClasses != null) {
             for (Class<?> waiClass : webAppInitializerClasses) {
                 if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
                         WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
                     try {
                         initializers.add((WebApplicationInitializer) waiClass.newInstance());
                     }
                     catch (Throwable ex) {
                         throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
                     }
                 }
             }
         }

         if (initializers.isEmpty()) {
             servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
             return;
         }

         AnnotationAwareOrderComparator.sort(initializers);
         servletContext.log("Spring WebApplicationInitializers detected on classpath: " + initializers);

         for (WebApplicationInitializer initializer : initializers) {
             initializer.onStartup(servletContext);
         }
     }

 }

org.springframework.web.SpringServletContainerInitializer

看到源码就很清晰了,应用在启动时会创建该类实例执行它的 onStartup 方法,而在该类上通过标注 @HandlesTypes(WebApplicationInitializer.class) 将当前程序中的所有 WebApplicationInitializer 的实现类的字节码对象通过构造方法注入,而 SpringBootServletInitializer 类就是 WebApplicationInitializer 的一个实现类,所以我们自己编写的 ServletInitializer 的字节码对象将会被注入,并且在第 29 行创建实例,在第 47 行执行了我们自己编写的 ServletInitializer 类对象的 onStartup 方法:

 @Override
 public void onStartup(ServletContext servletContext) throws ServletException {
     this.logger = LogFactory.getLog(getClass());
     WebApplicationContext rootAppContext = createRootApplicationContext(
             servletContext);
     if (rootAppContext != null) {
         servletContext.addListener(new ContextLoaderListener(rootAppContext) {
             @Override
             public void contextInitialized(ServletContextEvent event) {
             }
         });
     }
     else {
         this.logger.debug("No ContextLoaderListener registered, as "
                 + "createRootApplicationContext() did not "
                 + "return an application context");
     }
 }

org.springframework.boot.web.support.SpringBootServletInitializer#onStartup

接着执行 createRootApplicationContext(servletContext) 方法:

 protected WebApplicationContext createRootApplicationContext(
         ServletContext servletContext) {
     SpringApplicationBuilder builder = createSpringApplicationBuilder();
     builder.main(getClass());
     ApplicationContext parent = getExistingRootWebApplicationContext(servletContext);
     if (parent != null) {
         this.logger.info("Root context already created (using as parent).");
         servletContext.setAttribute(
                 WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, null);
         builder.initializers(new ParentContextApplicationContextInitializer(parent));
     }
     builder.initializers(
             new ServletContextApplicationContextInitializer(servletContext));
     builder.contextClass(AnnotationConfigEmbeddedWebApplicationContext.class);
     builder = configure(builder);
     builder.listeners(new WebEnvironmentPropertySourceInitializer(servletContext));
     SpringApplication application = builder.build();
     if (application.getSources().isEmpty() && AnnotationUtils
             .findAnnotation(getClass(), Configuration.class) != null) {
         application.getSources().add(getClass());
     }
     Assert.state(!application.getSources().isEmpty(),
             "No SpringApplication sources have been defined. Either override the "
                     + "configure method or add an @Configuration annotation");
     if (this.registerErrorPageFilter) {
         application.getSources().add(ErrorPageFilterConfiguration.class);
     }
     return run(application);
 }

org.springframework.boot.web.support.SpringBootServletInitializer#createRootApplicationContext

接着在第 15 行又执行了 configure 方法,而这个 configure 方法正是我们自己编写的继承 SpringBootServletInitializer 类重写的 configure 方法:

 @Override
 protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
     return application.sources(Webdev3Application.class);
 }

com.springboot.webdev3.ServletInitializer#configure

在该方法中通过 application.sources(Webdev3Application.class) 传入了当前 SpringBoot 项目的入口类,返回一个 Spring 程序构建器。回到上一步,在 15 行拿到该构建器,在第 17 行创建了 Spring 程序实例,最后在第 28 行执行了 Spring 程序实例的 run 方法,即 SpringBoot 程序随服务器的启动而启动了。

上一篇:Android 9.0适配遇到的问题1


下一篇:redis之入门操作