avatar

09.SpringBoot使用外部servlet容器

上次说到SpringBoot中嵌入式容器的配置和启动原理。那么SpringBoot是否可以使用外置的Servlet容器呢?答案是肯定的。接下来听我道来。

一、嵌入式容器和外置Servlet容器比较

  1. 优点:
    • 嵌入式容器相比外置的容器来说比较简单和便捷,不需要额外的部署环境。一个jar包全部搞定。
  2. 缺点
    • 嵌入式容器默认不支持JSP、优化定制比较复杂(通过ServerProperties、WebServerFactoryCustomizer两种方法)

二、如何配置?

  1. 创建maven项目时,选择打包方式为war包(这里以IDEA为例)

  2. 创建完成后,因为打包方式为war。所以SpringBoot在创建的时候,把pom文件中的spring-boot-starter-tomcat改成了provided

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
    <scope>provided</scope>
    </dependency>
  3. 这时的目录结构为

    可以发现,在in.zyzl下除了有一个WebDemoApplication的启动类还有一个ServletInitializer类。

  4. ServletInitializer 源码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    public class ServletInitializer extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
    //传入我们SpringBoot应用的主程序
    return application.sources(WebDemoApplication.class);
    }
    }

    如果需要使用外部的servlet容器,那么这个类是必须得。类名无所谓,重要的是该类必须继承SpringBootServletInitializer方法,并重写configure方法,把SpringBoot的启动类传进去。

  5. 因为在IDEA中,创建的war包,不会帮你自动创建webapp文件夹和WEB-INF以及web.xml,这里需要我们手动操作。操作如下:

    1. 右键项目名,选择open module settings

    2. 点击后,打开如下窗口,按照步骤来即可创建webapp文件夹

    3. 接下来创建WEB-INF文件夹和web.xml。这两个文件,可以自己在webapp下面,手动创建。这里我们使用IDEA创建。按照下图步骤,即可创建。

  6. 配置好外部Servlet容器。这里使用tomcat,然后启动应用。配置步骤如下;

    • 编辑Configuration

    • 新加配置

    • 选择你的本地tomcat

    • 把SpringBoot应用部署到容器

    • 部署后,启动即可。

三、原理

  1. 通过启动时候的观察,我们可以发现jar包版和war包版,启动servlet容器的顺序有些许不同。如下:

    • jar包版,先启动SpringBoot应用,再选择IOC容器并创建,接下来再是初始化servlet容器。
    • war包版,先启动servlet容器,再去启动SpringBoot应用,然后再创建IOC容器。
  2. jar包版启动原理我们已经分析了,那么war包版的他是如何启动的呢?首先我们看看servlet3.1规范8.2.4章节。中文文档参考http://jinnianshilongnian.iteye.com/blog/1750736 这里总结下:

    • 在容器启动时,会查找所有jar包下面的ServletContainerInitializer实例。
    • ServletContainerInitializer的实现类的类名必须写在META-INF/services目录下的javax.servlet.ServletContainerInitializer文件中。
    • 除了ServletContainerInitializer 外,还可以使用@HandlesTypes 注解实现同样的功能。
  3. 既然servlet容器会在启动时扫描jar包下面的所有ServletContainerInitializer 实例,而这个实例的全类名又是记录在META-INF/services/javax.servlet.ServletContainerInitializer 中,而spring-web.jar 中有这个文件,内容如下:

    1
    org.springframework.web.SpringServletContainerInitializer
  4. SpringServletContainerInitializer源码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    @HandlesTypes({WebApplicationInitializer.class})
    public class SpringServletContainerInitializer implements ServletContainerInitializer {
    public SpringServletContainerInitializer() {
    }
    //webAppInitializerClasses容器中的类型为WebApplicationInitializer
    public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException {
    List<WebApplicationInitializer> initializers = new LinkedList();
    Iterator var4;
    if(webAppInitializerClasses != null) {
    var4 = webAppInitializerClasses.iterator();
    //遍历所有实现了WebApplicationInitializer的类
    while(var4.hasNext()) {
    Class<?> waiClass = (Class)var4.next();
    //如果当前类不是接口,也不是抽象类
    if(!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) && WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
    try {
    //加入到List中
    initializers.add((WebApplicationInitializer)ReflectionUtils.accessibleConstructor(waiClass, new Class[0]).newInstance(new Object[0]));
    } catch (Throwable var7) {
    throw new ServletException("Failed to instantiate WebApplicationInitializer class", var7);
    }
    }
    }
    }

    if(initializers.isEmpty()) {
    servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
    } else {
    //如果List不为空
    servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
    AnnotationAwareOrderComparator.sort(initializers);
    var4 = initializers.iterator();
    //遍历List,并执行每个实例的onStartup()方法
    while(var4.hasNext()) {
    WebApplicationInitializer initializer = (WebApplicationInitializer)var4.next();
    initializer.onStartup(servletContext);
    }

    }
    }
    }

    也就是说Servlet容器启动时,会执行ServletContainerInitializeronStartup 方法, SpringServletContainerInitializer中的onStartup方法做了如下几件事情

    • 遍历所有实现了WebApplicationInitializer的类,如果不是接口和抽象类,则加入到list
    • 遍历list,执行里面每个实例的onStartup方法。
  5. 接下来我们来看看WebApplicationInitializer的实现类SpringBootServletInitializer,源码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    public void onStartup(ServletContext servletContext) throws ServletException {
    this.logger = LogFactory.getLog(this.getClass());
    //创建WebApplicationContext实例 createRootApplicationContext()方法如下
    WebApplicationContext rootAppContext = this.createRootApplicationContext(servletContext);
    if(rootAppContext != null) {
    servletContext.addListener(new ContextLoaderListener(rootAppContext) {
    public void contextInitialized(ServletContextEvent event) {
    }
    });
    } else {
    this.logger.debug("No ContextLoaderListener registered, as createRootApplicationContext() did not return an application context");
    }

    }
    //createRootApplicationContext方法
    protected WebApplicationContext createRootApplicationContext(ServletContext servletContext) {
    //创建SpringApplicationBuilder
    SpringApplicationBuilder builder = this.createSpringApplicationBuilder();
    //初始化运行环境之类的
    StandardServletEnvironment environment = new StandardServletEnvironment();
    environment.initPropertySources(servletContext, (ServletConfig)null);
    builder.environment(environment);
    .....
    //调用configure()方法。
    builder = this.configure(builder);
    //创建Spring应用
    SpringApplication application = builder.build();
    //调用run()方法,启动Spring应用
    return this.run(application);
    }

    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
    return builder;
    }

    protected WebApplicationContext run(SpringApplication application) {
    //SpringApplication的run方法就是我们昨天分析run方法,这里就不再说了
    return (WebApplicationContext)application.run(new String[0]);
    }

    因为SpringBootServletInitializer 是一个抽象类。从上面的源码上看,configure肯定会被子类重写。回过头来,之前我们创建war工程的时候,SpringBoot除了创建了启动类,还帮我们创建了一个类ServletInitializer 他不就是继承了SpringBootServletInitializer这个抽象类,并实现了configure方法么?源码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //继承SpringBootServletInitializer类,所以在SpringBootServletInitializer的createRootApplicationContext方法中调用configure方法,实际上是调用该类的。
    public class ServletInitializer extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
    //把SpringBoot应用启动类传进去
    return application.sources(WebDemoApplication.class);
    }

    }

    到此,整套运行流程已经出来了。即:

    1. Servlet容器启动,扫描所有的ServletContainerInitializer 实例。

    2. SpringServletContainerInitializer 实现了ServletContainerInitializer,在onStartup方法中,遍历所有实现了 WebApplicationInitializer 类的实例,并执行他的onStartup方法

    3. SpringBootServletInitializer 实现了WebApplicationInitializer ,在OnStartup方法中,调用子类ServletInitializer的configure(),初始化builder,然后创建SpringApplication,并启动它。


评论