SpringBoot灵魂拷问之内嵌Servlet启动

770 阅读16分钟

坐标杭州,平时打打游戏、看看书、为公司写写bug,但有幸为公司招聘新鲜血液,在筛查简历的时候,发现大家必填的一项SpringBoot,包括(掌握SpringBoot了,熟悉SpringBoot了,熟悉使用了,精通等等)什么~~还有精通的吗,看来这小伙子必须得面一面。

所以作为一个老实、沉稳的面试官,SpringBoot的这种面试深海还是需要跟求职人深入探讨一下的,基本上的讨论包括了自动装配原理,什么是内嵌server容器,有哪些方式生成bean呀等等,今天的主题就是一期探讨下内嵌的servlet是如何启动、配置的

为何使用java -jar就能启动web项目

引用Spring Boot编程思想第2.2运行SpringBoot应用

我们的项目启动类被装载并执行,资源中的Start-Class属性(也即我们的启动类)被JarLauncher管理项目引导类

私下想要尝试的小伙伴,可以在maven文件添加如下插件,并打包和解压文件查看项目里的MANIFEST.MF文件

 1<plugin>
2   <groupId>org.apache.maven.plugins</groupId>
3   <artifactId>maven-jar-plugin</artifactId>
4   <version>3.0.2</version>
5   <configuration>
6      <archive>
7         <manifest>
8            <addClasspath>true</addClasspath>
9            <classpathPrefix>lib/</classpathPrefix>
10            <mainClass>com.wang.springboottest.SpringBootTestApplication</mainClass>
11         </manifest>
12      </archive>
13   </configuration>
14</plugin>

项目里怎么没有web.xml文件

还记得我们Servlet 3.0以前的版本都是含有web.xml 配置上下文信息,过滤器、监听器啥的信息是吧,但是当我们接触springBoot后,神奇的发现项目内并没有此文件,那么springboot是如何启动servlet并且如何配置过滤器、监听器呢

经过以上三个新特性,我们就可以抛弃web.xml文件了,具体在SpringBoot如何使用,在这里卖个关子,稍后源码一见分晓

有哪些方式修改绑定的端口号

大家都知道SpringBoot项目创建的时候有个配置文件,里面可以配置各种属性,包括绑定端口号,那么除了配置文件还知道其他方式吗。并且为何在配置文件写内容,数据就会被同步到内嵌Server的中呢
先如下举例下绑定方式

  • (-. = 但是本人测试失败无法使用server.port的环境变量)

内嵌Servlet如何启动

什么是内嵌servlet

先举例正常项目启动的操作流程
1、
2、
3、
4、
但SpringBoot不需要我们自己下载tomcat,Spring已经在我们项目中的导入必要的第三方jar包,如下

1  <dependency>
2            <groupId>org.springframework.boot</groupId>
3            <artifactId>spring-boot-starter-web</artifactId>
4        </dependency>

包含

1        <dependency>
2      <groupId>org.springframework.boot</groupId>
3      <artifactId>spring-boot-starter-tomcat</artifactId>
4      <version>2.2.5.RELEASE</version>
5      <scope>compile</scope>
6    </dependency>

此jar包包含各种tomcat核心jar包

启动图我们可以清晰看到tomcat的打印日志

当然springBoot并非只支持Tomcat内嵌,我们可以替换pom文件替换内嵌servlet
查看官方文档 我们可以看到

1Spring Boot includes support for embedded Tomcat, Jetty, and Undertow servers. Most developers use the appropriate “Starter” to obtain a fully configured instance. By default, the embedded server listens for HTTP requests on port 8080.

当然替换很简单
如下

 1  <dependency>
2            <groupId>org.springframework.boot</groupId>
3            <artifactId>spring-boot-starter-web</artifactId>
4            <exclusions>
5                <exclusion>
6                    <groupId>org.springframework.boot</groupId>
7                    <artifactId>spring-boot-starter-tomcat</artifactId>
8                </exclusion>
9            </exclusions>
10        </dependency>
11
12        <dependency>
13            <groupId>org.springframework.boot</groupId>
14            <artifactId>spring-boot-starter-jetty</artifactId>
15        </dependency>

相当于Spring在启动main文件后,会直接启动内嵌的jetty,以供服务于servlet容器

照例、直接上图:

可以清晰的看到

接下来就是详细而无聊的源码讲解,到底springBoot是如何启动内嵌的容器(一下栗子采取的都是内嵌Tomcat服务器)

  • 首先我们查看启动类的的run方法,会先创建应用上下文
 1  context = createApplicationContext();
2
3  Class<?> contextClass = this.applicationContextClass;
4        if (contextClass == null) {
5            try {
6                switch (this.webApplicationType) {
7                // 我们项目包含了webjar包,属于servlet项目,并生成了AnnotationConfigServletWebServerApplicationContext对象
8                case SERVLET:
9                    contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
10                    break;
11                case REACTIVE:
12                    contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
13                    break;
14                default:
15                    contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
16                }
17            }
18            catch (ClassNotFoundException ex) {
19                throw new IllegalStateException(
20                        "Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);
21            }
22        }
23        return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
  • 接下来就是我们的重头戏,他会在刷新上下文的时候创建server,如下
 1  @Override
2    protected void onRefresh() {
3        super.onRefresh();
4        try {
5          // 创建tomcat服务器 初始化servlet
6            createWebServer();
7        }
8        catch (Throwable ex) {
9            throw new ApplicationContextException("Unable to start web server", ex);
10        }
11    }
  • 进入createWebServer
 1  private void createWebServer() {
2    // 还未初始化Server 所以以下2项都为空
3        WebServer webServer = this.webServer;
4        ServletContext servletContext = getServletContext();
5        if (webServer == null && servletContext == null) {
6      // 不是重点,稍微介绍下
7      // 获取当前项目的web服务工厂,比如web默认的TomcatWeb服务 或者替换的jetty、undertow
8      // 并且注册为Spring的bean
9            ServletWebServerFactory factory = getWebServerFactory();
10      // 继续深入分析,这里是我们的重点,也是内嵌Tomcat的重点
11            this.webServer = factory.getWebServer(getSelfInitializer());
12        }
13        else if (servletContext != null) {
14            try {
15                getSelfInitializer().onStartup(servletContext);
16            }
17            catch (ServletException ex) {
18                throw new ApplicationContextException("Cannot initialize servlet context", ex);
19            }
20        }
21        initPropertySources();
22    }
  • 继续深入getWebServer
 1    @Override
2    public WebServer getWebServer(ServletContextInitializer... initializers) {
3        if (this.disableMBeanRegistry) {
4            Registry.disableRegistry();
5        }
6        Tomcat tomcat = new Tomcat();
7        File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
8        tomcat.setBaseDir(baseDir.getAbsolutePath());
9        Connector connector = new Connector(this.protocol);
10        connector.setThrowOnFailure(true);
11        tomcat.getService().addConnector(connector);
12        customizeConnector(connector);
13        tomcat.setConnector(connector);
14        tomcat.getHost().setAutoDeploy(false);
15        configureEngine(tomcat.getEngine());
16        for (Connector additionalConnector : this.additionalTomcatConnectors) {
17            tomcat.getService().addConnector(additionalConnector);
18        }
19        prepareContext(tomcat.getHost(), initializers);
20        return getTomcatWebServer(tomcat);
21    }

此处因与本文联系不大,后续章节会出一版<<深入了解SpringBoot的Tomcat详解>>

现粗略讲下大致流程

相信大家都熟悉了这张图,在此再贴一次 毕竟Tomcat就像咱的女朋友不仅每天使用,百看不厌嘛

Tomcat架构图
Tomcat架构图
  • 可能大家对Tomcat不太熟悉的人对这种跳跃性难以接受(其实是我自己也看不下去了,但作为第一次写博,希望大家稍微谅解下哈),在此给大家画个时序图&贴栈信息的情况供大家粗略了解下
  • 栈信息如下:
  • 配合时序图观看效果更佳哦
  • 咱继续深入初始化Servlet容器初始化
 1private void selfInitialize(ServletContext servletContext) throws ServletException {
2   // spring 上下文 设置 servletContext
3   prepareWebApplicationContext(servletContext);
4   // 注册应用上下文属性的servlet包装类
5   registerApplicationScope(servletContext);
6   // 注册servlet各属性到Spring容器的单例bean(包括:servletContext,servletConfig,contextAttributes ...)
7   WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);
8  // 重点来咯,getServletContextInitializerBeans的时候会初始化ServletContextInitializerBeans对象
9  // 这个对象做了许多bean初始化的操作,待我后续慢慢讲来
10   for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
11             // 调用start方法     配置具体的servlet filter listener等
12      beans.onStartup(servletContext);
13   }
14}
  • - 初始化Servlet上下文Bean对象

     1@SafeVarargs
    2  public ServletContextInitializerBeans(ListableBeanFactory beanFactory,
    3          Class<? extends ServletContextInitializer>... initializerTypes)
     
    {
    4      // 初始化数组
    5      this.initializers = new LinkedMultiValueMap<>();
    6      // a 
    7      this.initializerTypes = (initializerTypes.length != 0) ? Arrays.asList(initializerTypes)
    8              : Collections.singletonList(ServletContextInitializer.class);
    9      // b 
    10      addServletContextInitializerBeans(beanFactory);
    11      // c
    12      addAdaptableBeans(beanFactory);
    13      List<ServletContextInitializer> sortedInitializers = this.initializers.values().stream()
    14              .flatMap((value) -> value.stream().sorted(AnnotationAwareOrderComparator.INSTANCE))
    15              .collect(Collectors.toList());
    16      this.sortedList = Collections.unmodifiableList(sortedInitializers);
    17      logMappings(this.initializers);
    18  }
  1. a: 因参数initializerTypes传过来为null,所以会新增ServletContextInitializer,大家还记得上面讲过的消失的web.xml吗,是的,我们开始收尾呼应了有木有,(老铁们,双击666,点个关注不迷路)

  2. b:重点 初始化dispatcherServlet到spring上下文的容器内

    1. 1private void addServletContextInitializerBeans(ListableBeanFactory beanFactory) {
      2        for (Class<? extends ServletContextInitializer> initializerType : this.initializerTypes) {
      3            for (Entry<String, ? extends ServletContextInitializer> initializerBean : getOrderedBeansOfType(beanFactory,
      4                    initializerType)) {
      5                addServletContextInitializerBean(initializerBean.getKey(), initializerBean.getValue(), beanFactory);
      6            }
      7        }
      8    }
    2. 这时候就会有同学提问了,为什么这里加载的就是dispatcherServlet呢,ServletContextInitializer有很多的子类呀,不知大家是否还记得自动配置原理(这里先不讲述,如果懂的人不是很多,可以下一期讲解下)我们在spring-boot-autoconfigure的jar包下的spring.factories看到org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration这行代码,点进发现这是一个自动配置类,并且我们的项目已经符合了他的condition(如下),所以在项目启动初期的时候该类下的Bean,包括(DispatcherServlet、MultipartResolver、DispatcherServletRegistrationBean)会被beanFactory扫描到,加载class到bean工厂内

    3. 1@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
      2@Configuration(proxyBeanMethods = false)
      3@ConditionalOnWebApplication(type = Type.SERVLET)
      4@ConditionalOnClass(DispatcherServlet.class)
      5@AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class)
      6public class DispatcherServletAutoConfiguration
      7

    4. 至此,我们的servlet已经初始化成功

  3. c:重点,注册上传文件配置、servlet注册器、filter注册器、listener注册器到spring容器

    1.  1protected void addAdaptableBeans(ListableBeanFactory beanFactory) {
      2    // A
      3        MultipartConfigElement multipartConfig = getMultipartConfig(beanFactory);
      4      // B
      5        addAsRegistrationBean(beanFactory, Servlet.class, new ServletRegistrationBeanAdapter(multipartConfig));
      6    // C
      7        addAsRegistrationBean(beanFactory, Filter.class, new FilterRegistrationBeanAdapter());
      8        for (Class<?> listenerType : ServletListenerRegistrationBean.getSupportedTypes()) {
      9      // D
      10            addAsRegistrationBean(beanFactory, EventListener.class, (Class<EventListener>) listenerType,
      11                    new ServletListenerRegistrationBeanAdapter());
      12        }
      13    }
    2. A:上传文件配置bean的注册

    3. B:注册spring.factories包含的servlet(除dispatcherServlet)

    4. C:注册spring.factories包含的Filter到spring的容器内

    5. D:注册spring.factories包含的listener到spring容器内

总结

SpringBoot的内嵌容器展现了其灵活的可扩展性、并且灵活运用了其自动装配和IOC容器等,我们可以在其项目启动期间做一些我们个人的操作等。

当然文章中还有很多的遗漏点,其实那些大家抽空的时间稍晚看下源码也能猜到大概的意思,期间还是离不开我们老生常谈的了Spring的ApplicationContext和BeanFactory

唠叨

工作三年有余,思来想去,在工作之时并没有记录那些映象深刻的问题、以及Bug,但多在平时工作中经常遇到,致遗忘的比较快,特此以写文的方式,与大家一起探讨、检索工作中遇到的难解的问题。

如遇文章有错误的点,请您动动小手,帮忙指点一二,让我们一同成长,一起成为面试中的霸者。offer拿到堆不下,数钱数到手抽筋

参考资料:https://www.cnkirito.moe/servlet-explore/ (寻找遗失的web.xml)

​ 小马哥的Spring Boot编程思想