Spring Boot之EmbeddedTomcat篇

in Spring BootJava with 1 comment

在上文中我们介绍了Spring Boot的一些基本概要以及我在这个系列中会讲到的一些东西,正如上文所述,这篇会讲解EmbeddedTomcat的原理。

传统Tomcat

写过Java的同学应该知道,在Spring Boot之前,我们一般会使用Tomcatweb容器来管理我们的web项目,类似于nginx,但又有所区别。
虽然nginxtomcat最终所做的事情大致一样,但是我们给nginx的定义是web服务器,而tomcatweb容器tomcat是实现了servlet接口的开源项目,把我们需要自定义servlet的基础架构给抽离出来。

内嵌Tomcat

那么内嵌Tomcat有是什么呢?
内嵌Tomcat可以让我们把Tomcat内嵌于我们的项目中,让我们的项目更方便部署、方便调试。

实现原理

正如我在上文所说,我们会以「逆向」的方式来帮助大家快速了解其原理。在Spring Boot中,给我们提供了配置诸如server.port的能力。我们不妨先修改server.port试试,看看最终tomcat是否以另外一个端口执行:

server.port=8081

果然,执行端口更改了:

2019-04-01 21:08:57.644  INFO 28714 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8081 (http) with context path ''

我们不妨点开server.port,发现跳转到了ServerProperties:

public Integer getPort() {
    return this.port;
}

public void setPort(Integer port) {
    this.port = port;
}

既然我们是通过setPort来设置端口,那么就意味着在调用的时候是从getPort来获取端口。我们不妨点击查看getPort这个方法被谁调用了:

1554124402808.jpg

ReactiveWebServerFactoryCustomizer先不管,我们先看看ServletWebServerFactoryCustomizer:

    @Override
    public void customize(ConfigurableServletWebServerFactory factory) {
        PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
        map.from(this.serverProperties::getPort).to(factory::setPort);
        map.from(this.serverProperties::getAddress).to(factory::setAddress);
        map.from(this.serverProperties.getServlet()::getContextPath)
                .to(factory::setContextPath);
        map.from(this.serverProperties.getServlet()::getApplicationDisplayName)
                .to(factory::setDisplayName);
        map.from(this.serverProperties.getServlet()::getSession).to(factory::setSession);
        map.from(this.serverProperties::getSsl).to(factory::setSsl);
        map.from(this.serverProperties.getServlet()::getJsp).to(factory::setJsp);
        map.from(this.serverProperties::getCompression).to(factory::setCompression);
        map.from(this.serverProperties::getHttp2).to(factory::setHttp2);
        map.from(this.serverProperties::getServerHeader).to(factory::setServerHeader);
        map.from(this.serverProperties.getServlet()::getContextParameters)
                .to(factory::setInitParameters);
    }

我们可以看到某个地方调度了customize来注入ConfigurableServletWebServerFactory的一个实现,谁调用了customize我们可以先放在一边,我们先看看ConfigurableServletWebServerFactory有哪些实现:

1554124581680.jpg

我们看到了非常熟悉的几个类,因为我们已经通过console得知了最终执行的是tomcat的结论,那么我们现在需要证明的是为什么调用了TomcatServletWebServerFactory

1554124706827.jpg

我们可以看到,调用的地方很多,在查看之前,我们不妨先思考一下,假如你是设计者,有tomcatjetty等需要根据用户的自定义来调度,那么你去设计这个类,是不是至少在名字上不应该跟tomcatjetty有关?因此,我们不妨先去ServletWebServerFactoryConfiguration去验证一下我们的想法是否正确。


@Configuration
class ServletWebServerFactoryConfiguration {

    @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();
        }

    }

    /**
     * Nested configuration if Jetty is being used.
     */
    @Configuration
    @ConditionalOnClass({ Servlet.class, Server.class, Loader.class,
            WebAppContext.class })
    @ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
    public static class EmbeddedJetty {

        @Bean
        public JettyServletWebServerFactory JettyServletWebServerFactory() {
            return new JettyServletWebServerFactory();
        }

    }

    /**
     * Nested configuration if Undertow is being used.
     */
    @Configuration
    @ConditionalOnClass({ Servlet.class, Undertow.class, SslClientAuthMode.class })
    @ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
    public static class EmbeddedUndertow {

        @Bean
        public UndertowServletWebServerFactory undertowServletWebServerFactory() {
            return new UndertowServletWebServerFactory();
        }

    }

}

我们发现其中有一个注解ConditionalOnClass(原理在自动装配篇会讲),故名知意,就是说在存在这个类的时候才会执行,同时,我们可以看到最后会注册成一个bean

当注册TomcatServletWebServerFactory之后,我们似乎没有了头绪,我们不妨可以思考一下,不管最终注册了哪个bean,最终肯定是通过某个接口来完成这个bean的调度,因此,一方面我们可以查看这个类实现了哪个接口,同时也可以看这个类里面会有接口方法的重写

通过上述理论,我们不难发现TomcatServletWebServerFactory实现了ServletWebServerFactory,并重写了getWebServer方法。

@Override
    public WebServer getWebServer(ServletContextInitializer... initializers) {
        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);
        }
        prepareContext(tomcat.getHost(), initializers);
        return getTomcatWebServer(tomcat);
    }

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

随之,我们不难查到ServletWebServerApplicationContext调度了getWebServer方法:

private void createWebServer() {
        WebServer webServer = this.webServer;
        ServletContext servletContext = getServletContext();
        if (webServer == null && servletContext == null) {
            ServletWebServerFactory factory = getWebServerFactory();
            this.webServer = factory.getWebServer(getSelfInitializer());
        }
        else if (servletContext != null) {
            try {
                getSelfInitializer().onStartup(servletContext);
            }
            catch (ServletException ex) {
                throw new ApplicationContextException("Cannot initialize servlet context",
                        ex);
            }
        }
        initPropertySources();
    }

//通过ServletWebServerFactory获取bean,也就是TomcatServletWebServerFactory
protected ServletWebServerFactory getWebServerFactory() {
        // Use bean names so that we don't consider the hierarchy
        String[] beanNames = getBeanFactory()
                .getBeanNamesForType(ServletWebServerFactory.class);
        if (beanNames.length == 0) {
            throw new ApplicationContextException(
                    "Unable to start ServletWebServerApplicationContext due to missing "
                            + "ServletWebServerFactory bean.");
        }
        if (beanNames.length > 1) {
            throw new ApplicationContextException(
                    "Unable to start ServletWebServerApplicationContext due to multiple "
                            + "ServletWebServerFactory beans : "
                            + StringUtils.arrayToCommaDelimitedString(beanNames));
        }
        return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
    }

当我们获取TomcatServletWebServerFactory之后,会调度getWebServer方法,最后会返回一个TomcatWebServer

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 {
        logger.info("Tomcat initialized with port(s): " + getPortsDescription(false));
        synchronized (this.monitor) {
            try {
                addInstanceIdToEngineName();

                Context context = findContext();
                context.addLifecycleListener((event) -> {
                    if (context.equals(event.getSource())
                            && Lifecycle.START_EVENT.equals(event.getType())) {
                        // Remove service connectors so that protocol binding doesn't
                        // happen when the service is started.
                        removeServiceConnectors();
                    }
                });

                // Start the server to trigger initialization listeners
                this.tomcat.start();

                // We can re-throw failure exception directly in the main thread
                rethrowDeferredStartupExceptions();

                try {
                    ContextBindings.bindClassLoader(context, context.getNamingToken(),
                            getClass().getClassLoader());
                }
                catch (NamingException ex) {
                    // Naming is not enabled. Continue
                }

                // Unlike Jetty, all Tomcat threads are daemon threads. We create a
                // blocking non-daemon to stop immediate shutdown
                startDaemonAwaitThread();
            }
            catch (Exception ex) {
                stopSilently();
                throw new WebServerException("Unable to start embedded Tomcat", ex);
            }
        }
    }

看到这里,我们就大概有一个比较清晰的思路了,我们看到TomcatWebServer有一个initialize方法,在构造的时候执行,输出的内容与我们在console看到的一致:

1554132190180.jpg

然后我们通过ServletWebServerApplicationContextcreateWebServer方法一层一层的往上追溯,发现是SpringApplicationrefresh方法调度了ServletWebServerApplicationContextonRefresh方法,而SpringApplicationrefresh方法又是通过run方法来调度的:

public ConfigurableApplicationContext run(String... args) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        ConfigurableApplicationContext context = null;
        Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
        configureHeadlessProperty();
        SpringApplicationRunListeners listeners = getRunListeners(args);
        listeners.starting();
        try {
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(
                    args);
            ConfigurableEnvironment environment = prepareEnvironment(listeners,
                    applicationArguments);
            configureIgnoreBeanInfo(environment);
            Banner printedBanner = printBanner(environment);
            context = createApplicationContext();
            exceptionReporters = getSpringFactoriesInstances(
                    SpringBootExceptionReporter.class,
                    new Class[] { ConfigurableApplicationContext.class }, context);
            prepareContext(context, environment, listeners, applicationArguments,
                    printedBanner);
            refreshContext(context);
            afterRefresh(context, applicationArguments);
            stopWatch.stop();
            if (this.logStartupInfo) {
                new StartupInfoLogger(this.mainApplicationClass)
                        .logStarted(getApplicationLog(), stopWatch);
            }
            listeners.started(context);
            callRunners(context, applicationArguments);
        }
        catch (Throwable ex) {
            handleRunFailure(context, ex, exceptionReporters, listeners);
            throw new IllegalStateException(ex);
        }

        try {
            listeners.running(context);
        }
        catch (Throwable ex) {
            handleRunFailure(context, ex, exceptionReporters, null);
            throw new IllegalStateException(ex);
        }
        return context;
    }

最后,就到了我们Spring Boot的入口:

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}

那么问题就来了,为什么SpringApplication能指定到ServletWebServerApplicationContext这个上下文?

protected ConfigurableApplicationContext createApplicationContext() {
        Class<?> contextClass = this.applicationContextClass;
        if (contextClass == null) {
            try {
                switch (this.webApplicationType) {
                case SERVLET:
                    contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
                    break;
                case REACTIVE:
                    contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
                    break;
                default:
                    contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
                }
            }
            catch (ClassNotFoundException ex) {
                throw new IllegalStateException(
                        "Unable create a default ApplicationContext, "
                                + "please specify an ApplicationContextClass",
                        ex);
            }
        }
        return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
    }

原因就在于创建的时候会指定我们的上下文。

至此,我们还有一个问题没有解决,也就是我们上面说到的,谁调度了ServletWebServerFactoryCustomizercustomize方法?

我们往上追溯,发现是WebServerFactoryCustomizerBeanPostProcessorpostProcessBeforeInitialization方法调度了,而这个类实现了BeanPostProcessorpostProcessBeforeInitialization方法就好比一个hook,当你在注册bean的时候会执行。

@Override
    public Object postProcessBeforeInitialization(Object bean, String beanName)
            throws BeansException {
        if (bean instanceof WebServerFactory) {
            postProcessBeforeInitialization((WebServerFactory) bean);
        }
        return bean;
    }

至此,我们的内嵌tomcat的执行流程原理就完成了。

总结

我们来做一个小结,大致的运行过程就是SpringApplication执行run方法,他会根据指定的context,然后会调度ServletWebServerApplicationContextrefresh方法,然后通过自己的父类调度onRefresh方法去创建我们的webserver,在创建的过程中,会执行getWebServerFactory方法,来根据ServletWebServerFactory返回我们自定义的bean,比如默认的就是TomcatServletWebServerFactory,最后通过这个类去返回一个TomcatWebServer的类,来完成我们内嵌webserver的调度。

Responses
  1. 小手一抖,回复到手,手提酱油,低头猛走。

    Reply