专注于 JetBrains IDEA 全家桶,永久激活,教程
持续更新 PyCharm,IDEA,WebStorm,PhpStorm,DataGrip,RubyMine,CLion,AppCode 永久激活教程

SpringBoot源码解析-内嵌Tomcat容器的启动

tomcat使用简单示范

简单回顾下内嵌tomcat使用,新建一个maven项目,导入如下依赖

<dependencies>
    <dependency>
      <groupId>javax.annotation</groupId>
      <artifactId>javax.annotation-api</artifactId>
      <version>1.3.2</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.apache.tomcat.embed</groupId>
      <artifactId>tomcat-embed-core</artifactId>
      <version>9.0.12</version>
      <scope>compile</scope>
      <exclusions>
        <exclusion>
          <artifactId>tomcat-annotations-api</artifactId>
          <groupId>org.apache.tomcat</groupId>
        </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <groupId>org.apache.tomcat.embed</groupId>
      <artifactId>tomcat-embed-el</artifactId>
      <version>9.0.12</version>
      <scope>compile</scope>
    </dependency>
  </dependencies>

新建一个servlet类,实现对应的方法。

public class HomeServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("request scheme: " + req.getScheme());
        resp.getWriter().print("hello tomcat");
    }

}

在main函数中添加如下代码

    public static void main(String[] args) throws Exception {
        Tomcat tomcat = new Tomcat();
        //设置路径
        tomcat.setBaseDir("d:tomcat/dir");
        tomcat.getHost().setAutoDeploy(false);

        Connector connector = new Connector();
        //设置端口
        connector.setPort(10086);
        tomcat.getService().addConnector(connector);

        Context context = new StandardContext();
        //设置context路径
        context.setPath("");
        context.addLifecycleListener(new Tomcat.FixContextListener());
        tomcat.getHost().addChild(context);

        //添加servlet
        tomcat.addServlet("", "homeServlet", new HomeServlet());
        //设置servlet路径
        context.addServletMappingDecoded("/", "homeServlet");

        tomcat.start();
        tomcat.getServer().await();
    }

这样的话一个简单的tomcat服务器就启动了,打开浏览器输入localhost:10086,就可以看到servlet中的返回值。

springboot中tomcat容器的启动

还记得前两节讲到springboot自动化配置里面的配置文件么,配置文件中有一个类,org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration ,进入这个类。

@Configuration
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@ConditionalOnClass(ServletRequest.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(ServerProperties.class)
@Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
        ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,
        ServletWebServerFactoryConfiguration.EmbeddedJetty.class,
        ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })
public class ServletWebServerFactoryAutoConfiguration {

发现上面有一个import注解,进入import注解导入的类

    @Configuration
    @ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
    @ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
    public static class EmbeddedTomcat {

        @Bean
        public TomcatServletWebServerFactory tomcatServletWebServerFactory() {
            return new TomcatServletWebServerFactory();
        }

    }

根据上一节学习的判断条件可以知道,import注解向spring容器中注入了一个TomcatServletWebServerFactory类,这个类我们先标记着。

回到main函数中,顺着SpringApplication.run(Application.class, args);方法进入AbstractApplicationContext的refresh方法

    // Initialize other special beans in specific context subclasses.
    onRefresh();

在onRefresh方法上发现一行注释,在子类方法中初始化特殊的bean。tomcat容器应该算是一个特殊的bean了,所以我们进入子类的onRefresh方法。在子类ServletWebServerApplicationContext发现了这样的代码。

    @Override
    protected void onRefresh() {
        super.onRefresh();
        try {
            createWebServer();
        }
        catch (Throwable ex) {
            throw new ApplicationContextException("Unable to start web server", ex);
        }
    }

猜也能猜到,createWebServer方法就是tomcat初始化的地方了。所以进入方法一探究竟。

private void createWebServer() {
        WebServer webServer = this.webServer;
        ServletContext servletContext = getServletContext();
        //初始化进来,webServer和servletContext两个对象都是null,所以进入if
        if (webServer == null && servletContext == null) {
            ServletWebServerFactory factory = getWebServerFactory();
            this.webServer = factory.getWebServer(getSelfInitializer());
        }
        else if (servletContext != null) {
            ...
        }
        initPropertySources();
    }

首先看一下getWebServerFactory方法。

    protected ServletWebServerFactory getWebServerFactory() {
        // Use bean names so that we don't consider the hierarchy
        String[] beanNames = getBeanFactory()
                .getBeanNamesForType(ServletWebServerFactory.class);
        if (beanNames.length == 0) {
            ...
        }
        if (beanNames.length > 1) {
            ...
        }
        return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
    }

方法逻辑比较简单,获取容器中ServletWebServerFactory类型的实例,并校验其数量,多了或者少了都不行,必须是正好1个。这个时候看一下上面通过自动化配置那边导入spring容器的TomcatServletWebServerFactory类,这个类就是ServletWebServerFactory的子类。所以在没有其他配置的情况下,getWebServerFactory方法,获取到的就是TomcatServletWebServerFactory类。

获取到factory实例后,就来看一下factory的getWebServer方法。

        Tomcat tomcat = new Tomcat();
        File baseDir = (this.baseDirectory != null) ? this.baseDirectory
                : createTempDir("tomcat");
        tomcat.setBaseDir(baseDir.getAbsolutePath());
        //设置端口
        Connector connector = new Connector(this.protocol);
        tomcat.getService().addConnector(connector);
        //配置连接
        customizeConnector(connector);
        tomcat.setConnector(connector);
        tomcat.getHost().setAutoDeploy(false);
        configureEngine(tomcat.getEngine());
        for (Connector additionalConnector : this.additionalTomcatConnectors) {
            tomcat.getService().addConnector(additionalConnector);
        }
        配置context
        prepareContext(tomcat.getHost(), initializers);
        return getTomcatWebServer(tomcat);

虽然比我们一开始那个示范要复杂许多,但是大致的逻辑还是很清晰的,不难看懂。(这个地方如果不理解的话,你需要补充一下tomcat的知识)

进入getTomcatWebServer方法。

protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
        return new TomcatWebServer(tomcat, getPort() >= 0);
    }

    public TomcatWebServer(Tomcat tomcat, boolean autoStart) {
        Assert.notNull(tomcat, "Tomcat Server must not be null");
        this.tomcat = tomcat;
        this.autoStart = autoStart;
        initialize();
    }

    private void initialize() throws WebServerException {
        synchronized (this.monitor) {
            try {
                addInstanceIdToEngineName();

                Context context = findContext();
                context.addLifecycleListener((event) -> {
                    if (context.equals(event.getSource())
                            && Lifecycle.START_EVENT.equals(event.getType())) {
                        removeServiceConnectors();
                    }
                });
                this.tomcat.start();
                ...
                startDaemonAwaitThread();
            }
            ...
        }
    }

在getTomcatWebServer方法中,发现了tomcat启动相关的代码,所以这个地方就是tomcat容器启动的地方啦。不过如果你用debug的话,你会发现这个地方即使tomcat启动过后,依然无法访问。因为在启动前spring框架还做了一件事。

                Context context = findContext();
                context.addLifecycleListener((event) -> {
                    if (context.equals(event.getSource())
                            && Lifecycle.START_EVENT.equals(event.getType())) {
                        //移除tomcat容器的连接器connector
                        removeServiceConnectors();
                    }
                });

因为这个时候作为一个特殊的bean,tomcat容器需要优先初始化,但是此时其他bean还没有初始化完成,连接进来后是无法处理的。所以spring框架在这个地方移除了连接器。

那么被移除的连接器在那个地方启动的呢?在AbstractApplicationContext的refresh方法中,onRefresh方法后面还有一个方法finishRefresh方法。进入子类的这个方法(进入这个方法之前,所有的非lazy属性的bean已经全部完成了初始化)

    @Override
    protected void finishRefresh() {
        super.finishRefresh();
        WebServer webServer = startWebServer();
        if (webServer != null) {
            publishEvent(new ServletWebServerInitializedEvent(webServer, this));
        }
    }

    private WebServer startWebServer() {
        WebServer webServer = this.webServer;
        if (webServer != null) {
            webServer.start();
        }
        return webServer;
    }

    public void start() throws WebServerException {
        ...
                addPreviouslyRemovedConnectors();
                Connector connector = this.tomcat.getConnector();
                if (connector != null && this.autoStart) {
                    performDeferredLoadOnStartup();
                }
                ...
        }
    }

在这个方法中,我们找到了被移除的connector。spring框架将刚刚移除得到连接器又放到tomcat容器中,并且启用了他,这样的话tomcat就可以被访问到了。

tomcat的启动到这儿我们已经了解了,不知道大家有没有发现一个问题,就是我们并没有看到类似示例中添加servlet和设置servlet路径相关的代码。那这部分代码在哪里呢?

回到刚刚factory的getWebServer方法。这个方法中传入了一个参数getSelfInitializer()我们看一下这个参数是啥。

    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);
        for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
            beans.onStartup(servletContext);
        }
    }

这个lambda表达式应该还很好理解吧,返回了一个ServletContextInitializer实例,该实例的onStartup方法就是调用了这边的selfInitialize方法。这个selfInitialize方法里,最关键的就是getServletContextInitializerBeans方法了。但是我们从这边分析代码的话,其实不太看得出来getServletContextInitializerBeans到底获取到了那些类,所以可以取巧一下,使用IDEA的debug功能。借助debug我们看到了这边获取到的几个类,关键的是DispatcherServletRegistrationBean。也就是这个地方会调用DispatcherServletRegistrationBean的onStartup方法。

那么他的onStartup到底干了那些事呢?

    @Override
    public final void onStartup(ServletContext servletContext) throws ServletException {
        String description = getDescription();
        if (!isEnabled()) {
            logger.info(StringUtils.capitalize(description)
                    + " was not registered (disabled)");
            return;
        }
        register(description, servletContext);
    }

    @Override
    protected final void register(String description, ServletContext servletContext) {
        D registration = addRegistration(description, servletContext);
        if (registration == null) {
            logger.info(StringUtils.capitalize(description) + " was not registered "
                    + "(possibly already registered?)");
            return;
        }
        configure(registration);
    }

    @Override
    protected ServletRegistration.Dynamic addRegistration(String description,
            ServletContext servletContext) {
        String name = getServletName();
        //这个地方将servlet添加进了context
        return servletContext.addServlet(name, this.servlet);
    }

    @Override
    protected void configure(ServletRegistration.Dynamic registration) {
        super.configure(registration);
        String[] urlMapping = StringUtils.toStringArray(this.urlMappings);
        if (urlMapping.length == 0 && this.alwaysMapUrl) {
            urlMapping = DEFAULT_MAPPINGS;
        }
        if (!ObjectUtils.isEmpty(urlMapping)) {
        //这个方法则对servlet的路径进行了配置
            registration.addMapping(urlMapping);
        }
        registration.setLoadOnStartup(this.loadOnStartup);
        if (this.multipartConfig != null) {
            registration.setMultipartConfig(this.multipartConfig);
        }
    }

既然知道了ServletContextInitializer的作用,那么我们就追踪一下这个ServletContextInitializer被放置到了什么地方,何时调用他的方法。

    @Override
    public WebServer getWebServer(ServletContextInitializer... initializers) {
        ...
        prepareContext(tomcat.getHost(), initializers);
        return getTomcatWebServer(tomcat);
    }

    protected void prepareContext(Host host, ServletContextInitializer[] initializers) {
        ...
        ServletContextInitializer[] initializersToUse = mergeInitializers(initializers);
        ...
        configureContext(context, initializersToUse);
        ...
    }

    protected void configureContext(Context context,
            ServletContextInitializer[] initializers) {
        TomcatStarter starter = new TomcatStarter(initializers);
        if (context instanceof TomcatEmbeddedContext) {
            TomcatEmbeddedContext embeddedContext = (TomcatEmbeddedContext) context;
            embeddedContext.setStarter(starter);
            embeddedContext.setFailCtxIfServletStartFails(true);
        }
        context.addServletContainerInitializer(starter, NO_CLASSES);
        ...
    }

可以看到ServletContextInitializer被包装成了一个TomcatStarter放入了context中。在context的start方法里,我们就可以看到initializers的启动(这个地方涉及到tomcat容器的启动,如果不熟悉的话可以回顾下)。

    @Override
    protected synchronized void startInternal() throws LifecycleException {
            ...
            // Call ServletContainerInitializers
            for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry :
                initializers.entrySet()) {
                try {
                    entry.getKey().onStartup(entry.getValue(),
                            getServletContext());
                } catch (ServletException e) {
                    log.error(sm.getString("standardContext.sciFail"), e);
                    ok = false;
                    break;
                }
            }
            ...
    }

总结

经过这几轮的分析,从SpringApplication的启动,到自动化配置,再到今天的tomcat容器的启动。我们已经窥探到了整个springboot框架的全貌。所以后面就需要对常用功能定点学习了。


返回目录

文章永久链接:https://tech.souyunku.com/19187

未经允许不得转载:搜云库技术团队 » SpringBoot源码解析-内嵌Tomcat容器的启动

JetBrains 全家桶,激活、破解、教程

提供 JetBrains 全家桶激活码、注册码、破解补丁下载及详细激活教程,支持 IntelliJ IDEA、PyCharm、WebStorm 等工具的永久激活。无论是破解教程,还是最新激活码,均可免费获得,帮助开发者解决常见激活问题,确保轻松破解并快速使用 JetBrains 软件。获取免费的破解补丁和激活码,快速解决激活难题,全面覆盖 2024/2025 版本!

联系我们联系我们