Spring Boot啓動過程(四):Spring Boot內嵌Tomcat啓動

  以前在Spring Boot啓動過程(二)提到過createEmbeddedServletContainer建立了內嵌的Servlet容器,我用的是默認的Tomcat。html

    private void createEmbeddedServletContainer() {
        EmbeddedServletContainer localContainer = this.embeddedServletContainer;
        ServletContext localServletContext = getServletContext();
        if (localContainer == null && localServletContext == null) {
            EmbeddedServletContainerFactory containerFactory = getEmbeddedServletContainerFactory();
            this.embeddedServletContainer = containerFactory
                    .getEmbeddedServletContainer(getSelfInitializer());
        }
        else if (localServletContext != null) {
            try {
                getSelfInitializer().onStartup(localServletContext);
            }
            catch (ServletException ex) {
                throw new ApplicationContextException("Cannot initialize servlet context",
                        ex);
            }
        }
        initPropertySources();
    }

  getEmbeddedServletContainerFactory方法中調用了ServerProperties,從ServerProperties的實例方法customize能夠看出Springboot支持三種內嵌容器的定製化配置:Tomcat、Jetty、Undertow。git

  這裏直接說TomcatEmbeddedServletContainerFactory的getEmbeddedServletContainer方法了,緣由在前面那篇裏說過了。不過首先是getSelfInitializer方法先執行的:github

    private org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer() {
        return new ServletContextInitializer() {
            @Override
            public void onStartup(ServletContext servletContext) throws ServletException {
                selfInitialize(servletContext);
            }
        };
    }

  將初始化的ServletContextInitializer傳給了getEmbeddedServletContainer方法。進入了getEmbeddedServletContainer方法直接就是實例化了一個Tomcat:web

        Tomcat tomcat = new Tomcat();

   而後生成一個臨時目錄,並tomcat.setBaseDir,setBaseDir方法的註釋說Tomcat須要一個目錄用於臨時文件而且它應該是第一個被調用的方法;若是方法沒有被調用會使用默認的幾個位置system properties - catalina.base, catalina.home - $PWD/tomcat.$PORT,另外/tmp從安全角度來講不建議。spring

  接着:apache

        Connector connector = new Connector(this.protocol);

  建立Connector過程當中,靜態代碼塊:單獨抽出來寫了。RECYCLE_FACADES屬性能夠經過啓動參數JAVA_OPTS來配置: -Dorg.apache.catalina.connector.RECYCLE_FACADES=,默認是false,配置成true能夠提升安全性但同時性能會有些損耗,參考:http://tomcat.apache.org/tomcat-7.0-doc/config/systemprops.html和http://bztax.gov.cn/docs/security-howto.html。其餘屬性不細說了,Connector構造的邏輯主要是在NIO和APR選擇中選擇一個協議,個人是org.apache.coyote.http11.Http11NioProtocol,而後反射建立實例並強轉爲ProtocolHandler。關於apr,彷佛是更native,性能聽說更好,但我沒測,相關文檔可參考:http://tomcat.apache.org/tomcat-8.5-doc/apr.html。這裏簡單提一下coyote,它的主要做用是將socket接受的信息封裝爲request和response並提供給上Servlet容器,進行上下層之間的溝通,文檔我沒找到比較新的:http://tomcat.apache.org/tomcat-4.1-doc/config/coyote.html。STRICT_SERVLET_COMPLIANCE也是啓動參數控制,默認是false,配置項是org.apache.catalina.STRICT_SERVLET_COMPLIANCE,默認狀況下會設置URIEncoding = "UTF-8"和URIEncodingLower = URIEncoding.toLowerCase(Locale.ENGLISH),相關詳細介紹可參考:http://tomcat.apache.org/tomcat-7.0-doc/config/systemprops.html。Connector的建立過程比較關鍵,容我單獨寫一篇吧。tomcat

  Connector實例建立好了以後tomcat.getService().addConnector(connector),getService的getServer中new了一個StandardServer,StandardServer的初始化主要是建立了globalNamingResources(globalNamingResources主要用於管理明明上下文和JDNI上下文),並根據catalina.useNaming判斷是否註冊NamingContextListener監聽器給lifecycleListeners。建立Server以後initBaseDir,先讀取catalina.home配置System.getProperty(Globals.CATALINA_BASE_PROP),若是沒取到則使用以前生成的臨時目錄,這段直接看代碼吧:安全

    protected void initBaseDir() {
        String catalinaHome = System.getProperty(Globals.CATALINA_HOME_PROP);
        if (basedir == null) {
            basedir = System.getProperty(Globals.CATALINA_BASE_PROP);
        }
        if (basedir == null) {
            basedir = catalinaHome;
        }
        if (basedir == null) {
            // Create a temp dir.
            basedir = System.getProperty("user.dir") +
                "/tomcat." + port;
        }

        File baseFile = new File(basedir);
        baseFile.mkdirs();
        try {
            baseFile = baseFile.getCanonicalFile();
        } catch (IOException e) {
            baseFile = baseFile.getAbsoluteFile();
        }
        server.setCatalinaBase(baseFile);
        System.setProperty(Globals.CATALINA_BASE_PROP, baseFile.getPath());
        basedir = baseFile.getPath();

        if (catalinaHome == null) {
            server.setCatalinaHome(baseFile);
        } else {
            File homeFile = new File(catalinaHome);
            homeFile.mkdirs();
            try {
                homeFile = homeFile.getCanonicalFile();
            } catch (IOException e) {
                homeFile = homeFile.getAbsoluteFile();
            }
            server.setCatalinaHome(homeFile);
        }
        System.setProperty(Globals.CATALINA_HOME_PROP,
                server.getCatalinaHome().getPath());
    }

   而後又實例化了個StandardService,代碼並無什麼特別的:微信

        service = new StandardService();
        service.setName("Tomcat");
        server.addService( service )

  server.addService( service )這裏除了發佈了一個PropertyChangeEvent事件,也沒作什麼特別的,最後返回這個server。addConnector的邏輯和上面addService沒什麼區別。而後是customizeConnector,這裏設置了Connector的端口、編碼等信息,並將「bindOnInit」和對應值false寫入了最開頭說的靜態代碼塊中的replacements集合,IntrospectionUtils.setProperty(protocolHandler, repl, value)經過反射的方法將protocolHandler實現對象的setBindOnInit存在的狀況下(拼字符串拼出來的)set爲前面的false,這個方法裏有大量的判斷好比參數類型及setter的參數類型,好比返回值類型以及沒找到還會try a setProperty("name", "value")等,setProperty能夠處理好比AbstractEndpoint中有個HashMap<String, Object> attributes的屬性時會attributes.put(name, value)。若是是ssl還會執行customizeSsl方法,設置一些SSL用的屬性好比協議好比祕鑰還有能夠用上祕鑰倉庫等。若是配置了壓縮,這裏還會給協議的相關setter設置值。tomcat.setConnector(connector)不解釋。tomcat.getHost().setAutoDeploy(false),getHost方法中建立了StandardHost並設置host名(例如localhost),並getEngine().addChild( host );而後設置host的自動部署。configureEngine(tomcat.getEngine()),getEngine中若是engine爲null就初始化標準引擎,設置名字爲Tomcat,設置Realm和service.setContainer(engine),不過這裏engine已經在getHost初始化過了因此直接返回;configureEngine方法先設置引擎的後臺進程延遲,並將引擎的Value對象註冊給引擎的pipeline,此時尚無value對象實例。這裏簡單說明一下:value對象在Tomcat的各級容器中都有標準類型,而且各級容器都有一個pipeline,在請求處理過程當中會從各級的第一個value對象開始依次執行一遍,value用於加入到對應的各級容器的邏輯,默認有一個標註value實現,名字相似StandardHostValue。session

  prepareContext(tomcat.getHost(), initializers),initializers這裏是AnnotationConfigEmbeddedWebApplicationContext,Context級的根;準備Context的過程主要設置Base目錄,new一個TomcatEmbeddedContext並在構造中判斷了下loadOnStartup方法是否被重寫;註冊一個FixContextListener監聽,這個監聽用於設置context的配置狀態以及是否加入登陸驗證的邏輯;context.setParentClassLoader;設置各類語言的編碼映射,我這裏是en和fr設置爲UTF-8,此處可使用配置文件org/apache/catalina/util/CharsetMapperDefault .properties;設置是否使用相對地址重定向useRelativeRedirects=false,此屬性應該是Tomcat 8.0.30版本加上的;接着就是初始化webapploader,這裏和完整版的Tomcat有點不同,它用的是虛擬機的方式,會將加載類向上委託loader.setDelegate(true),context.setLoader(loader);以後就開始建立Wapper了,至此engine,host,context及wrapper四個層次的容器都建立完了:

    private void addDefaultServlet(Context context) {
        Wrapper defaultServlet = context.createWrapper();
        defaultServlet.setName("default");
        defaultServlet.setServletClass("org.apache.catalina.servlets.DefaultServlet");
        defaultServlet.addInitParameter("debug", "0");
        defaultServlet.addInitParameter("listings", "false");
        defaultServlet.setLoadOnStartup(1);
        // Otherwise the default location of a Spring DispatcherServlet cannot be set
        defaultServlet.setOverridable(true);
        context.addChild(defaultServlet);
        addServletMapping(context, "/", "default");
    }

   connector從socket接收的數據,解析成HttpServletRequest後就會通過這幾層容器,有容器各自的Value對象鏈依次處理。

  接着是是否註冊jspServlet,jasperInitializer和StoreMergedWebXmlListener我這裏是都沒有的。接着的mergeInitializers方法:

    protected final ServletContextInitializer[] mergeInitializers(
            ServletContextInitializer... initializers) {
        List<ServletContextInitializer> mergedInitializers = new ArrayList<ServletContextInitializer>();
        mergedInitializers.addAll(Arrays.asList(initializers));
        mergedInitializers.addAll(this.initializers);
        return mergedInitializers
                .toArray(new ServletContextInitializer[mergedInitializers.size()]);
    }

 

  configureContext(context, initializersToUse)對context作了些設置工做,包括TomcatStarter(實例化並set給context),LifecycleListener,contextValue,errorpage,Mime,session超時持久化等以及一些自定義工做:

        TomcatStarter starter = new TomcatStarter(initializers);
        if (context instanceof TomcatEmbeddedContext) {
            // Should be true
            ((TomcatEmbeddedContext) context).setStarter(starter);
        }
        context.addServletContainerInitializer(starter, NO_CLASSES);
        for (LifecycleListener lifecycleListener : this.contextLifecycleListeners) {
            context.addLifecycleListener(lifecycleListener);
        }
        for (Valve valve : this.contextValves) {
            context.getPipeline().addValve(valve);
        }
        for (ErrorPage errorPage : getErrorPages()) {
            new TomcatErrorPage(errorPage).addToContext(context);
        }
        for (MimeMappings.Mapping mapping : getMimeMappings()) {
            context.addMimeMapping(mapping.getExtension(), mapping.getMimeType());
        }

 

  Session若是不須要持久化會註冊一個DisablePersistSessionListener。其餘定製化操做是經過TomcatContextCustomizer的實現類實現的:

  context配置完了做爲child add給host,add時給context註冊了個內存泄漏跟蹤的監聽MemoryLeakTrackingListener。postProcessContext(context)方法是空的,留給子類重寫用的。

   getEmbeddedServletContainer方法的最後一行:return getTomcatEmbeddedServletContainer(tomcat)。

    protected TomcatEmbeddedServletContainer getTomcatEmbeddedServletContainer(
            Tomcat tomcat) {
        return new TomcatEmbeddedServletContainer(tomcat, getPort() >= 0);
    }

   TomcatEmbeddedServletContainer的構造函數:

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

  initialize的第一個方法addInstanceIdToEngineName對全局原子變量containerCounter+1,因爲初始值是-1,因此addInstanceIdToEngineName方法內後續的獲取引擎並設置名字的邏輯沒有執行:

    private void addInstanceIdToEngineName() {
        int instanceId = containerCounter.incrementAndGet();
        if (instanceId > 0) {
            Engine engine = this.tomcat.getEngine();
            engine.setName(engine.getName() + "-" + instanceId);
        }
    }

   initialize的第二個方法removeServiceConnectors,將上面new的connection以service(這裏是StandardService[Tomcat])作key保存到private final Map<Service, Connector[]> serviceConnectors中,並將StandardService中的protected Connector[] connectors與service解綁(connector.setService((Service)null);),解綁後下面利用LifecycleBase啓動容器就不會啓動到Connector了。

  以後是this.tomcat.start(),這段比較複雜,我單獨總結一篇吧。

  TomcatEmbeddedServletContainer的初始化,接下來是rethrowDeferredStartupExceptions,這個方法檢查初始化過程當中的異常,若是有直接在主線程拋出,檢查方法是TomcatStarter中的private volatile Exception startUpException,這個值是在Context啓動過程當中記錄的:

    @Override
    public void onStartup(Set<Class<?>> classes, ServletContext servletContext)
            throws ServletException {
        try {
            for (ServletContextInitializer initializer : this.initializers) {
                initializer.onStartup(servletContext);
            }
        }
        catch (Exception ex) {
            this.startUpException = ex;
            // Prevent Tomcat from logging and re-throwing when we know we can
            // deal with it in the main thread, but log for information here.
            if (logger.isErrorEnabled()) {
                logger.error("Error starting Tomcat context. Exception: "
                        + ex.getClass().getName() + ". Message: " + ex.getMessage());
            }
        }
    }

  Context context = findContext():

    private Context findContext() {
        for (Container child : this.tomcat.getHost().findChildren()) {
            if (child instanceof Context) {
                return (Context) child;
            }
        }
        throw new IllegalStateException("The host does not contain a Context");
    }

  綁定命名的上下文和classloader,不成功也無所謂:

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

  startDaemonAwaitThread方法的註釋是:與Jetty不一樣,Tomcat全部的線程都是守護線程,因此建立一個非守護線程(例:Thread[container-0,5,main])來避免服務到這就shutdown了:

    private void startDaemonAwaitThread() {
        Thread awaitThread = new Thread("container-" + (containerCounter.get())) {

            @Override
            public void run() {
                TomcatEmbeddedServletContainer.this.tomcat.getServer().await();
            }

        };
        awaitThread.setContextClassLoader(getClass().getClassLoader());
        awaitThread.setDaemon(false);
        awaitThread.start();
    }

  這個await每10秒檢查一次是否關閉了:

            try {
                awaitThread = Thread.currentThread();
                while(!stopAwait) {
                    try {
                        Thread.sleep( 10000 );
                    } catch( InterruptedException ex ) {
                        // continue and check the flag
                    }
                }
            } finally {
                awaitThread = null;
            }
            return;

  回到EmbeddedWebApplicationContext,initPropertySources方法,用初始化好的servletContext完善環境變量:

    /**
     * {@inheritDoc}
     * <p>Replace {@code Servlet}-related property sources.
     */
    @Override
    protected void initPropertySources() {
        ConfigurableEnvironment env = getEnvironment();
        if (env instanceof ConfigurableWebEnvironment) {
            ((ConfigurableWebEnvironment) env).initPropertySources(this.servletContext, null);
        }
    }

  createEmbeddedServletContainer就結束了,內嵌容器的啓動過程至此結束。

==========================================================

咱最近用的github:https://github.com/saaavsaaa

微信公衆號:

                      

相關文章
相關標籤/搜索