SpringBoot(20)之外部Servlet容器使用及其原理

SpringBoot(20)之外部Servlet容器使用及其原理

微信搜索 zze_coding 或扫描 👉 二维码关注我的微信公众号获取更多资源推送:

使用

这里以使用本地的 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 资源目录:

image.png

image.png

5、最终目录结构如下:

image.png

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

image.png

原理分析

之前我们启动打包方式为 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 文件:

# spring-web-4.3.22.RELEASE.jar!/META-INF/services/javax.servlet.ServletContainerInitializer
org.springframework.web.SpringServletContainerInitializer

查看该类:

// org.springframework.web.SpringServletContainerInitializer
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 {
                        // <1>
                        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) {
            // <2>
            initializer.onStartup(servletContext);
        }
    }

}

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

// org.springframework.boot.web.support.SpringBootServletInitializer#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");
    }
}

接着执行 createRootApplicationContext(servletContext) 方法:

// org.springframework.boot.web.support.SpringBootServletInitializer#createRootApplicationContext
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);
    // <3>
    builder = configure(builder);
    builder.listeners(new WebEnvironmentPropertySourceInitializer(servletContext));
    // <4>
    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);
    }
    // <5>
    return run(application);
}

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

// com.springboot.webdev3.ServletInitializer#configure
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
    return application.sources(Webdev3Application.class);
}

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

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://www.zze.xyz/archives/springboot20.html

Buy me a cup of coffee ☕.