Spring 应用的多种启动方式

本文基于:
SpringBoot-2.1.4

方式1. Servlet3.0 之前使用 web.xml 配置监听器,Servlet,Filter 就可以了,如下:


    org.springframework.web.context.ContextLoaderListener


      ...


      ...

ContextLoaderListener实现了 ServletContextListener,当 Servlet 容器启动时,contextInitialized(ServletContextEvent event)方法会被回调,Spring 会做相应的 ApplicationContext 初始化。

方式2. Servlet3.0 之后,可以使用编程方式启动 Spring(不使用 Springboot)

该方式得益于 Servlet3.0 的 ServletContainerInitializer 接口,该接口使用 SPI 加载实现类,Spring 的SpringServletContainerInitializer 实现了该接口,源码如下:

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
    @Override
    public void onStartup(@Nullable Set> webAppInitializerClasses, ServletContext servletContext)
            throws ServletException {
  ...
  }
}

HandlesTypes: HandlesTypes的作用是其配置的类会被注入到onStartup方法的webAppInitializerClasses参数。

我们也可以自己实现 ServletContainerInitializer 来做我们想做的事情。但是一般我们只要实现了 AbstractAnnotationConfigDispatcherServletInitializer 就可以使用 Spring(非 SpringBoog)。

方式3. SpringBoot war包启动

如果你是使用的 SpringBoot war包启动的话,你的启动类可能是这样的:

@SpringBootApplication
public class SpringTestApplication extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(SpringTestApplication.class);
    }
}

SpringBootServletInitializer 实现了 WebApplicationInitializer,就像上面提到的,我们的启动类会被注入到 SpringServletContainerInitializeronStartup 方法。在 onStartup 方法中 Spring 会做相应的ApplicationContext 初始化。看 SpringBootServletInitializer 的源码你就会发现,其实 war 包启动的方式最后还是会使用到 SpringApplication#run。

方式4. SpringBoot jar方式启动

该方式,Spring 会通过 main 方法中的 SpringApplication.run(SpringTestApplication.class, args); 来做相应的 ApplicationContext 初始化,并且它使用嵌入式的 Tomcat 来加载 Servlet,但是它没有完全遵守 servlet3.0 的规范,你可以尝试在 SpringServletContainerInitializer 中打个断点,你会发现它并没有被运行。它其实会进入到TomcatStarterTomcatStarterSpringServletContainerInitializer 一样也实现了 ServletContainerInitializer 接口,监听器,Servlet,Filter 的注册依靠的是 ServletContextInitializer。具体请看 spring-boot中tomcat的启动过程

对于为什么不使用 SpringServletContainerInitializer? SpringBoot 在 github 的 issues 中也做了回答。Springboot 考虑到了如下的问题,我们在使用 Springboot 时,开发阶段一般都是使用内嵌 tomcat 容器,但部署时却存在两种选择:一种是打成 jar 包,使用 java -jar 的方式运行;另一种是打成 war 包,交给外置容器去运行。前者就会导致容器搜索算法出现问题,因为这是 jar 包的运行策略,不会按照 servlet3.0 的策略去加载 ServletContainerInitializer!最后作者还提供了一个替代选项:ServletContextInitializer。该类被使用在了 TomcatServer 中。其实 war 包启动也会使用到 ServletContextInitializer,只要是 SpringBoot 启动都会使用 ServletContextInitializer 而不是 ServletContainerInitializer

3.1 自定义 Servler,Filter,Listener 的一种注册方式

基于以上内容,我们可以自定义 ServletContextInitializer 的实现类来实现 Servler,Filter,Listener 的注册(无论你是war包启动还是jar包启动)。其实 Spirng 已经帮我们做好了,当你去看它的实现类时,你会看到:

  • ServletRegistrationBean:用来注册 Servlet
  • FilterRegistrationBean:用来注册 Filter
  • ServletListenerRegistrationBean:用来注册 Listener
  • ...

Spring 会通过以下方法为你注册:

ServletWebServerApplicationContext.java

private void createWebServer() {
    WebServer webServer = this.webServer;
    ServletContext servletContext = getServletContext();
    if (webServer == null && servletContext == null) { //jar包启动入口
        ServletWebServerFactory factory = getWebServerFactory();
        // 这一步 getSelfInitializer() 方法返回的是一个 ServletContextInitializer,
        // 它负责处理所有的 Servler,Filter,Listener 的注册
        this.webServer = factory.getWebServer(getSelfInitializer());
    }
    else if (servletContext != null) {//war包启动入口
        try {
            getSelfInitializer().onStartup(servletContext);
        }
        catch (ServletException ex) {
            throw new ApplicationContextException("Cannot initialize servlet context",
                    ex);
        }
    }
    initPropertySources();
}

private org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer() {
    return this::selfInitialize;
}
private void selfInitialize(ServletContext servletContext) throws ServletException {
    prepareWebApplicationContext(servletContext);
    registerApplicationScope(servletContext);
    WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);
    // getServletContextInitializerBeans()  会获取所有有注册的 ServletContextInitializer 实例
    for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
        beans.onStartup(servletContext);
    }
}

题外话

  1. 注解的方式注册Servlet。 3.0 以后,@WebServlet,@WebFilter,@WebListener + @ServletComponentScan
  2. Filter 的做种注册方式,详情

总结

  1. Servlet 3.0 后使整个应用更加轻量了,出去繁琐的 xml 配置,甚至可以动态配置 Servlet,Filter,Listener 等。
  2. 无论 SpringBoot 是 war 包还是 jar 启动,它最后的都是调用的 SpringApplication#run 来启动的。

你可能感兴趣的:(Spring 应用的多种启动方式)