淺讀Tomcat源碼(二)---啓動過程

上一篇我們講了Tomcat源碼包的下載配置以及Tomcat組件的基本介紹,這一篇我們着重來講述Tomcat的完整啓動過程。


衆所周知,Tomcat的啓動和停止是有startup.sh/bat和shutdown.sh/bat來控制的,這裏sh是linux的shellScript腳本,其運行的模式是調用catalina.sh,並傳入參數startup或shutdown,具體就不做詳述了,而catalina.sh最終會調用java類org.apache.catalina.startup.BootStrap的start或stop方法,我們來看下Bootstrap的start方法:

  /**
     * Start the Catalina daemon.
     */
    public void start()
        throws Exception {
        if( catalinaDaemon==null ) init();

        Method method = catalinaDaemon.getClass().getMethod("start", (Class [] )null);
        method.invoke(catalinaDaemon, (Object [])null);

    }

這裏我們可以看到bootstrap的start方法其實是用反射形式調用真正的啓動類的start方法,這裏的catalinaDamon其實是同一個包下的Catalina類,其start方法如下:

 /**
     * Start a new server instance.
     */
    public void start() {

        if (getServer() == null) {
            load();
        }

        if (getServer() == null) {
            log.fatal("Cannot start server. Server instance is not configured.");
            return;
        }

        long t1 = System.nanoTime();

        // Start the new server
        try {
            getServer().start();
        } catch (LifecycleException e) {
            log.error("Catalina.start: ", e);
        }

        long t2 = System.nanoTime();
        if(log.isInfoEnabled())
            log.info("Server startup in " + ((t2 - t1) / 1000000) + " ms");

        try {
            // Register shutdown hook
            if (useShutdownHook) {
                if (shutdownHook == null) {
                    shutdownHook = new CatalinaShutdownHook();
                }
                Runtime.getRuntime().addShutdownHook(shutdownHook);

                // If JULI is being used, disable JULI's shutdown hook since
                // shutdown hooks run in parallel and log messages may be lost
                // if JULI's hook completes before the CatalinaShutdownHook()
                LogManager logManager = LogManager.getLogManager();
                if (logManager instanceof ClassLoaderLogManager) {
                    ((ClassLoaderLogManager) logManager).setUseShutdownHook(
                            false);
                }
            }
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            // This will fail on JDK 1.2. Ignoring, as Tomcat can run
            // fine without the shutdown hook.
        }

        if (await) {
            await();
            stop();
        }

    }

代碼很長,我們可以看到他使用了getServer,獲取到了對應的server組件,server組件的初始化是在init的過程中,對應到本類的load方法,其中核心的代碼塊是:

// Create and execute our Digester
        Digester digester = createStartDigester();
// some code
     digester.push(this);
     digester.parse(inputSource); 	

我們可以看到這裏使用Digester去初始化一些組件,Digester的實際作用是解析server.xml,用的是SAX包解析,這裏本篇先不做詳述。


總而言之在解析完server.xml之後,catalina中的server類算是初始化了,然後就調用了server類的startup方法,來看server接口的實現類org.apache.catalina.core.StandardServer:

@Override
    protected void startInternal() throws LifecycleException {

        fireLifecycleEvent(CONFIGURE_START_EVENT, null);
        setState(LifecycleState.STARTING);

        globalNamingResources.start();
        
        // Start our defined Services
        synchronized (services) {
            for (int i = 0; i < services.length; i++) {
                services[i].start();
            }
        }
    }
我們可以看到server類的start就是將server.xml中解析出來的service全部啓動,但是要注意一點的是,server類還有個非常大的作用,在上述catalina的啓動中我們很清楚看到了其調用了await方法:

/**
     * Await and shutdown.
     */
    public void await() {

        getServer().await();

    }


而server類的await方法是這樣的:

/**
     * Wait until a proper shutdown command is received, then return.
     * This keeps the main thread alive - the thread pool listening for http 
     * connections is daemon threads.
     */
    @Override
    public void await() {
        // Negative values - don't wait on port - tomcat is embedded or we just don't like ports
        if( port == -2 ) {
            // undocumented yet - for embedding apps that are around, alive.
            return;
        }
        if( port==-1 ) {
            try {
                awaitThread = Thread.currentThread();
                while(!stopAwait) {
                    try {
                        Thread.sleep( 10000 );
                    } catch( InterruptedException ex ) {
                        // continue and check the flag
                    }
                }
            } finally {
                awaitThread = null;
            }
            return;
        }

        // Set up a server socket to wait on
        try {
            awaitSocket = new ServerSocket(port, 1,
                    InetAddress.getByName(address));
        } catch (IOException e) {
            log.error("StandardServer.await: create[" + address
                               + ":" + port
                               + "]: ", e);
            return;
        }

        try {
            awaitThread = Thread.currentThread();

            // Loop waiting for a connection and a valid command
            while (!stopAwait) {
                ServerSocket serverSocket = awaitSocket;
                if (serverSocket == null) {
                    break;
                }
    
                // Wait for the next connection
                Socket socket = null;
                StringBuilder command = new StringBuilder();
                try {
                    InputStream stream;
                    try {
                        socket = serverSocket.accept();
                        socket.setSoTimeout(10 * 1000);  // Ten seconds
                        stream = socket.getInputStream();
                    } catch (AccessControlException ace) {
                        log.warn("StandardServer.accept security exception: "
                                + ace.getMessage(), ace);
                        continue;
                    } catch (IOException e) {
                        if (stopAwait) {
                            // Wait was aborted with socket.close()
                            break;
                        }
                        log.error("StandardServer.await: accept: ", e);
                        break;
                    }

                    // Read a set of characters from the socket
                    int expected = 1024; // Cut off to avoid DoS attack
                    while (expected < shutdown.length()) {
                        if (random == null)
                            random = new Random();
                        expected += (random.nextInt() % 1024);
                    }
                    while (expected > 0) {
                        int ch = -1;
                        try {
                            ch = stream.read();
                        } catch (IOException e) {
                            log.warn("StandardServer.await: read: ", e);
                            ch = -1;
                        }
                        if (ch < 32)  // Control character or EOF terminates loop
                            break;
                        command.append((char) ch);
                        expected--;
                    }
                } finally {
                    // Close the socket now that we are done with it
                    try {
                        if (socket != null) {
                            socket.close();
                        }
                    } catch (IOException e) {
                        // Ignore
                    }
                }

                // Match against our command string
                boolean match = command.toString().equals(shutdown);
                if (match) {
                    log.info(sm.getString("standardServer.shutdownViaPort"));
                    break;
                } else
                    log.warn("StandardServer.await: Invalid command '"
                            + command.toString() + "' received");
            }
        } finally {
            ServerSocket serverSocket = awaitSocket;
            awaitThread = null;
            awaitSocket = null;

            // Close the server socket and return
            if (serverSocket != null) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    // Ignore
                }
            }
        }
    }

這段代碼曾經將我帶入了一個徹底的誤區,以爲這就是Tomcat接收請求的serverSocket的啓動,但是事實完全不是這樣子,當我看到port的默認值的時候:

private int port = 8005;

以及在server.xml的配置的時候
<Server port="8005" shutdown="SHUTDOWN">

我才明白過來,衆所周知Tomcat啓動時候默認佔用了三個端口:8080、8005、8009,8080和8009分別是HTTP和AJP協議的服務端口,那麼8005是幹嘛的呢,其實8005是tomcat停止監聽線程的服務端口,看上面await方法就可以看出,這個serverSocket一直在監聽shutdown指令,如果接受到了則開始停止過程。


這也說明了上面在catalina的start過程中爲何要把awiat方法放在最後,因爲我們看到這個停止監聽是運行在當前線程(即啓動線程)下的,必須確保所有需要開啓的服務都在新線程中運行起來再進行await過程。


現在回過頭來看service的start過程,我們看到org.apache.catalina.core.StandardService類

 @Override
    protected void startInternal() throws LifecycleException {

        if(log.isInfoEnabled())
            log.info(sm.getString("standardService.start.name", this.name));
        setState(LifecycleState.STARTING);

        // Start our defined Container first
        if (container != null) {
            synchronized (container) {
                container.start();
            }
        }

        synchronized (executors) {
            for (Executor executor: executors) {
                executor.start();
            }
        }

        // Start our defined Connectors second
        synchronized (connectors) {
            for (Connector connector: connectors) {
                try {
                    // If it has already failed, don't try and start it
                    if (connector.getState() != LifecycleState.FAILED) {
                        connector.start();
                    }
                } catch (Exception e) {
                    log.error(sm.getString(
                            "standardService.connector.startFailed",
                            connector), e);
                }
            }
        }
    }

可以看到這個過程的啓動有三件,其中目前我比較清楚的是頭尾兩件,一是容器container的啓動,這裏的container實則是engine容器;二是connector的啓動,Connector是server.xml中註冊的,我們先來看Connector的啓動過程,看到org.apache.catalina.connector.Connector類:

/**
     * Begin processing requests via this Connector.
     *
     * @exception LifecycleException if a fatal startup error occurs
     */
    @Override
    protected void startInternal() throws LifecycleException {

        setState(LifecycleState.STARTING);

        try {
            protocolHandler.start();
        } catch (Exception e) {
            String errPrefix = "";
            if(this.service != null) {
                errPrefix += "service.getName(): \"" + this.service.getName() + "\"; ";
            }

            throw new LifecycleException
                (errPrefix + " " + sm.getString
                 ("coyoteConnector.protocolHandlerStartFailed"), e);
        }

        mapperListener.start();
    }

這裏啓動了protocolHandler,根據Connector的實質作用的不同,Tomcat會給他分配不同的Handler,這裏我們看到用於處理Http1.1的handler,org.apache.coyote.http11.Http11Protocol類,看到其對應的start,這裏的start是在其父類AbstratcProtocolHandler中實現的:

   @Override
    public void start() throws Exception {
        if (getLog().isInfoEnabled())
            getLog().info(sm.getString("abstractProtocolHandler.start",
                    getName()));
        try {
            endpoint.start();
        } catch (Exception ex) {
            getLog().error(sm.getString("abstractProtocolHandler.startError",
                    getName()), ex);
            throw ex;
        }
    }

這裏啓動了endpoint,看到其子類JIOEndpoint:

 @Override
    public void startInternal() throws Exception {

        if (!running) {
            running = true;
            paused = false;

            // Create worker collection
            if (getExecutor() == null) {
                createExecutor();
            }
            
            initializeConnectionLatch();

            // Start acceptor threads
            for (int i = 0; i < acceptorThreadCount; i++) {
                Thread acceptorThread = new Thread(new Acceptor(),
                        getName() + "-Acceptor-" + i);
                acceptorThread.setPriority(threadPriority);
                acceptorThread.setDaemon(getDaemon());
                acceptorThread.start();
            }
            
            // Start async timeout thread
            Thread timeoutThread = new Thread(new AsyncTimeout(),
                    getName() + "-AsyncTimeout");
            timeoutThread.setPriority(threadPriority);
            timeoutThread.setDaemon(true);
            timeoutThread.start();
        }
    }

在這裏啓動了新線程,新線程的操作定義在其內部類Acceptor類中:

/**
     * Server socket acceptor thread.
     */
    protected class Acceptor implements Runnable {


        /**
         * The background thread that listens for incoming TCP/IP connections and
         * hands them off to an appropriate processor.
         */
        @Override
        public void run() {

            int errorDelay = 0;

            // Loop until we receive a shutdown command
            while (running) {

                // Loop if endpoint is paused
                while (paused && running) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        // Ignore
                    }
                }

                if (!running) {
                    break;
                }
                try {
                    //if we have reached max connections, wait
                    awaitConnection();

                    Socket socket = null;
                    try {
                        // Accept the next incoming connection from the server
                        // socket
                        socket = serverSocketFactory.acceptSocket(serverSocket);
                    } catch (IOException ioe) {
                        // Introduce delay if necessary
                        errorDelay = handleExceptionWithDelay(errorDelay);
                        // re-throw
                        throw ioe;
                    }
                    // Successful accept, reset the error delay
                    errorDelay = 0;

                    // Configure the socket
                    if (setSocketOptions(socket)) {
                        // Hand this socket off to an appropriate processor
                        if (!processSocket(socket)) {
                            // Close socket right away
                            try {
                                socket.close();
                            } catch (IOException e) {
                                // Ignore
                            }
                        } else {
                            countUpConnection();
                        }
                    } else {
                        // Close socket right away
                        try {
                            socket.close();
                        } catch (IOException e) {
                            // Ignore
                        }
                    }
                } catch (IOException x) {
                    if (running) {
                        log.error(sm.getString("endpoint.accept.fail"), x);
                    }
                } catch (NullPointerException npe) {
                    if (running) {
                        log.error(sm.getString("endpoint.accept.fail"), npe);
                    }
                } catch (Throwable t) {
                    ExceptionUtils.handleThrowable(t);
                    log.error(sm.getString("endpoint.accept.fail"), t);
                }
                // The processor will recycle itself when it finishes
            }
        }
    }
看到這裏,我們長舒了一口氣,因爲終於看到了處理http請求的監聽在8080端口serverSocket運行起來,從代碼看這裏把接收到的socket通過processSocket方法進行處理,這在之後的請求處理的篇章再做詳述,到此爲止Tomcat自身類的組件算是啓動完成了,但是對於我們開發人員添加的web應用的處理還尚未講解,我們回到service的start方法中,看到這裏container的啓動:

  @Override
    protected void startInternal() throws LifecycleException {

        if(log.isInfoEnabled())
            log.info(sm.getString("standardService.start.name", this.name));
        setState(LifecycleState.STARTING);

        // Start our defined Container first
        if (container != null) {
            synchronized (container) {
                container.start();
            }
        }
}

上一篇我們說過,tomcat的容器有engine、host、context、wrapper四種,其中前兩者都是在server.xml中解析出來的,而後兩者則需要在運行時候再去讀取,我們來看container的基本實現類org.apache.catalina.core.ContainerBase類的start過程:


/**
     * Start this component and implement the requirements
     * of {@link org.apache.catalina.util.LifecycleBase#startInternal()}.
     *
     * @exception LifecycleException if this component detects a fatal error
     *  that prevents this component from being used
     */
    @Override
    protected synchronized void startInternal() throws LifecycleException {

        // Start our subordinate components, if any
        if ((loader != null) && (loader instanceof Lifecycle))
            ((Lifecycle) loader).start();
        logger = null;
        getLogger();
        if ((logger != null) && (logger instanceof Lifecycle))
            ((Lifecycle) logger).start();
        if ((manager != null) && (manager instanceof Lifecycle))
            ((Lifecycle) manager).start();
        if ((cluster != null) && (cluster instanceof Lifecycle))
            ((Lifecycle) cluster).start();
        if ((realm != null) && (realm instanceof Lifecycle))
            ((Lifecycle) realm).start();
        if ((resources != null) && (resources instanceof Lifecycle))
            ((Lifecycle) resources).start();

        // Start our child containers, if any
        Container children[] = findChildren();
        for (int i = 0; i < children.length; i++) {
            children[i].start();
        }

        // Start the Valves in our pipeline (including the basic), if any
        if (pipeline instanceof Lifecycle)
            ((Lifecycle) pipeline).start();


        setState(LifecycleState.STARTING);

        // Start our thread
        threadStart();

    }
可以看到所有的容器都有一個啓動子組件的過程,但是重點在於剛開始父組件是如何拿到子組件的呢,其實我個人覺得這裏的for循環只是保險起見,萬一子組件已經被納入了父組件中的操作,真正實現啓動子組件的是下面的threadStart過程:

 /**
     * Start the background thread that will periodically check for
     * session timeouts.
     */
    protected void threadStart() {

        if (thread != null)
            return;
        if (backgroundProcessorDelay <= 0)
            return;

        threadDone = false;
        String threadName = "ContainerBackgroundProcessor[" + toString() + "]";
        thread = new Thread(new ContainerBackgroundProcessor(), threadName);
        thread.setDaemon(true);
        thread.start();

    }

這個過程是個非常長的鏈式調用,大家自己去一個個ctrl點擊進去,到最後會發現四個組件都有對應的初始化控制類,在startup包下的HostConfig、ContextConfig等,其實現了lifecycleEvent方法,我們專門來看一下Host組件對應的Config的過程:

 @Override
    public void lifecycleEvent(LifecycleEvent event) {

        if (event.getType().equals(Lifecycle.PERIODIC_EVENT))
            check();

        // Identify the host we are associated with
        try {
            host = (Host) event.getLifecycle();
            if (host instanceof StandardHost) {
                setCopyXML(((StandardHost) host).isCopyXML());
                setDeployXML(((StandardHost) host).isDeployXML());
                setUnpackWARs(((StandardHost) host).isUnpackWARs());
            }
        } catch (ClassCastException e) {
            log.error(sm.getString("hostConfig.cce", event.getLifecycle()), e);
            return;
        }

        // Process the event that has occurred
        if (event.getType().equals(Lifecycle.START_EVENT))
            start();
        else if (event.getType().equals(Lifecycle.STOP_EVENT))
            stop();

    }

這裏我們看到,如果tomcat處於啓動之初,會調用check方法,check則會調用deployApps方法:

**
     * Deploy applications for any directories or WAR files that are found
     * in our "application root" directory.
     */
    protected void deployApps() {

        File appBase = appBase();
        File configBase = configBase();
        String[] filteredAppPaths = filterAppPaths(appBase.list());
        // Deploy XML descriptors from configBase
        deployDescriptors(configBase, configBase.list());
        // Deploy WARs, and loop if additional descriptors are found
        deployWARs(appBase, filteredAppPaths);
        // Deploy expanded folders
        deployDirectories(appBase, filteredAppPaths);
        
    }
這裏就分流處理了,取得webapp文件夾的路徑之後,會分別處理war、文件夾等多種web應用的發佈,根據經驗我們知道war的發佈實質是tomcat將其解壓成爲文件夾應用,所以我們關鍵看下deployDirectories方法:

/**
     * Deploy directories.
     */
    protected void deployDirectories(File appBase, String[] files) {

        if (files == null)
            return;
        
        for (int i = 0; i < files.length; i++) {

            if (files[i].equalsIgnoreCase("META-INF"))
                continue;
            if (files[i].equalsIgnoreCase("WEB-INF"))
                continue;
            File dir = new File(appBase, files[i]);
            if (dir.isDirectory()) {
                ContextName cn = new ContextName(files[i]);

                if (isServiced(cn.getName()))
                    continue;

                deployDirectory(cn, dir, files[i]);
            }
        }
    }

獲取所有文件夾,並進入deployDirectory:

 /**
     * @param cn
     * @param dir
     * @param file
     */
    protected void deployDirectory(ContextName cn, File dir, String file) {
        
        if (deploymentExists(cn.getName()))
            return;

        DeployedApplication deployedApp = new DeployedApplication(cn.getName());

        // Deploy the application in this directory
        if( log.isInfoEnabled() ) 
            log.info(sm.getString("hostConfig.deployDir", file));
        try {
            Context context = null;
            File xml = new File(dir, Constants.ApplicationContextXml);
            File xmlCopy = null;
            if (deployXML && xml.exists()) {
                synchronized (digester) {
                    try {
                        context = (Context) digester.parse(xml);
                        if (context == null) {
                            log.error(sm.getString(
                                    "hostConfig.deployDescriptor.error",
                                    xml));
                            return;
                        }
                    } finally {
                        digester.reset();
                    }
                }
                if (copyXML) {
                    xmlCopy = new File(configBase(), file + ".xml");
                    InputStream is = null;
                    OutputStream os = null;
                    try {
                        is = new FileInputStream(xml);
                        os = new FileOutputStream(xmlCopy);
                        IOTools.flow(is, os);
                        // Don't catch IOE - let the outer try/catch handle it
                    } finally {
                        try {
                            if (is != null) is.close();
                        } catch (IOException e){
                            // Ignore
                        }
                        try {
                            if (os != null) os.close();
                        } catch (IOException e){
                            // Ignore
                        }
                    }
                    context.setConfigFile(xmlCopy.toURI().toURL());
                } else {
                    context.setConfigFile(xml.toURI().toURL());
                }
            } else {
                context = (Context) Class.forName(contextClass).newInstance();
            }

            Class<?> clazz = Class.forName(host.getConfigClass());
            LifecycleListener listener =
                (LifecycleListener) clazz.newInstance();
            context.addLifecycleListener(listener);

            context.setName(cn.getName());
            context.setPath(cn.getPath());
            context.setWebappVersion(cn.getVersion());
            context.setDocBase(file);
            host.addChild(context);
            deployedApp.redeployResources.put(dir.getAbsolutePath(),
                    Long.valueOf(dir.lastModified()));
            if (xmlCopy != null) {
                deployedApp.redeployResources.put(
                        xmlCopy.getAbsolutePath(),
                        Long.valueOf(xmlCopy.lastModified()));
            }
            addWatchedResources(deployedApp, dir.getAbsolutePath(), context);
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            log.error(sm.getString("hostConfig.deployDir.error", file), t);
        }

        deployed.put(cn.getName(), deployedApp);
    }

當初我看到這段代碼真的是一身爽快,因爲終於找到容器的初始化方式了,我們看到這裏初始化了StandardContext類對象,初始化完了以後將web應用的根路徑等信息全部填入context,並讓context去初始化wrapper的內容,中間過程我們省去了,大家自己看,我們直接找到ContextConfig解析過程的核心代碼:

protected void parseWebXml(InputSource source, WebXml dest,
            boolean fragment) {
        
        if (source == null) return;


        XmlErrorHandler handler = new XmlErrorHandler();


        // Web digesters and rulesets are shared between contexts but are not
        // thread safe. Whilst there should only be one thread at a time
        // processing a config, play safe and sync.
        Digester digester;
        if (fragment) {
            digester = webFragmentDigester;
        } else {
            digester = webDigester;
        }
        
        synchronized(digester) {
            
            digester.push(dest);
            digester.setErrorHandler(handler);
            
            if(log.isDebugEnabled()) {
                log.debug(sm.getString("contextConfig.applicationStart",
                        source.getSystemId()));
            }


            try {
                digester.parse(source);


                if (handler.getWarnings().size() > 0 ||
                        handler.getErrors().size() > 0) {
                    ok = false;
                    handler.logFindings(log, source.getSystemId());
                }
            } catch (SAXParseException e) {
                log.error(sm.getString("contextConfig.applicationParse",
                        source.getSystemId()), e);
                log.error(sm.getString("contextConfig.applicationPosition",
                                 "" + e.getLineNumber(),
                                 "" + e.getColumnNumber()));
                ok = false;
            } catch (Exception e) {
                log.error(sm.getString("contextConfig.applicationParse",
                        source.getSystemId()), e);
                ok = false;
            } finally {
                digester.reset();
                if (fragment) {
                    webFragmentRuleSet.recycle();
                } else {
                    webRuleSet.recycle();
                }
            }
        }
    }

這裏還是用degister,只不過這裏的degister是用來解析web.xml的,解析完了以後,將每個Servlet類的信息裝載在一個Wrapper中,到此爲止容器的基本啓動算是完成了。


最後還記得connector的start過程中有一個mappingListner的init嗎?

 @Override
    public void startInternal() throws LifecycleException {

        setState(LifecycleState.STARTING);

        // Find any components that have already been initialized since the
        // MBean listener won't be notified as those components will have
        // already registered their MBeans
        findDefaultHost();
        
        Engine engine = (Engine) connector.getService().getContainer();
        addListeners(engine);
        
        Container[] conHosts = engine.findChildren();
        for (Container conHost : conHosts) {
            Host host = (Host) conHost;
            if (!LifecycleState.NEW.equals(host.getState())) {
                // Registering the host will register the context and wrappers
                registerHost(host);
            }
        }
    }

這個過程就是從connector對應的service中取出容器engine,並註冊其子容器,依次註冊,之後在請求的處理過程中去調動註冊的容器,我們下一篇再講述。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章