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

文章目录
  1. 1. Tomcat源码调试环境准备
  2. 2. Tomcat Server的组成
    1. 2.1. 整体说明
    2. 2.2. 各组件详解
  3. 3. 源码分析
    1. 3.1. 启动总体流程
    2. 3.2. 启动流程详解
  • 参考书籍
  • 推荐博客
  • 前面的博客介绍了Tomcat的整个架构及各个配置文件及目录的作用,接下来就对Tomcat的源码进行分析了。

    Tomcat源码调试环境准备

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

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

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

    1
    2
    3
    4
    5
    6
    -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整体结构

    各组件详解

    可结合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各组件关系图(此图来此网上)

      QQ截图20170913174040

    源码分析

    启动总体流程

    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启动流程分析

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

    启动流程详解

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    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的相应方法。

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    // 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方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    // 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方法,从而关闭服务器。

    代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    // 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

    推荐博客

    解析XML之Digester

    如果文章对你有帮助,欢迎点击上方按钮打赏作者