Tomcat源码分析之启动流程分析

前面的博客介绍了Tomcat的整个架构及各个配置文件及目录的作用,接下来就对Tomcat的源码进行分析了。

Tomcat源码调试环境准备

首先下载Tomcat源码,读者可自行去Tomcat官网 下载,若执行力差的同学也可直接从此处pull。

Tomcat源码导入到开发工具中的方法有多种,笔者采用最直接的方式,解压源码包后直接导入到开发工具中,导入之后的源码并不能直接运行,还需要几个依赖包,读者可从此处的lib目录下获取,也可自行搜集。

找好依赖包也并不能让Tomcat源码正常运行,还需要为Bootstrap这个启动类增加几个启动参数。

-Dcatalina.home=/Users/chenmin/GitHub/tomcat
-Dcatalina.base=/Users/chenmin/GitHub/tomcat
-Djava.endorsed.dirs=/Users/chenmin/GitHub/tomcat/endorsed
-Djava.io.tmpdir=/Users/chenmin/GitHub/tomcat/temp
-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
-Djava.util.logging.config.file=/Users/chenmin/GitHub/tomcat/conf/logging.properties

上面的参数具体代表的意思就不一一详述了,其实光看名字就知道都是干嘛用的了。

以上准备步骤做好之后,就可以直接运行Bootstrap类,运行Tomcat源码进行调试了。

Tomcat Server的组成

整体说明

在上面对配置文件的说明中,通过server.xml的解释,我们知道server.xml中最顶级的元素是server,而server.xml中的每一个元素我们都可以把它看做是Tomcat中的某一个部分。所以我们可以参照着server.xml来分析源码。

Tomcat最顶层的容器叫Server,它代表着整个Tomcat服务器。Server中至少要包含一个Service来提供服务。Service包含两部分:Connector和Container。Connector负责网络连接,request/response的创建,并对Socket和request、response进行转换等,Container用于封装和管理Servlet,并处理具体的request请求。

一个Tomcat中只有一个Server,一个Server可以有多个Service来提供服务,一个Service只有一个Container,但是可以有多个Connector(一个服务可以有多个连接)。

Tomcat源码分析之启动流程分析_第1张图片
tomcat整体结构.png

各组件详解

可结合conf/配置文件说明中的server.xml的说明来看

  • Server

    Server代表整个Servlet容器

  • Service

    Service是由一个或多个Connector以及一个Engine,负责处理所有Connector所获得的客户请求的集合。

  • Connector

    Connector将在某个指定端口上侦听客户请求,并将获得的请求交给Engine来处理,从Engine处获得回应并返回给客户端。

    Tomcat有两个默认的Connector,一个直接监听来自浏览器的http请求,一个监听来自其他WebServer的请求。

    Coyote Http/1.1 Connector在端口8080上监听来自浏览器的http请求

    Coyote AJP/1.3 Connector在端口8009上监听来自其他WebServer的servlet/jsp代理请求。

  • Engine

    Engine下可以配置多个虚拟主机,每个虚拟主机都有一个域名,当Engine获得一个请求时,Engine会把该请求匹配到某个Host上,然后把该请求交给该Host来处理。

    Engine有一个默认虚拟主机,当请求无法匹配到任何一个Host上的时候,将交给该默认Host来处理。

  • Host

    代表一个虚拟主机,每个虚拟主机和某个网络域名相匹配。每个虚拟主机下都可以部署一个或者多个WebApp,每个WebApp对应于一个Context,有一个ContextPath。当Host获得一个请求时,将把该请求匹配到某个Context上,然后把该请求交给该Context来处理。匹配的方法是“最长匹配”,所以一个path==“”的Context将成为该Host的默认Context,所有无法和其他Context的路径名匹配的请求都将最终和该默认Context匹配。

  • Context

    一个Context对应于一个Web Application(Web应用),一个Web应用有一个或多个Servlet组成,Context在创建的时候将根据配置文件$CATALINA_HOME/conf/web.xml和$WEBAPP_HOME/WEB-INF/web.xml载入Servlet类。如果找到,则执行该类,获得请求的回应,并返回。

    Tomcat各组件关系图(此图来此网上)

    Tomcat源码分析之启动流程分析_第2张图片
    tomcat-startup.gif

源码分析

启动总体流程

Tomcat里的Server由org.apache.catalina.startup.Catalina来管理,Catalina是整个Tomcat的管理类,它里面的三个方法load,start,stop分别用来管理整个服务器的生命周期,load方法用于根据conf/server.xml文件创建Server并调用Server的init方法进行初始化,start方法用于启动服务器,stop方法用于停止服务器,start和stop方法在内部分别调用了Server的start和stop方法,load方法内部调用了Server的init方法,这三个方法都会按容器的结构逐层调用相应的方法,比如,Server的start方法中会调用所有的Service中的start方法,Service中的start方法又会调用所有的Service中的start方法,Service中的start方法又会调用所有包含的Connectors和Container的start方法,这样这个服务器就启动了,init和stop方法也一样,这就是整个Tomcat的生命周期的管理方式。Catalina还有个await方法,await方法直接调用了Server的await方法,这个方法的作用是进入一个循环,让主线程不退出。

Tomcat的启动入口上面说过,是org.apache.catalina.startup.Bootstrap,作用类似于一个CatalinaAdaptor,具体的处理过程还是使用Catalina来完成的,这么做的好处是可以把启动的入口和具体的管理类分开,从而可以很方便的创建出多种启动方式,每种启动方式只需要写一个相应的CatalinaAdaptor就可以了。

Tomcat源码分析之启动流程分析_第3张图片
tomcat启动流程分析.png

注:图片比较模糊,如需查看清晰图片,请自行下载resources/images目录中的tomcat启动流程分析.png 或 resources/docs中的Tomcat源码分析.mdl ,使用Rational Rose等工具打开即可。

启动流程详解

正常情况下启动Tomcat,就是调用Bootstrap的main方法,代码如下:

public static void main(String args[]) {
        if (daemon == null) {
            // Don't set daemon until init() has completed
            // 初始化了ClassLoader,并用ClassLoader创建了Catalina实例,赋给catalinaDaemon变量
            Bootstrap bootstrap = new Bootstrap();
            try {
                bootstrap.init();
            } catch (Throwable t) {
                handleThrowable(t);
                t.printStackTrace();
                return;
            }
            daemon = bootstrap;
        } else {
            // When running as a service the call to stop will be on a new
            // thread so make sure the correct class loader is used to prevent
            // a range of class not found exceptions.
            Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
        }
        try {
            String command = "start";
            if (args.length > 0) {
                command = args[args.length - 1];
            }
            if (command.equals("startd")) {
                args[args.length - 1] = "start";
                daemon.load(args);
                daemon.start();
            } else if (command.equals("stopd")) {
                args[args.length - 1] = "stop";
                daemon.stop();
            } else if (command.equals("start")) {
                daemon.setAwait(true);
                daemon.load(args);
                daemon.start();
            } else if (command.equals("stop")) {
                daemon.stopServer(args);
            } else if (command.equals("configtest")) {
                daemon.load(args);
                if (null==daemon.getServer()) {
                    System.exit(1);
                }
                System.exit(0);
            } else {
                log.warn("Bootstrap: command \"" + command + "\" does not exist.");
            }
        } catch (Throwable t) {
            // Unwrap the Exception for clearer error reporting
            if (t instanceof InvocationTargetException &&
                    t.getCause() != null) {
                t = t.getCause();
            }
            handleThrowable(t);
            t.printStackTrace();
            System.exit(1);
        }
    }

main方法中,首先执行init方法初始化了Tomcat自己的类加载器,并通过类加载器创建Catalina实例,然后赋给catalinaDaemon变量,后续操作都使用catalinaDaemon来执行。

后面默认执行start命令,将调用setAwait(true),load(args)和start()这三个方法,这三个方法内部都通过反射调用了Catalina的相应方法。

// org.apache.catalina.startup.Catalina
public void setAwait(boolean b) {
    await = b;
}

setAwait方法用于设置Server启动完成后是否进入等待状态的标志,如果为true则进入,否则不进入。

// org.apache.catalina.startup.Catalina
/**
  * Start a new server instance.
  */
public void load() {
    long t1 = System.nanoTime();
    initDirs();
    // Before digester - it may be needed
    initNaming();
    // Create and execute our Digester
    Digester digester = createStartDigester();
    InputSource inputSource = null;
    InputStream inputStream = null;
    File file = null;
    try {
        try {
            file = configFile();
            inputStream = new FileInputStream(file);
            inputSource = new InputSource(file.toURI().toURL().toString());
        } catch (Exception e) {
            if (log.isDebugEnabled()) {
                log.debug(sm.getString("catalina.configFail", file), e);
            }
        }
        if (inputStream == null) {
            try {
                inputStream = getClass().getClassLoader().getResourceAsStream(getConfigFile());
                inputSource = new InputSource(getClass().getClassLoader().getResource(getConfigFile()).toString());
            } catch (Exception e) {
                if (log.isDebugEnabled()) {
                    log.debug(sm.getString("catalina.configFail",getConfigFile()), e);
                }
            }
        }
        // This should be included in catalina.jar
        // Alternative: don't bother with xml, just create it manually.
        if (inputStream == null) {
            try {
                inputStream = getClass().getClassLoader().getResourceAsStream("server-embed.xml");
                inputSource = new InputSource(getClass().getClassLoader().getResource("server-embed.xml").toString());
            } catch (Exception e) {
                if (log.isDebugEnabled()) {
                    log.debug(sm.getString("catalina.configFail","server-embed.xml"), e);
                }
            }   
      }
      if (inputStream == null || inputSource == null) {
            if  (file == null) {
                log.warn(sm.getString("catalina.configFail",getConfigFile() + "] or [server-embed.xml]"));
            } else {
                log.warn(sm.getString("catalina.configFail",file.getAbsolutePath()));
                if (file.exists() && !file.canRead()) {
                    log.warn("Permissions incorrect, read permission is not allowed on the file.");
                }
            }
            return;
        }
        try {
            inputSource.setByteStream(inputStream);
            digester.push(this);
            digester.parse(inputSource);
        } catch (SAXParseException spe) {
            log.warn("Catalina.start using " + getConfigFile() + ": " + spe.getMessage());
            return;
        } catch (Exception e) {
            log.warn("Catalina.start using " + getConfigFile() + ": " , e);
            return;
        }
    } finally {
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (IOException e) {
            // Ignore
            }
        }
    }
    getServer().setCatalina(this);
    getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
    getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());
    // Stream redirection
    initStreams();
    // Start the new server
    try {
        getServer().init();
    } catch (LifecycleException e) {
        if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) {
            throw new java.lang.Error(e);
        } else {
            log.error("Catalina.start", e);
        }
    }
    long t2 = System.nanoTime();
    if(log.isInfoEnabled()) {
        log.info("Initialization processed in " + ((t2 - t1) / 1000000) + " ms");
    }
}

Catalina的load方法根据conf/server.xml创建了Server对象,并赋值给server属性(具体是通过开源项目Digester完成的),然后调用了server的init方法。

// org.apache.catalina.startup.Catalina
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.fatal(sm.getString("catalina.serverStartFail"), e);
        try {
            getServer().destroy();
        } catch (LifecycleException e1) {
            log.debug("destroy() failed for failed Server ", e1);
        }
        return;
    }
    long t2 = System.nanoTime();
    if(log.isInfoEnabled()) {
        log.info("Server startup in " + ((t2 - t1) / 1000000) + " ms");
    }
    // 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);
        }
    }
    if (await) {
        await();
        stop();
    }
}

这里首先判断Server是否已经存在了,如果不存在则调用load方法来初始化Server,然后调用Server的start方法来启动服务器,最后注册了关闭钩子并根据await属性判断是否进入等待状态,之前我们已经将这里的await属性设置为true,所以需要进入等待状态。进入等待状态会调用await和stop两个方法,await方法会直接调用Server的await方法,Server的await方法内部会执行一个while循环,这样程序就停到了await方法,当await方法里的while循环退出时,就会执行stop方法,从而关闭服务器。

代码如下:

// org.apache.catalina.core.StandardServer
@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;
                long acceptStartTime = System.currentTimeMillis();
                try {
                    socket = serverSocket.accept();
                    socket.setSoTimeout(10 * 1000);  // Ten seconds
                    stream = socket.getInputStream();
                } catch (SocketTimeoutException ste) {
                    // This should never happen but bug 56684 suggests that
                    // it does.
                    log.warn(sm.getString("standardServer.accept.timeout",
                                Long.valueOf(System.currentTimeMillis() - acceptStartTime)), ste);
                    continue;
                } 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;
                    }
                    // Control character or EOF (-1) terminates loop
                    if (ch < 32 || ch == 127) {
                        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
            }
        }
    }
}

--------------------------待续------------------------

参考书籍

[看透springMvc源代码分析与实践.pdf](链接:http://pan.baidu.com/s/1o7Zp1Q6 密码:c87j)

推荐博客

解析XML之Digester


关注我的微信公众号:FramePower
我会不定期发布相关技术积累,欢迎对技术有追求、志同道合的朋友加入,一起学习成长!


Tomcat源码分析之启动流程分析_第4张图片
微信公众号

你可能感兴趣的:(Tomcat源码分析之启动流程分析)