暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

SpringBoot2.x#外置Servlet容器的配置和启动原理

糖爸的架构师之路 2021-06-24
805
写在前面

在之前的文章SpringBoot2.x嵌入式Servlet容器的配置和启动原理中,我们详细的介绍了SpringBoot启动时是如何通过jar包依赖的方式动态的创建不同类型的web容器的相关流程。其中主要针对嵌入式Servlet容器做了源码级别的解读,并通过源代码了解到如何自定义Servlet容器的相关属性。那是不是如果我们使用SpringBoot后就只能使用jar包的方式启动服务呢?SpringBoot使用的嵌入式Servlet容器有什么缺点呢?针对这些问题,今天我们就来介绍一下外置Servlet容器的配置和启动原理。

缺陷

之前的文章中我们说过,SpringBoot之所以能诞生,就是因为Spring官方意识到随着Spring生态的逐步完善,当项目高度依赖Spring并且集成大量其他类型的中间件时,手动配置会变得非常庞大和繁杂。为了解决这个问题,SpringBoot便通过自动配置的方式自动的帮我们装配了许多组件并针对每个组件都提供了基本的功能和默认的参数设置。与此同时,引入的嵌入式Web容器也可以帮助我们省掉自己去配置Web服务器的过程,大大提高开发和运维的效率。但任何事情都不会有绝对的好与坏,嵌入式Web容器也有自己的缺点,例如:

不支持JSP

无法深度优化和定制Web容器

...

针对这种情况,SpringBoot同时也给我们提供了可以外置化Servlet的相关入口,让我们可以根据自己的业务场景自由的切换。下面我们就来看一下如何使用SpringBoot搭建一个外置Servlet容器的应用。也就是通过安装Tomcat服务,并将自己的应用通过war包的方式打包并部署。怎么样,是不是感觉很熟悉的操作又回来了?那就一起来看看吧~
配置

方式一:Maven项目,手动配置

步骤一:

这种方式步骤会稍微多一点,但是如果你是一个老司机,一定对在没有SpringBoot之前的项目结构非常熟悉。没错,我们就是要构建出原生的war项目。像下面这样:

对,你没有看错,我们熟悉的webappweb.xml,是不是又回来了。激动么?
步骤二:

引入maven依赖

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
    <scope>provided</scope>
    </dependency>
    这里要留意一下,既然我们要使用Tomcat作为web容器,就要引入Tomcat对应的依赖,但是需要设置成
      <scope>provided</scope>

      即在编译和测试过程中有效,最后生成的war包时不会加入。因为servlet-api  tomcat服务器已经存在了,如果再打包会冲突。

      同时在pom.xml中定义打包方式:
          <packaging>war</packaging>

        步骤三:

        编写一个SpringBootServletInitializer的子类,并调用configure方法

        这样就完成啦,剩下的工作就是我们熟悉的配方了。通过Maven打成war包,安装Tomcat服务,将war包放在指定目录下,配置Tomcat相关属性,最后启动Tomcat就可以啦。如果你使用IDEA在本地启动,也只需要配置一下IDEA中的Tomcat配置指向本地的Tomcat安装路径,如下图:

        最后要提醒一下小伙伴,如果本地启动服务的话,就不要再使用SpringBoot主启动类的main()来启动啦。直接启动Tomcat服务就可以啦。


        方式二:Spring Initializer配置

        Spring Initializer是IDEA提供的Spring脚手架,方便我们可以快速的创建Spring工程。我们也可以利用它来快速的创建项目。

        next之后就进入详情配置,这里没啥要说的,记得将打包方式修改成war

        后面还有一些选择导入依赖的页面比较简单,这里就不在细说。创建完成后,会看到IDEA帮我们默认创建好了主启动类和SpringBootServletInitializer的子类。

        此时我们还缺少webapp和对应的web.xml,你可以通过手动的方式进行创建,同时也可以通过IDEA的配置自动的帮我们生成。这里我重点说一下第二种方式。

        首选选择Product structure -> Modules -> web,这里就是构建web工程文件结构的地方,红色字那里证明我们现在还没有webapp文件,选择➕,设置好路径后点击OK,IDEA就会帮我们自动创建webapp文件夹。如下图:

        创建好webapp后,点击上面的➕,选择web.xml,配置好路径后点击OK。web.xml也就创建成功了。

        我个人比较推荐使用IDEA帮我们生成这种方式,全程点点鼠标就可以,轻松 + 愉快。


        启动原理

        配置完成后有的小伙伴一定会有疑问,尤其是针对

        编写一个SpringBootServletInitializer的子类,并调用configure方法

        ...

        这一步骤,那为什么要创建SpringBootServletInitializer的子类,并调用configure方法呢?外置Web容器启动和之前介绍过的嵌入式Web容器启动有什么区别呢?废话不多说,我们还是直接看源码。


        Servlet 3.1规范

        首先需要了解下Servlet3.1官方提供的规范,文档地址如下:

        https://download.oracle.com/otn-pub/jcp/servlet-3_1-fr-eval-spec/servlet-3_1-final.pdf

        文档中有的8.2.4 Shared libraries runtimes pluggability,有这样一段话

        啊~原来是这样:
        • 服务器启动(web应用启动)会创建当前web应用里面每一个jar包里面ServletContainerInitializer实例。
        • ServletContainerInitializer的实现放在jar包的META-INF/services文件夹下,有一个名为
          javax.servlet.ServletContainerInitializer 的文件,内容就是ServletContainerInitializer的实现类的全类名
        • 如果我们想使用某些类,可以在@HandlesTypes注解中标识


        那么在@HandlesTypes注解中标识的类我们要如何获取到呢,再看下文档:

        啊~只要在@HandlesTypes注解中标识,便会在onStartUp()方法中告诉我们。文档看完了,我们去看看SpringBoot是应用的


        SpringServletContainerInitializer

        根据上面文档中的描述,在Spring-web包中我们找到
        java.servlet.ServletContainerInitializer
        这个文件内容就是ServletContainerInitializer的实现类的全类名。如下图:

        在ServletContainerInitializer内部,首选会看到@HandlersTypes注解

        启动Tomcat并在ServletContainerInitializer的onStartup()方法加入断点

        通过截图中的红框对比可以看出,当onStartup()方法被调用时,参数webAppInitializerClass集合中的元素正是@HandlersTypes注解中标识的WebApplicationInitializer接口的实现类。


        下面我们来看看onStartup()的内部实现

        这里没什么难点,主要做了两件事

        • 创建WebApplicationInitializer实现类的实例对象

        • 通过实例对象调用各自的onStartUp()方法


        SpringBootServletInitializer

        SpringBootServletInitializer类的onStartUp()方法中定义了创建Spring根容器的方法,如下图:

        这里最主要的目的还是为了创建并启动Spring应用,需要特别注意的是,这里会回调我们自定义并继承的configure()方法,从代码中看,重写configure的目的就是将我们的启动类提交到builder中。如下图:

        执行run()方法后的逻辑就和我们之前分析的嵌入式Web容器启动流程串联起来了


        区别

        经过刚才和之前文章的分析,不论是嵌入式Web容器还是外置的Web容器的启动流程我们都有了一个比较深入的了解。那他们之间有什么区别呢?下面我们来总结一下:
        • jar包:执行SpringBoot主类的main方法,启动IOC容器,创建嵌入式的Web容器。即先有IOC,再有Web容器
        • war包:启动服务器,服务器启动SpringBoot应用,启动ioc容器。即先有Web容器,再有IOC容器。


        遗留问题

        讲到这里,今天的内容基本上就结束啦。但是在浏览这部分的代码的时候其实还有一个问题。当SpringBoot启动时如果即引入了嵌入式Web容器的包,又配置了外置的Web容器。这时候SpringBoot会如何处理呢?带着这个疑问,我又翻阅了一下之前的代码。总算是有了答案。

        首先我们在引入pom依赖时就自动的规避了这类问题的出现,看一下pom依赖

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

          这就意味着,当应用程序被打包之后,war包中引入的lib是不会有tomcat的jar存在的。又因为

          TomcatServletWebServerFactory注入的前提是项目中要引入Tomcat包,如下图:

          所以TomcatServletWebServerFactory压根儿就不会加载。也就不会出现同时加载两个Web容器的情况。

          即便我们真的不小心引入了tomcat的包导致TomcatServletWebServerFactory加载到容器中,也不会出现问题。因为在CreateWebServer()方法中,只有当this.servletContext == null时才会执行getWebServer方法。

          而this.servletContext早在
          createRootApplicationContext()方法中的初始化
          ServletContextApplicationContextInitializer对象时就被赋值过了,所以不可能是null,也就不会执行创建嵌入式Tomcat容器的方法了。


          好啦~本文的所有内容就结束啦~谢谢阅读

          文章转载自糖爸的架构师之路,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

          评论