Tomcat生命周期(Tomcat源码阅读系列之三)

本文是Tomcat源码阅读系列的第三篇,在前两篇文章中我们分别介绍了如何搭建Tomcat源码分析环境和从整体上分析了Tomcat的整体架构,在阅读本文之前,建议先去阅读前两篇文章,并对照源码一步步跟着进行分析,欢迎Email分享交流。
Tomcat源码分析环境搭建
Tomcat整体架构

前言

在上篇中我们提到了StandardServer 继承LifecycleMBeanBase 的目的是与Tomcat的生命周期的管理有关,在本文我们将会从Tomcat的生命周期管理、Tomcat的启动过程、Tomcat的关闭过程三个方面对Tomcat的生命周期进行阐述。

Tomcat生命周期管理

Tomcat整体架构 中,我们都知道Tomcat中最顶层的容器叫Server,代表着整个服务器,而Tomcat里的Server有org.apache.catalina.startup.Catalina 来管理,Catalina是整个Tomcat的管理类,它里面的三个方法load、start、stop分别用于管理整个服务器的生命周期,这三个方法会按照容器的结构逐层调用相应方法,如Server的start方法中会调用所有的Service的start的start方法,Service中的start方法又会调用所有包含的Connectors和Container的start方法,这样整个服务器就启动了。下面我们将来分析这样做的原因。
Tomcat整体架构这篇文章中列出的组件的继承结构类图都有一个共同之处:它们都继承org.apache.catalina.util.LifecycleMBeanBase,而LifecycleMBeanBase又继承自org.apache.catalina.util.LifecycleBase,LifecycleBase实现了org.apache.catalina.Lifecycle接口。

首先我们来看看Lifecycle接口,其类图如下:

Lifecycle类图

从上图我们可以清楚地看到,Lifecycle主要有四个生命周期:init(初始化)、start(启动)、stop(停止)、destroy(销毁),知道Lifecycle的四个生命周期之后,接下来我们来看看它的实现类LifecycleBase,其类图如下:

LifecycleBase

从上面的类图或许我们无法看出些什么,我们再来看看生命周期的其中一个方法init是如何实现的,其代码如下:

public final synchronized void init() throws LifecycleException {
   //1
   if (!state.equals(LifecycleState.NEW)) {
       invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
   }
   setStateInternal(LifecycleState.INITIALIZING, null, false);

   try {
      //2
      //钩子方法
      initInternal();
   } catch (Throwable t) {
      ExceptionUtils.handleThrowable(t);
      setStateInternal(LifecycleState.FAILED, null, false);
      throw new LifecycleException(
            sm.getString("lifecycleBase.initFail",toString()), t);
   }
   //3
   setStateInternal(LifecycleState.INITIALIZED, null, false);
}
protected abstract void initInternal() throws LifecycleException;

从上面的代码,我们可以看出LifecycleBase在实现init方法时,在里面调用initInternal 模板方法,为此我们推测 LifecycleBase采用模板方法模式来对所有支持生命周期管理的组件的生命周期各个阶段进行了总体管理,每个需要生命周期管理的组件只需要继承这个基类,然后覆盖对应的钩子方法即可完成相应的声明周期阶段的管理工作,我们的推测是否正确,接下来请跟着我们的脚步,一步步揭开它神秘的面纱。

首先我们还是先从LifecycleBaseinit 方法开始分析,我们将逐一分析上述代码中标注了数字的地方:
1. 标注1处,首先检测当前组件的状态是不是New(新建)状态,如果不是则调用org.apache.catalina.util.LifecycleBase#invalidTransition 方法将当前的状态转换过程终止,并抛出org.apcache.catalina.LifecycleException异常,如果是则调用setStateInternal方法将状态设置为INITIALIZING(初始化中)。
2. 标注2处的代码是init的模板方法,由子类实现,从而纳入初始化的流程
3. 标注3的代码是将组件的状态设置为INITIALIZED(已初始化)

分析完init方法,接下来我们再看看start方法又是如何实现的。其代码如下:

public final synchronized void start() throws LifecycleException {
    //1
    if (LifecycleState.STARTING_PREP.equals(state) ||
            LifecycleState.STARTING.equals(state) ||
            LifecycleState.STARTED.equals(state)) {

        if (log.isDebugEnabled()) {
            Exception e = new LifecycleException();
            log.debug(sm.getString("lifecycleBase.alreadyStarted",
                toString()), e);
        } else if (log.isInfoEnabled()) {
            log.info(sm.getString("lifecycleBase.alreadyStarted",
                        toString()));
        }

        return;
    }
    //2 
    if (state.equals(LifecycleState.NEW)) {
       init();
    } else if (state.equals(LifecycleState.FAILED)){
       stop();
    } else if (!state.equals(LifecycleState.INITIALIZED) &&
       !state.equals(LifecycleState.STOPPED)) {
       invalidTransition(Lifecycle.BEFORE_START_EVENT);
    }
    //3
    setStateInternal(LifecycleState.STARTING_PREP, null, false);

    try {
       //4
       startInternal();
    } catch (Throwable t) {
       ExceptionUtils.handleThrowable(t);
       setStateInternal(LifecycleState.FAILED, null, false);
       throw new LifecycleException(
           sm.getString("lifecycleBase.startFail",toString()), t);
    }
    //5
    if (state.equals(LifecycleState.FAILED) ||
       state.equals(LifecycleState.MUST_STOP)) {
       stop();
    } else {
       //6
       if (!state.equals(LifecycleState.STARTING)) {
           invalidTransition(Lifecycle.AFTER_START_EVENT);
       }
       //7
       setStateInternal(LifecycleState.STARTED, null, false);
    }
}

同样,我们将逐一分析上述代码中标注了数字的地方:
1. 标注1的代码是检测当前的状态是不是STARTING_PREP(准备启动)、STARTING(正在启动)、STARTED(已启动)中的任意一个,如果是则抛出异常LifecycleException
2. 标注2的检查主要是为了保证组件状态的完整性,在正常的启动过程中,应该是不会出现没有初始化就启动,或者还启动就失败的情况发生的。
3. 标注3是将组件的状态设置为STARTING_PREP(准备启动)状态
4. 标注4是start的模板方法的钩子方法,子类通过实现该方法来纳入组件启动的流程中
5. 标注5和标注6都是状态检查
6. 标注7是将组件的状态设置为STARTED(已启动)状态。

至此,我们已经分析了init和start方法,至于stop和destroy方法,其基本流程与我们已经分析的init和start方法的总体流程是类似的,大家可以按照Tomcat源码分析环境搭建所教的方法搭建Tomcat源码分析环境自行阅读一下,在此就不再详述。如果大家有把生命周期的方法都阅读分析了,大家可能会发现生命周期方法的一个公共的流程:
1. 对组件状态进行检查
2. 设置组件状态为准备进入相应声明周期的状态
3. 调用相应的生命周期的模板方法的实现
4. 设置组件状态为进入相应生命周期后的状态

伪代码进行表示为:

public final void lifecycleMethod() throws LifecycleException {
  //检查组件的状态
  statteCheck();
  //设置组件状态为准备进入相应生命周期的状态
  setStateInternal(LifecycleState.BEFORE_STATE,null,false);
  //调用相应的生命周期的模板方法的实现
  lifecycleMethodInternal();
  //设置组件状态为进入相应生命周期后的状态
  setStateInternal(LifecycleState.AFTER_STATE,null,false);
}

小结Tomcat生命周期的管理

Lifecycle接口定义了Tomcat的四个生命周期:init(初始化)、start(启动)、stop(停止)、destroy(销毁),而 LifecycleBase采用模板方法模式来对所有支持生命周期管理的组件的生命周期各个阶段进行了总体管理,每个有生命周期的组件都直接或间接继承于LifecycleBase类,并实现其中的生命周期的模板方法以加入相应生命周期的流程中。而Tomcat里的Server有org.apache.catalina.startup.Catalina 来管理,Catalina是整个Tomcat的管理类,它里面的三个方法load、start、stop分别用于管理整个服务器的生命周期,这三个方法会按照容器的结构逐层调用相应方法,因为有生命周期的组件都实现了Lifecycle接口。这就是Tomcat生命周期的管理方式。

Tomcat的启动过程

Tomcat的主体是由Java语言实现,即可以简单地认为Tomcat是一个Java程序。我们都知道Java程序都有一个main函数作为程序启动的入口,而Tomcat的程序启动入口是org.apache.catalina.startup.Bootstrap#main,接下来我们将一步步揭开Tomat是如何一步步启动起来的。首先我们先来看看程序启动入口BootStrap#main,其代码如下:

public static void main(String args[]) {

   if (daemon == null) {
      // Don't set daemon until init() has completed
      //1
      Bootstrap bootstrap = new Bootstrap();
      try {
          //2
          bootstrap.init();
      } catch (Throwable t) {
          handleThrowable(t);
          t.printStackTrace();
          return;
      }
      //3
      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")) {
         //4
         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);
   }
}

同样我们来逐一分析上面代码标注的地方:
1. 标注1创建自身的实例
2. 标注2对Bootstrap实例进行了初始化
3. 标注3的代码将实例赋值给daemon
4. 标注4的代码对于命令行中的start命令,依次调用setAwait(true)、load(args)和start三个方法,这三个方法内部使用反射机制都调用了Catalina的相应方法进行具体执行。

接下来我们将分别分析Bootstrap的init、load和start三个方法具体都做了哪些工作。

BootStrap#init方法

首先我们来看看org.apache.catalina.startup.BootStrap#init方法,其代码如下:

public void init() throws Exception {

   //1
   setCatalinaHome();
   setCatalinaBase();
   //2
   initClassLoaders();
   //3
   Thread.currentThread().setContextClassLoader(catalinaLoader);

   SecurityClassLoad.securityClassLoad(catalinaLoader);

   // Load our startup class and call its process() method
   if (log.isDebugEnabled())
       log.debug("Loading startup class");
   //4
   Class<?> startupClass =
          catalinaLoader.loadClass
            ("org.apache.catalina.startup.Catalina");
   Object startupInstance = startupClass.newInstance();

   // Set the shared extensions class loader
   if (log.isDebugEnabled())
       log.debug("Setting startup class properties");
   //5
   String methodName = "setParentClassLoader";
   Class<?> paramTypes[] = new Class[1];
   paramTypes[0] = Class.forName("java.lang.ClassLoader");
   Object paramValues[] = new Object[1];
   paramValues[0] = sharedLoader;
   Method method =
       startupInstance.getClass().getMethod(methodName, paramTypes);
   method.invoke(startupInstance, paramValues);
   //6
   catalinaDaemon = startupInstance;

}

下面我们将重点分析上述代码中标注的地方:
1. 标注1的代码设置Catalina的工作目录,如果没有设置则默认使用当前目录作为Catalina的工作目录
2. 标注2初始化了Tomcat的类加载器
3. 标注3设置catalinaClassloader类加载器为当前Tomcat的类加载器
4. 标注4使用CatalinaClassLoader类加载器创建了Catalina实例
5. 标注5的代码调用了Catalina实例的setParentClassLoader方法设置了父类ClassLoader为java.lang.ClassLoader
6. 标注6将Catalina实例赋值给Bootstrap实例的catalinaDaemon

Bootstrap#load方法

接下来我们看看Bootstrap#load方法,其代码如下:

private void load(String[] arguments) throws Exception {

    // Call the load() method
    String methodName = "load";
    Object param[];
    Class<?> paramTypes[];
    if (arguments==null || arguments.length==0) {
       paramTypes = null;
       param = null;
    } else {
       paramTypes = new Class[1];
       paramTypes[0] = arguments.getClass();
       param = new Object[1];
       param[0] = arguments;
    }
    Method method =
        catalinaDaemon.getClass().getMethod(methodName, paramTypes);
    if (log.isDebugEnabled())
       log.debug("Calling startup class " + method);
    method.invoke(catalinaDaemon, param);
}

上述的代码功能非常简单,使用反射机制调用了org.apache.catalina.startup.Catalina#load方法,那么我们来看看Catalina的load方法,其代码(代码有所删减,保留了主体代码)如下:

public void load() {
        //1
        Digester digester = createStartDigester();

        InputSource inputSource = null;
        InputStream inputStream = null;
        File file = null;
        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 {
            try {
                inputStream.close();
            } catch (IOException e) {
                // Ignore
            }
        }

        getServer().setCatalina(this);

        // Stream redirection
        initStreams();

        // Start the new server
        try {
            //2
            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");
        }

    }

下面我们将逐一分析上述代码标注的地方:
1. 标注1的代码是创建Digester实例,解析conf/server.xml文档,创建Server对象
2. 标注2调用server的初始化方法进行初始化,最终调用StandardServer的init方法。

BootStrap#start方法

接着我们继续查看Bootstrap#start方法,其代码如下:

public void start() {
    //1
    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 {
       //2
       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");
    }

    //3
    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);
        }
    }
    //4
    if (await) {
      await();
      stop();
    }
}

同样我们来逐一分析标注的地方:
1. 标注1的代码判断Server是否已经创建,如果没有则调用load方法进行创建。
2. 标注2的代码调用Server的start方法启动服务器,最终实际调用的是StandardServer的start方法
3. 标注3的代码注册关闭钩子
4. 标注4根据await的标志,决定是否让程序进入等待状态。

如果大家有一步步debug,阅读源代码时,会发现在StandardEngine#init 方法并没有进行StandardHost的初始化,而且查看StandardHost 的源码时发现StandardHost 并没有重写initInternal,只是重写了startInternal方法,难道说是StandardHost 并没有进行初始化就可以直接调用startInternal 方法启动啦?可按照生命周期的说法,是有初始化的,而且是init方法调用initInternal钩子方法,既然有初始化又是什么时候在哪里进行初始化的呢?对于这个问题笔者一时也没有任何思路,于是在调试了多次无果的情况下,笔者将问题暂时放下了,可当笔者继续调试阅读源码代码,直到调试到StandardEngine#startInternal方法时它有使用异步线程池来启动它的子容器,而Engine正是它的子容器,于是笔者推测StandardHost 的初始化是在调用start()方法时进行的,可是对于这个推测,我们又该如何去验证呢?熟悉IDE编辑器Debug模式的同学知道,当设置了断点,并运行到断点的位置时,我们可以看到线程调用栈的情况,通过查阅资料,笔者了解到可以通过Throwable获取到方法调用栈的信息,有了这个思路,笔者修改了一下StandardHost类,重写了initInternal方法,并在重写的方法中输出调用栈的信息,其重写的initInternal方法如下:

@Override
protected void initInternal() throws LifecycleException {
    Throwable ex = new Throwable();
    StackTraceElement[] stackElements = ex.getStackTrace();
    if(stackElements != null) {
        for(int i = stackElements.length-1;i>0;i--) {
            StackTraceElement stackElement = stackElements[i];
            System.out.print(stackElement.getClassName()+"\t");
            System.out.print(stackElement.getFileName()+"\t");
            System.out.print(stackElement.getMethodName()+"\t");
            System.out.println(stackElement.getLineNumber());
        }
    }
    super.initInternal();
}

当再次运行时程序时,控制台输出如下信息:

2016-8-25 16:23:03 org.apache.catalina.core.StandardEngine startInternal
信息: Starting Servlet Engine: Apache Tomcat/@VERSION@
java.lang.Thread    Thread.java run 662
java.util.concurrent.ThreadPoolExecutor$Worker ThreadPoolExecutor.java run 918
java.util.concurrent.ThreadPoolExecutor$Worker ThreadPoolExecutor.java runTask 895
java.util.concurrent.FutureTask FutureTask.java run 138
java.util.concurrent.FutureTask$Sync   FutureTask.java innerRun    303
org.apache.catalina.core.ContainerBase$StartChild  ContainerBase.java  call    1
org.apache.catalina.core.ContainerBase$StartChild  ContainerBase.java  call    1559
org.apache.catalina.util.LifecycleBase  LifecycleBase.java  start   139
org.apache.catalina.util.LifecycleBase  LifecycleBase.java  init    102

通过上面的控制台的信息,我们看到的是StartChild#call方法调用的,而StartChild#call是StandardEngine#startInternal方法通过异步线程池启动子容器时调用的,也就是说init方法是在调用start方法时被调用的,因此我们的推测验证成功。

在此也总结一下当我们遇到不知道方法是什么时候被调用时,我们可以通过在控制台中打印调用栈信息来确定方法是何时被调用的,打印调用栈信息的方式请参考上面重写的initInternal方法。

既然我们已经知道StandardHost的初始化是调用start方法时进行的,那么我们再来看看StandardEngine#init到底都做了些什么工作呢?让我们来看一下StandardEngine#init 方法,其代码如下:

protected void initInternal() throws LifecycleException {
    // Ensure that a Realm is present before any attempt is made to start
    // one. This will create the default NullRealm if necessary.
    getRealm();
    super.initInternal();
}

从上面的代码我们可以看出StandardEngine#init除了获取安全域,就调用父类的initInternal方法,
而我们从StandardEngine 的继承类图:
StandardEngine继承类图
可以知道StandardEngine 的初始化钩子方法最终调用了ContainerBase 的initInternal方法,让我们再看看ContainerBase#initInternal 方法,其代码如下:

protected void initInternal() throws LifecycleException {
    BlockingQueue<Runnable> startStopQueue = new LinkedBlockingQueue<Runnable>();
    startStopExecutor = new ThreadPoolExecutor(
             getStartStopThreadsInternal(),
             getStartStopThreadsInternal(), 10, TimeUnit.SECONDS,
             startStopQueue,
             new StartStopThreadFactory(getName() + "-startStop-"));
    startStopExecutor.allowCoreThreadTimeOut(true);
    super.initInternal();
}

从上面的代码我们可以看到StandardEngine 的初始化仅仅是创建了一个ThreadPoolExecutor,这个ThreadPoolExecutor是用于异步启动子容器的。

按照上面的debug思路,我们能够得到以下的调用链:

org.apache.catalina.startup.Bootstrap#main
->org.apache.catalina.startup.Bootstrap#init
->org.apache.catalina.startup.Bootstrap#load
-->org.apache.catalina.startup.Catalina#load 通过反射机制调用
--->org.apache.catalina.core.StandardServer#init
---->org.apache.catalina.core.StandardService#init
----->org.apache.catalina.core.StandardEngine#init
----->org.apache.catalina.connector.Connector#init
->org.apache.catalina.startup.Bootstrap#start
-->org.apache.catalina.startup.Catalina#start 通过反射机制调用
--->org.apache.catalina.core.StandardServer#start
---->org.apache.catalina.core.StandardService#start
----->org.apache.catalina.core.StandardEngine#start
------>org.apache.catalina.Executor#start
------->org.apache.catalina.core.StandardHost#init
------->org.apache.catalina.core.StandardHost#start
----->org.apache.catalina.connector.Connector#start

至此,经过上面的分析,我们已经基本弄清楚Tomcat大体启动过程,下面我们小结一下Tomcat大体的启动过程:
Tomca启动从org.apache.catalina.startup.Bootstrap#main方法开始,由于Catalina是Tocmat生命周期的管理类,通过init、load、start三个方法管理整个生命周期,在BootStrap#main 创建Catalina实例后依次调用load、start方法,load方法按照Tomcat整体的架构的容器层次从顶层往内依次调用相应的init方法,而start方法与之类似,只是调用的是start方法,但需要注意的是Host层的启动是在其父层Engine调用start方法时,使用异步线程池进行初始化和启动的。

虽然说我们已经基本弄清楚Tomcat的大体启动过程,但是对于Tomcat的主要组件的启动过程我们还没有深入地进行分析,因此,下面我们将按照容器层次的顺序逐一分析Tomcat主要组件的启动过程。

Connector启动过程

由于Connector也实现了Lifecycle接口,所以也满足Tomcat的生命周期管理方式,因而我们只需要分析ininInternal 就可以知道Connector是如何进行初始化的,其代码如下:

protected void initInternal() throws LifecycleException {

     super.initInternal();

     //1
     adapter = new CoyoteAdapter(this);
     protocolHandler.setAdapter(adapter);

     // Make sure parseBodyMethodsSet has a default
     if( null == parseBodyMethodsSet ) {
        setParseBodyMethods(getParseBodyMethods());
     }

     if (protocolHandler.isAprRequired() &&
           !AprLifecycleListener.isAprAvailable()) {
         throw new LifecycleException(
              sm.getString("coyoteConnector.protocolHandlerNoApr",
                  getProtocolHandlerClassName()));
     }

     try {
         //2
         protocolHandler.init();
     } catch (Exception e) {
         throw new LifecycleException
             (sm.getString
             ("coyoteConnector.protocolHandlerInitializationFailed"), e);
     }

     // Initialize mapper listener
     mapperListener.init();
}

上述标注1的代码是为了初始化coyoteAdaptor,它是用于连接Connector的Container的桥梁,标注2的代码是调用org.apache.ProtocolHandler#init方法,而ProtocolHandler是在Connector的构造方法被创建的,而Connector的构造方法是在Digester解析conf/server.xml文件时调用的,接下来我们来看看Connector构造器中的setProtocol方法,其代码如下:

public void setProtocol(String protocol) {

    if (AprLifecycleListener.isAprAvailable()) {
    //使用APR时的情况
       if ("HTTP/1.1".equals(protocol)) {
             setProtocolHandlerClassName
                ("org.apache.coyote.http11.Http11AprProtocol");
       } else if ("AJP/1.3".equals(protocol)) {
             setProtocolHandlerClassName
                ("org.apache.coyote.ajp.AjpAprProtocol");
       } else if (protocol != null) {
             setProtocolHandlerClassName(protocol);
       } else {
             setProtocolHandlerClassName
               ("org.apache.coyote.http11.Http11AprProtocol");
       }
    } else {
         //默认调用
         if ("HTTP/1.1".equals(protocol)) {
              setProtocolHandlerClassName
                    ("org.apache.coyote.http11.Http11Protocol");
         } else if ("AJP/1.3".equals(protocol)) {
              setProtocolHandlerClassName
                    ("org.apache.coyote.ajp.AjpProtocol");
         } else if (protocol != null) {
                setProtocolHandlerClassName(protocol);
         }
    }
}

从上面代码我们可以看到,ProtocolHandler分为两大类,第一类是使用APR,第二类是不使用APR,默认是不使用APR,根据协议类型的不同初始化不同的类,如果是HTTP/1.1 协议则是初始化org.apache.coyote.http11.Http11Protocol类,是AJP/1.3 协议则初始化org.apache.coyote.ajp.AjpProtocol,下面我们来看看Http11Protocol和AJPProtocol的继承类图:

Http11Protocol
Http11Protocol继承类图

AJPProtocol继承类图
AJPProtocol继承类图

从上面的类图我们看到,它们都继承自org.apache.coyote.AbstractProtocol,我们再查看源码时发现它们都没有重写AbstractProtocol#init 方法,换句话说它们初始化时都是调用AbstractProtocol#init 方法,让我们来看看AbstractProtocol#init 方法,其代码(省略非本文关心的内容)如下:

public void init() throws Exception {
    String endpointName = getName();
    endpoint.setName(endpointName.substring(1, endpointName.length()-1));

    try {
       endpoint.init();
    } catch (Exception ex) {
         getLog().error(sm.getString("abstractProtocolHandler.initError",
             getName()), ex);
         throw ex;
    }
}

从上面我们可以看到,该方法调用了endpoint#init,而最终调用的是org.apache.tomcat.util.net.AbstractEndPoint#init方法, 而我们在查看Http11ProtocolAjpProtocol源码时发现,AbstractEndpoint是在它们的构造器中实例化的,也就是说AbstractEndpoint是在Http11ProtocolAjpProtocol实例化时进行实例化的,而且都是实例化org.apache.tomcat.util.net.JIOEndpoint,不过两者实例化的连接处理类不同,Http11Protocol实例化Http11ConnectionHandler,而AjpProtocol实例化的是AjpConnectionHandler,它们都是各自的静态内部类。

接下来我们再来看看,JIOEndpoint的初始化过程,先来看看它的继承类图:

JIOEnpoint继承类图

通过查看源码我们发现JIOEndpoint并没有重写init方法,也就是说它调用了父类的init方法,其代码如下:

public abstract void bind() throws Exception;
public final void init() throws Exception {
   if (bindOnInit) {
      bind();
      bindState = BindState.BOUND_ON_INIT;
   }
}

从上面我们发现init方法调用了bind方法,而bind是一个抽象方法,由子类实现,即JIOEndpoint实现,我们再来看看JIOEnpoint是如何实现该方法的,其代码如下:

public void bind() throws Exception {

     // Initialize thread count defaults for acceptor
     if (acceptorThreadCount == 0) {
        acceptorThreadCount = 1;
     }
     // Initialize maxConnections
     if (getMaxConnections() == 0) {
        // User hasn't set a value - use the default
        setMaxConnections(getMaxThreadsExecutor(true));
     }

     if (serverSocketFactory == null) {
         if (isSSLEnabled()) {
            serverSocketFactory =
                  handler.getSslImplementation().getServerSocketFactory(this);
         } else {
            serverSocketFactory = new DefaultServerSocketFactory(this);
         }
     }

     if (serverSocket == null) {
        try {
           //绑定端口
           if (getAddress() == null) {
              serverSocket = serverSocketFactory.createSocket(getPort(),
                     getBacklog());
           } else {
               serverSocket = serverSocketFactory.createSocket(getPort(),
                      getBacklog(), getAddress());
           }
         } catch (BindException orig) {
                String msg;
                if (getAddress() == null)
                    msg = orig.getMessage() + " <null>:" + getPort();
                else
                    msg = orig.getMessage() + " " +
                            getAddress().toString() + ":" + getPort();
                BindException be = new BindException(msg);
                be.initCause(orig);
                throw be;
         }
      }
}

通过上面的代码我们可以看出,该方法主要是调用org.apache.tomcat.util.net.ServerSocketFactory#createSocket方法创建java.next.ServerSocket,并绑定在conf/server.xml配置文件配置的端口上。至此,我们已基本搞清楚Connector的初始化过程,下面小结一下:

Connector初始化时会初始化默认配置的AJP和HTTP/1.1协议,而这两者在初始化时都是在JIOEndpoint的初始化,默认情况下会创建一个ServerSocket绑定在配置的端口上。

接下来我们再来看看Connector的启动过程,同样,我们需要分析startInternal方法,其代码如下:

protected void startInternal() throws LifecycleException {

     // Validate settings before starting
     if (getPort() < 0) {
         throw new LifecycleException(sm.getString(
              "coyoteConnector.invalidPort", Integer.valueOf(getPort())));
     }

     setState(LifecycleState.STARTING);

     try {
       //1
       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);
     }
     //2
     mapperListener.start();
}

通过上面的代码,该方法主要调用ProtocolHandler#start 方法,而由我们上面Connector#init的分析,可以知道不管是HTTP协议还是AJP协议的初始化最终调JIOEndPoint 的初始化,所以下面我们来看看JIOEndpoint#start 方法,其代码如下:

public void startInternal() throws Exception {

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

       //创建用于处理请求的工作线程
       if (getExecutor() == null) {
          createExecutor();
       }
       //初始化最大连接数
       initializeConnectionLatch();
       //1
       startAcceptorThreads();

       // Start async timeout thread
       //2
       Thread timeoutThread = new Thread(new AsyncTimeout(),
              getName() + "-AsyncTimeout");
       timeoutThread.setPriority(threadPriority);
       timeoutThread.setDaemon(true);
       timeoutThread.start();
     }
}
protected final void startAcceptorThreads() {
    int count = getAcceptorThreadCount();
    //count的值为1,即创建一个Acceptor线程
    acceptors = new Acceptor[count];

    for (int i = 0; i < count; i++) {
        acceptors[i] = createAcceptor();
        String threadName = getName() + "-Acceptor-" + i;
        acceptors[i].setThreadName(threadName);
        Thread t = new Thread(acceptors[i], threadName);
        t.setPriority(getAcceptorThreadPriority());
        t.setDaemon(getDaemon());
        t.start();
    }
}

从上面的代码我们看到JIOEndpoint#startInternal 方法创建了用于处理具体请求的线程池和启动了两个线程:Acceptor和AsyncTimeout线程,并且设置Daemon线程,这样做有何目的呢?在此我们先跳过这个问题,在下面的Tomcat的关闭中会进行说明,下面我们来看看Acceptor 线程和AsyncTimeout线程都做了哪些工作,其代码(省略异常处理的代码)如下:

protected class AsyncTimeout implements Runnable {

   //用于检查异步请求并将超时不活动的请求释放
   @Override
   public void run() {
      // 一直执行直到接收到关闭的命令
      while (running) {
         try {
            Thread.sleep(1000);
         } catch (InterruptedException e) {
             // Ignore
         }
         long now = System.currentTimeMillis();
         Iterator<SocketWrapper<Socket>> sockets =
                waitingRequests.iterator();
         while (sockets.hasNext()) {
             SocketWrapper<Socket> socket = sockets.next();
             long access = socket.getLastAccess();
             if (socket.getTimeout() > 0 &&
                   (now-access)>socket.getTimeout()) {
                 processSocketAsync(socket,SocketStatus.TIMEOUT);
             }
        }

        // Loop if endpoint is paused
        while (paused && running) {
           try {
              Thread.sleep(1000);
           } catch (InterruptedException e) {
              // Ignore
        }
      }
     }
   }
}
//用于监听连接进来的TCP/IP连接,并且将它们提交给相关的处理器进行处理
protected class Acceptor extends AbstractEndpoint.Acceptor {

   @Override
   public void run() {
       int errorDelay = 0;

       //一直执行,直到接收到关闭的命令
       while (running) {

          // Loop if endpoint is paused
          while (paused && running) {
              state = AcceptorState.PAUSED;
              try {
                 Thread.sleep(50);
              } catch (InterruptedException e) {
                 // Ignore
              }
          }

          if (!running) {
              break;
          }
          state = AcceptorState.RUNNING;

          try {
             //达到最大连接数时,阻塞等待
             countUpOrAwaitConnection();

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

             //配置Socket
             if (running && !paused && setSocketOptions(socket)) {
                // 将该连接提交给相关联的处理器进行处理
                 if (!processSocket(socket)) {
                    countDownConnection();
                    // 关闭Socket
                    closeSocket(socket);
                 }
              } else {
                 countDownConnection();
                 // Close socket right away
                 closeSocket(socket);
              }
           } catch (IOException x) {
               //省略异常处理代码
        }
        state = AcceptorState.ENDED;
     }
}

从上面的代码我们可以知道,Acceptor线程用于监听到达的TCP/IP连接,并将它们提交给相关联的处理器进行处理,AsyncTimeout线程用于监听异步连接,并且将超时非活动的异步请求结束,基于具体请求处理过程和异步请求的处理过程,由于不是本文所关注的重点,在此不作详细的说明,往后会有相关的文章进行说明。好了,至此我们已基本弄清楚Connector的启动过程,下面我们来小结一下:

Connector在启动时,会为AJP和HTTP/1.1协议分别启动Daemon线程,Acceptor线程和AsyncTimeout线程,Acceptor线程用于监听新到达的TCP/IP连接,并将它们提交给相关联的处理器进行具体的请求处理,而AsyncTimeout线程用于监听异步连接,并结束超时非活动的异步请求。

Engine启动过程

在上面我们已经分析过了Engine的初始化过程,它的initInternal方法调用了父类的InitInternal方法,创建了一个ThreadPoolExecutor,用于异步启动子容器的。接着我们再来看看Engine的启动过程,同样我们需要查看的是initInternal方法,其代码如下:

@Override
protected synchronized void startInternal() throws LifecycleException {

    // Log our server identification information
    if(log.isInfoEnabled())
       log.info( "Starting Servlet Engine: " + ServerInfo.getServerInfo());
    // Standard container startup
    super.startInternal();
}

我们可以看到,StandardEngine#startInternal方法仅仅是调用了父类的startInternal方法,而由Tomcat整体架构的文章中我们知道,StandardEngine继承自ContainerBase,因此,我们来看看ContainerBase#startInternal方法,其代码如下:

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 ((manager != null) && (manager instanceof Lifecycle))
       ((Lifecycle) manager).start();
    if ((cluster != null) && (cluster instanceof Lifecycle))
       ((Lifecycle) cluster).start();
    Realm realm = getRealmInternal();
    if ((realm != null) && (realm instanceof Lifecycle))
       ((Lifecycle) realm).start();
    if ((resources != null) && (resources instanceof Lifecycle))
       ((Lifecycle) resources).start();

    Container children[] = findChildren();
    List<Future<Void>> results = new ArrayList<Future<Void>>();
    for (int i = 0; i < children.length; i++) {
        //异步线程池启动子容器
        results.add(startStopExecutor.submit(new StartChild(children[i])));
    }

    boolean fail = false;
    for (Future<Void> result : results) {
       try {
          result.get();
       } catch (Exception e) {
           log.error(sm.getString("containerBase.threadedStartFailed"), e);
           fail = true;
       }
    }
    if (fail) {
       throw new LifecycleException(
            sm.getString("containerBase.threadedStartFailed"));
    }

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

    setState(LifecycleState.STARTING);

    //启动后台线程用于周期性检查Session的超时情况
    threadStart();
}

从上面我们可以看到startInternal方法主要通过异步线程池启动子容器,并且启动线程后台线程后于周期性检查Session的超时情况。我们再来看看异步线程池启动子容器,我们从上面可以看到,子容器是被包装在StartChild中提交给线程池的,因此,我们来看看StartChild#call方法,其代码如下:

@Override
public Void call() throws LifecycleException {
    child.start();
    return null;
}

从上面我们可以看出,call方法只是简单地调用了子容器的start方法,这样做有何好处呢?首先通过异步线程池来启动子容器可以减少服务器的启动时间,同时我们也看到在startInternal方法中使用future.get()方法来获取子容器的启动结果,而当子容器没有启动完成或者抛出异常是,get()是会被阻塞的,因此这样做能够保证所有的子容器都启动完毕后才进行下一步。好了,到了这,我们已基本理清Engine的启动过程,下面来小结一下:

Engine的初始化过程会创建一个ThreadPoolExecutor,用于在启动过程中异步启动子容器,这样能够减少服务器启动的时间,同时使用Future#get方法来保证所有子容器都启动完毕后才进行下一步。

Host启动过程

从上面我们已经知道Host在启动时才被初始化的,只是调用了父类ContainerBase#initInternal方法,创建一个线程池,用于启动子容器,下面我们再来看看Host的启动,同样需要查看startInternal方法,其代码如下:

@Override
protected synchronized void startInternal() throws LifecycleException {

    // Set error report valve
    String errorValve = getErrorReportValveClass();
    if ((errorValve != null) && (!errorValve.equals(""))) {
       try {
          boolean found = false;
          Valve[] valves = getPipeline().getValves();
          for (Valve valve : valves) {
             if (errorValve.equals(valve.getClass().getName())) {
                found = true;
                break;
             }
          }
          if(!found) {
              Valve valve = (Valve) Class.forName(errorValve).newInstance();
              getPipeline().addValve(valve);
          }
       } catch (Throwable t) {
             ExceptionUtils.handleThrowable(t);
            log.error(sm.getString(
                 "standardHost.invalidErrorReportValveClass",
                    errorValve), t);
       }
    }
    super.startInternal();
}

我们可以看到,除了错误的信息之外,就调用了父类ContainerBase#StartInternal方法,也就是说,Host也是使用异步线程池启动子容器。

Host的初始化过程是调用父类ContainerBase#initInternal方法创建线程池,而启动过程则是使用异步线程池启动子容器。

Context启动过程

我们都知道webapps目录下存放着我们开发的应用,那么他们是如何启动的呢?下面我们来探探其内幕。通过Tomcat整体结构我们知道每个应用对应一个Context,那么我们使用上面所介绍的打印出调用栈信息的方式,看看StandardContext#initInternal方法是何时被调用的,控制台输出以下信息:

2016-8-31 21:03:21 org.apache.catalina.startup.HostConfig deployDirectory
信息: Deploying web application directory F:\Workspaces\Eclipse\tomcat-7.0.x\output\build\webapps\ROOT
java.lang.Thread    Thread.java run 662
java.util.concurrent.ThreadPoolExecutor$Worker ThreadPoolExecutor.java run 918
java.util.concurrent.ThreadPoolExecutor$Worker ThreadPoolExecutor.java runTask 895
java.util.concurrent.FutureTask FutureTask.java run 138
java.util.concurrent.FutureTask$Sync   FutureTask.java innerRun    303
java.util.concurrent.Executors$RunnableAdapter Executors.java  call    439
org.apache.catalina.startup.HostConfig$DeployDirectory HostConfig.java run 1671
org.apache.catalina.startup.HostConfig  HostConfig.java deployDirectory 1113
org.apache.catalina.core.StandardHost   StandardHost.java   addChild    633
org.apache.catalina.core.ContainerBase  ContainerBase.java  addChild    877
org.apache.catalina.core.ContainerBase  ContainerBase.java  addChildInternal    901
org.apache.catalina.util.LifecycleBase  LifecycleBase.java  start   139
org.apache.catalina.util.LifecycleBase  LifecycleBase.java  init    102

从上面的调用栈信息,我们可以知道看到一个org.apache.catalina.core.startup.HostConfig$DeployDirectory,我们进入该类发现实现了Runnable接口,应该是用于线程执行的,而我们通过Eclipse的调用栈分析工具(Alt+Shift+H)可以获得DeployDirectory构造方法的调用情况:

DeployDirectory

我们从上面可以看到DeployDirectory最终被HostConfig#lifecycleEvent方法调用,也就是说Context是被某个组件生命周期事件的监听器所启动的,根据类的名称,我们推断是Host组件,那么它监听哪个事件呢?我们来看看lifecycleEvent方法,其代码如下:

@Override
public void lifecycleEvent(LifecycleEvent event) {

    // 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.PERIODIC_EVENT)) {
       check();
    } else if (event.getType().equals(Lifecycle.START_EVENT)) {
        start();
    } else if (event.getType().equals(Lifecycle.STOP_EVENT)) {
        stop();
    }
}

由上面我们可以知道监听事件是START_EVENT,通常查看LifecycleState可以知道STARTING(true, Lifecycle.START_EVENT),此时组件的状态为STARTING,我们回到ContainerBase#startInternal方法,有一句用于设置组件状态为setState(LifecycleState.STARTING);,因此我们猜测是StandardHost的生命周期方法的监听器,为了进一步验证该猜测,我们在StandardHost#startInternal方法中设置断点,可以得到下图:

HostConfigStart

因此我们的推测验证正确。下面我们小结一下webapps目录下的应用的启动。

webapps目录下应用的启动是在StandardHost设置状态STARTING时,通过Lifecycle.START_EVENT事件监听器HostConfig进行启动的。

至于Context在启动中做了哪些事,在此不打算进行深入的探索。

Tomcat的关闭

不知道大家是否还记得我们在上面仍留有一个问题没有解决,那就是为什么要把Acceptor线程和AsyncTimeout线程设置为Daemon线程。首先我们要明白一个概念:Daemon线程,又称后台线程或守护线程,负责在程序运行期提供一种通用服务的线程,比如垃圾收集线程,也就是说服务用户线程的,而用户线程就是我们普遍使用的线程,当JVM中不存在用户线程时,JVM就会退出,而Tomcat正是利用这一点来关闭服务器的。可能这样说,大家仍未理解这是怎么一回事,那么首先我们来带大家看看,当Tomcat启动后,到底存在哪些线程呢,如下图:

TomcatThreads

我们可以看到一共存在6条线程,只有main一条为非Daemon线程,也就是说只要关闭了mian线程,JVM就是kill掉其他的Daemon线程,Tomcat也就退出了,我们返回看看Catalina#start方法,其代码如下:

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);
   }
 }
 //2
 if (await) {
    await();
    stop();
 }

以上的两个标注就是Tomcat关闭的入口方法,首先我们来看看标注1,标注1的代码使用了JVM的关闭钩子的机制。那么何谓关闭钩子机制呢?首先我们要明白在Java中,虚拟机会对两类时间进行响应,然后执行关闭操作:

  • 当调用System.exit()方法或者程序的最后一个非守护进程线程退出时,应用程序正常退出。
  • 用户突然强制虚拟机中断运行,如用户按CTRL+C快捷键或在未关闭Java程序的情况下,从系统中退出。

而虚拟机在执行关闭操作时,会经过以下两个阶段:
1. 虚拟机启动所有已经注册的关闭钩子,如果有的话,关闭钩子是先前已经通过Runtime类注册的线程,所有的关闭钩子会并发执行,直到完成。
2. 虚拟机根据情况调用所有没有被调用过的终结器。

理解关闭钩子是怎么一回事后,我们再来看看CatalinaShutdownHook,其代码如下:

protected class CatalinaShutdownHook extends Thread {

     @Override
     public void run() {

        try {
          if (getServer() != null) {
             Catalina.this.stop();
          }
        } catch (Throwable ex) {
            ExceptionUtils.handleThrowable(ex);
            log.error(sm.getString("catalina.shutdownHookFail"), ex);
        } finally {
            // If JULI is used, shut JULI down *after* the server shuts down
            // so log messages aren't lost
            LogManager logManager = LogManager.getLogManager();
            if (logManager instanceof ClassLoaderLogManager) {
               ((ClassLoaderLogManager) logManager).shutdown();
            }
        }
    }
}

从上面的代码我们看到CatalinaShutdownHook只是创建了一个线程,并没有启动,也就是说虚拟机会在运行关闭序列时启动并执行关闭钩子,CatalinaShutdownHook调用了Catalina#stop方法,而Catalina#stop方法又调用了Serverstop方法和destroy方法。到此,我们知道Tomcat是利用虚拟机的关闭钩子机制在JVM关闭时关闭各个组件,那么问题又来了,JVM又是何时被关闭的呢?接下来我们再来看看标注2的代码,标注2的代码首先判断 await 是否为true,若为true时,调用awaitstop方法,而await方法调用了 StandardServer#await方法,其代码如下:

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 {
        //port的值为8005
        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
                }
            }
    }
}

上面的代码看似很长,除了日志如理和异常处理的代码,其实做了:
1. 创建一个ServerSocket监听8005端口
2. 当接收到Socket后,比较Socket传递的信息是否与“SHUTDOWN”字符串相一致,一致则关闭socket,退出方法。

我们再来看看Catalina#Stop方法,其代码如下:

public void stop() {

    try {
       // Remove the ShutdownHook first so that server.stop()
       // doesn't get invoked twice
       //首先移除关闭钩子避免server.stop方法被调用两次
       if (useShutdownHook) {
            Runtime.getRuntime().removeShutdownHook(shutdownHook);

            // If JULI is being used, re-enable JULI's shutdown to ensure
            // log messages are not lost
            LogManager logManager = LogManager.getLogManager();
            if (logManager instanceof ClassLoaderLogManager) {
                ((ClassLoaderLogManager) logManager).setUseShutdownHook(true);
            }
        }
    } catch (Throwable t) {
         ExceptionUtils.handleThrowable(t);
         // This will fail on JDK 1.2. Ignoring, as Tomcat can run
         // fine without the shutdown hook.
    }

    // Shut down the server
    try {
       Server s = getServer();
       LifecycleState state = s.getState();
       if (LifecycleState.STOPPING_PREP.compareTo(state) <= 0
             && LifecycleState.DESTROYED.compareTo(state) >= 0) {
           // Nothing to do. stop() was already called
       } else {
           s.stop();
           s.destroy();
       }
    } catch (LifecycleException e) {
        log.error("Catalina.stop", e);
    }
}

从上面我们可以看到,stop方法首先移除了关闭钩子,为了避免Server.stop方法被执行两次,接着调用Server#stopServer.destroy方法关闭服务器,stop方法结束后,main线程也就结束了,Tomcat服务也退出了。下面我们总结一下,

Tomcat是利用虚拟机的关闭钩子机制在JVM关闭时关闭各个组件,同时Tomcat可以通过监听8005端口传递过来的SHUTDOWN关闭字符串关闭Tomcat服务,既然是可以也就是说可以禁止监听8005端口来关闭Tomcat,即禁止通过网络关闭Tomcat,只需要将关闭的端口设置为-1即可。

我们关闭Tomcat常常会用到shutdown.bat文件(Linux环境下shutdown.sh,还可以使用ps aux | grep java ,kill -9),下面我们来看看shutdown.bat是否真的如我们上面分析的一样,向端口8005发送命令SHUTDOWN,其文件内容如下:

@echo off
rem Licensed to the Apache Software Foundation (ASF) under one or more
rem contributor license agreements.  See the NOTICE file distributed with
rem this work for additional information regarding copyright ownership.
rem The ASF licenses this file to You under the Apache License, Version 2.0
rem (the "License"); you may not use this file except in compliance with
rem the License.  You may obtain a copy of the License at
rem
rem     http://www.apache.org/licenses/LICENSE-2.0
rem
rem Unless required by applicable law or agreed to in writing, software
rem distributed under the License is distributed on an "AS IS" BASIS,
rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
rem See the License for the specific language governing permissions and
rem limitations under the License.

if "%OS%" == "Windows_NT" setlocal
rem ---------------------------------------------------------------------------
rem Stop script for the CATALINA Server
rem
rem $Id: shutdown.bat 895392 2010-01-03 14:02:31Z kkolinko $
rem ---------------------------------------------------------------------------

rem Guess CATALINA_HOME if not defined
set "CURRENT_DIR=%cd%"
if not "%CATALINA_HOME%" == "" goto gotHome
set "CATALINA_HOME=%CURRENT_DIR%"
if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome
cd ..
set "CATALINA_HOME=%cd%"
cd "%CURRENT_DIR%"
:gotHome
if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome
echo The CATALINA_HOME environment variable is not defined correctly
echo This environment variable is needed to run this program
goto end
:okHome

set "EXECUTABLE=%CATALINA_HOME%\bin\catalina.bat"

rem Check that target executable exists
if exist "%EXECUTABLE%" goto okExec
echo Cannot find "%EXECUTABLE%"
echo This file is needed to run this program
goto end
:okExec

rem Get remaining unshifted command line arguments and save them in the
set CMD_LINE_ARGS=
:setArgs
if ""%1""=="""" goto doneSetArgs
set CMD_LINE_ARGS=%CMD_LINE_ARGS% %1
shift
goto setArgs
:doneSetArgs
##调用了`catalina.bat`文件,并且传递了参数`stop`
call "%EXECUTABLE%" stop %CMD_LINE_ARGS%

:end

从最后一句我们可以看到shutdown.bat调用了catalina.bat文件,并且传递了参数stop,而catalina.bat最终调用了org.apache.catalina.startup.Bootstrap#main,并传递了参数stop,我们再来看看Bootstrap#main方法,对于stop参数的处理如下:

if (command.equals("stop")) {
   daemon.stopServer(args);
   //daemon是指Bootstrap实例

我们再来看看Bootstrap#stopServer方法,其代码(只保留核心代码)如下:

public void stopServer(String[] arguments)
        throws Exception {
    Method method =
         catalinaDaemon.getClass().getMethod("stopServer", paramTypes);
    method.invoke(catalinaDaemon, param);
}

从上面我们可以知道,stopServer方法通过反射机制调用了Catalina#stopServer方法,我们再来看看Catalina#stopServer方法,其代码如下:

public void stopServer(String[] arguments) {

    if (arguments != null) {
       arguments(arguments);
    }

    Server s = getServer();
    if( s == null ) {
       //1
       Digester digester = createStopDigester();
       digester.setClassLoader(Thread.currentThread().getContextClassLoader());
       File file = configFile();
       FileInputStream fis = null;
       try {
          InputSource is =
               new InputSource(file.toURI().toURL().toString());
          fis = new FileInputStream(file);
          is.setByteStream(fis);
          digester.push(this);
          digester.parse(is);
       } catch (Exception e) {
           log.error("Catalina.stop: ", e);
           System.exit(1);
       } finally {
           if (fis != null) {
             try {
               fis.close();
             } catch (IOException e) {
               // Ignore
             }
           }
      }
    } else {
       // Server object already present. Must be running as a service
       try {
          s.stop();
       } catch (LifecycleException e) {
          log.error("Catalina.stop: ", e);
       }
      return;
    }

    //2
    s = getServer();
    if (s.getPort()>0) {
       Socket socket = null;
       OutputStream stream = null;
       try {
         //创建向端口8005通信的socket
         socket = new Socket(s.getAddress(), s.getPort());
         stream = socket.getOutputStream();
         String shutdown = s.getShutdown();
         //发送`SHUTDOWN`指令
         for (int i = 0; i < shutdown.length(); i++) {
             stream.write(shutdown.charAt(i));
         }
         stream.flush();
        } catch (ConnectException ce) {
          log.error(sm.getString("catalina.stopServer.connectException",
                s.getAddress(),
                String.valueOf(s.getPort())));
                log.error("Catalina.stop: ", ce);
                System.exit(1);
        } catch (IOException e) {
          log.error("Catalina.stop: ", e);
          System.exit(1);
        } finally {
            if (stream != null) {
              try {
                stream.close();
              } catch (IOException e) {
                // Ignore
              }
            }
            if (socket != null) {
              try {
                socket.close();
              } catch (IOException e) {
                 // Ignore
              }
            }
        }
    } else {
        log.error(sm.getString("catalina.stopServer"));
        System.exit(1);
    }
}

我们来分析一下上面代码标注的地方:
1. 标注1,由于是新建的进程,conf/server.xml文件并没有解析,因此通过Digester解析conf/server.xml,最终生成了未初始化的StandardServer对象。
2. 标注2,获取StandardServer对象,向standardServer.getPort()返回的端口(其实这里面返回即是conf/server.xmlServer根节点配置的portshutdown属性)发送了standardServer.getShutdown()返回的字符串,而默认情况下这个字符串就是SHUTDOWN

至此,我们已基本理解Tomcat的关闭,总结如下:

Tomcat提供了两种方式关闭服务器,一种是利用虚拟机的关闭钩子机制,在虚拟机关闭时关闭各个组件,进而关闭服务器,另外一种则是通过监听网络8005端口,发送的SHUTDOWN指令,关闭各个组件,我们常用的shutdown.bat文件就是通过这个方法关闭Tomcat服务器的。

综上,本文从Tomcat的生命周期管理、Tomcat的启动、Tomcat的关闭三个方面对Tomcat生命周期进行了分析,下面我们来总结一下整篇文章的内容:

对生命周期进行管理的类是org.apahce.catalina.startup.Catalina,该类中的三个方法loadstartstop正是管理整个Tomcat服务器生命周期的方法,这三个方法会按照容器结构层次调用调用相应的方法,达到统一启动/关闭这些组件的效果,因为Tomcat的各个组件实现org.apache.catalina.Lifecycle。而Lifecycle接口定义了Tomcat所有的生命周期方法:init(初始化)、start(启动)、stop(停止)、destroy(销毁),其实现类LifecycleBase采用模板方法模式来对所有支持生命周期管理的组件的生命周期各个阶段进行了总体管理,每个有生命周期的组件都直接或间接继承于LifecycleBase类,并实现其中的生命周期的模板方法以加入相应生命周期的流程中。这就是Tomcat生命周期的管理方式。
Tomcat的启动时从顶层结构开始,在父层启动过程中调用子层的启动,从而达到统一启动组件的效果,特别需要注意的是Host层及以下的层都是使用异步线程池来启动子容器,这样做的目的是减少服务器的启动时间,同时保证所有子容器都启动完毕时才进行下一步操作;web应用是在Host启动时通过事件
Lifecycle.START_EVENT的监听器HostConfig来启动的。
Tomcat提供两种关闭服务器的方式:一种是利用虚拟机的关闭钩子机制,在虚拟机关闭时关闭各个组件,进而关闭服务器;另一种通过监听网络端口
8005传递的SHUTDOWN`指令,关闭各个组件。

注:以上的分析仅为个人的见解,难以必满存在错误地方,若发现了错误或者有不同的见解,欢迎留言留言或者Email交流,Email地址:woodyoilovecn@gmail.com

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值