Tomcat源碼分析 (六)----- Tomcat 啟動過程(一)

  • 2019 年 10 月 3 日
  • 筆記

說到Tomcat的啟動,我們都知道,我們每次需要運行tomcat/bin/startup.sh這個腳本,而這個腳本的內容到底是什麼呢?我們來看看。

啟動腳本

startup.sh 腳本

#!/bin/sh  os400=false  case "`uname`" in  OS400*) os400=true;;  esac    # resolve links - $0 may be a softlink  PRG="$0"    while [ -h "$PRG" ] ; do    ls=`ls -ld "$PRG"`    link=`expr "$ls" : '.*-> (.*)$'`    if expr "$link" : '/.*' > /dev/null; then      PRG="$link"    else      PRG=`dirname "$PRG"`/"$link"    fi  done    PRGDIR=`dirname "$PRG"`  EXECUTABLE=catalina.sh    # Check that target executable exists  if $os400; then    # -x will Only work on the os400 if the files are:    # 1. owned by the user    # 2. owned by the PRIMARY group of the user    # this will not work if the user belongs in secondary groups    eval  else    if [ ! -x "$PRGDIR"/"$EXECUTABLE" ]; then      echo "Cannot find $PRGDIR/$EXECUTABLE"      echo "The file is absent or does not have execute permission"      echo "This file is needed to run this program"      exit 1    fi  fi    exec "$PRGDIR"/"$EXECUTABLE" start "$@"

我們來看看這腳本。該腳本中有2個重要的變數:

  1. PRGDIR:表示當前腳本所在的路徑
  2. EXECUTABLE:catalina.sh 腳本名稱
    其中最關鍵的一行程式碼就是 exec "$PRGDIR"/"$EXECUTABLE" start "$@",表示執行了腳本catalina.sh,參數是start。

catalina.sh 腳本

然後我們看看catalina.sh 腳本中的實現:

elif [ "$1" = "start" ] ; then      if [ ! -z "$CATALINA_PID" ]; then      if [ -f "$CATALINA_PID" ]; then        if [ -s "$CATALINA_PID" ]; then          echo "Existing PID file found during start."          if [ -r "$CATALINA_PID" ]; then            PID=`cat "$CATALINA_PID"`            ps -p $PID >/dev/null 2>&1            if [ $? -eq 0 ] ; then              echo "Tomcat appears to still be running with PID $PID. Start aborted."              echo "If the following process is not a Tomcat process, remove the PID file and try again:"              ps -f -p $PID              exit 1            else              echo "Removing/clearing stale PID file."              rm -f "$CATALINA_PID" >/dev/null 2>&1              if [ $? != 0 ]; then                if [ -w "$CATALINA_PID" ]; then                  cat /dev/null > "$CATALINA_PID"                else                  echo "Unable to remove or clear stale PID file. Start aborted."                  exit 1                fi              fi            fi          else            echo "Unable to read PID file. Start aborted."            exit 1          fi        else          rm -f "$CATALINA_PID" >/dev/null 2>&1          if [ $? != 0 ]; then            if [ ! -w "$CATALINA_PID" ]; then              echo "Unable to remove or write to empty PID file. Start aborted."              exit 1            fi          fi        fi      fi    fi      shift    touch "$CATALINA_OUT"    if [ "$1" = "-security" ] ; then      if [ $have_tty -eq 1 ]; then        echo "Using Security Manager"      fi      shift      eval $_NOHUP ""$_RUNJAVA"" ""$LOGGING_CONFIG"" $LOGGING_MANAGER $JAVA_OPTS $CATALINA_OPTS         -classpath ""$CLASSPATH""         -Djava.security.manager         -Djava.security.policy==""$CATALINA_BASE/conf/catalina.policy""         -Dcatalina.base=""$CATALINA_BASE""         -Dcatalina.home=""$CATALINA_HOME""         -Djava.io.tmpdir=""$CATALINA_TMPDIR""         org.apache.catalina.startup.Bootstrap "$@" start         >> "$CATALINA_OUT" 2>&1 "&"      else      eval $_NOHUP ""$_RUNJAVA"" ""$LOGGING_CONFIG"" $LOGGING_MANAGER $JAVA_OPTS $CATALINA_OPTS         -classpath ""$CLASSPATH""         -Dcatalina.base=""$CATALINA_BASE""         -Dcatalina.home=""$CATALINA_HOME""         -Djava.io.tmpdir=""$CATALINA_TMPDIR""         org.apache.catalina.startup.Bootstrap "$@" start         >> "$CATALINA_OUT" 2>&1 "&"      fi      if [ ! -z "$CATALINA_PID" ]; then      echo $! > "$CATALINA_PID"    fi      echo "Tomcat started."
該腳本很長,但我們只關心我們感興趣的:如果參數是 start, 那麼執行這裡的邏輯,關鍵再最後一行執行了 org.apache.catalina.startup.Bootstrap "$@" start, 也就是說,執行了我們熟悉的main方法,並且攜帶了start 參數,那麼我們就來看Bootstrap 的main方法是如何實現的。

Bootstrap.main

首先我們啟動 main 方法:

public static void main(String args[]) {      System.err.println("Have fun and Enjoy! cxs");        // daemon 就是 bootstrap      if (daemon == null) {          Bootstrap bootstrap = new Bootstrap();          try {              //類載入機制我們前面已經講過,在這裡就不在重複了              bootstrap.init();          } catch (Throwable t) {              handleThrowable(t);              t.printStackTrace();              return;          }          daemon = bootstrap;      } else {          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);// bootstrap 和 Catalina 一脈相連, 這裡設置, 方法內部設置 Catalina 實例setAwait方法              daemon.load(args);// args 為 空,方法內部調用 Catalina 的 load 方法.              daemon.start();// 相同, 反射調用 Catalina 的 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);      }  }

我們來看看bootstrap.init();的部分程式碼

public void init() throws Exception {        // 類載入機制我們前面已經講過,在這裡就不在重複了      initClassLoaders();        Thread.currentThread().setContextClassLoader(catalinaLoader);      SecurityClassLoad.securityClassLoad(catalinaLoader);        // 反射方法實例化Catalina      Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");      Object startupInstance = startupClass.getConstructor().newInstance();          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);        // 引用Catalina實例      catalinaDaemon = startupInstance;  }

我們可以看到是通過反射實例化Catalina類,並將實例引用賦值給catalinaDaemon,接著我們看看daemon.load(args);

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);      //通過反射調用Catalina的load()方法      method.invoke(catalinaDaemon, param);    }

Catalina.load

我們可以看到daemon.load(args)實際上就是通過反射調用Catalina的load()方法.那麼我們進入 Catalina 類的 load 方法看看:

public void load() {        initDirs();        // 初始化jmx的環境變數      initNaming();        // Create and execute our Digester      // 定義解析server.xml的配置,告訴Digester哪個xml標籤應該解析成什麼類      Digester digester = createStartDigester();        InputSource inputSource = null;      InputStream inputStream = null;      File file = null;      try {          // 首先嘗試載入conf/server.xml,省略部分程式碼......        // 如果不存在conf/server.xml,則載入server-embed.xml(該xml在catalina.jar中),省略部分程式碼......        // 如果還是載入不到xml,則直接return,省略部分程式碼......          try {            inputSource.setByteStream(inputStream);              // 把Catalina作為一個頂級實例            digester.push(this);              // 解析過程會實例化各個組件,比如Server、Container、Connector等            digester.parse(inputSource);        } catch (SAXParseException spe) {            // 處理異常......        }      } finally {          // 關閉IO流......      }        // 給Server設置catalina資訊      getServer().setCatalina(this);      getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());      getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());        // Stream redirection      initStreams();        // 調用Lifecycle的init階段      try {          getServer().init();      } catch (LifecycleException e) {          // ......      }        // ......    }

Server初始化

可以看到, 這裡有一個我們今天感興趣的方法, getServer.init(), 這個方法看名字是啟動 Server 的初始化, 而 Server 是我們上面圖中最外層的容器. 因此, 我們去看看該方法, 也就是LifecycleBase.init() 方法. 該方法是一個模板方法, 只是定義了一個演算法的骨架, 將一些細節演算法放在子類中去實現.我們看看該方法:

LifecycleBase.init()

@Override  public final synchronized void init() throws LifecycleException {      // 1      if (!state.equals(LifecycleState.NEW)) {          invalidTransition(Lifecycle.BEFORE_INIT_EVENT);      }      // 2      setStateInternal(LifecycleState.INITIALIZING, null, false);        try {          // 模板方法          /**           * 採用模板方法模式來對所有支援生命周期管理的組件的生命周期各個階段進行了總體管理,           * 每個需要生命周期管理的組件只需要繼承這個基類,           * 然後覆蓋對應的鉤子方法即可完成相應的聲明周期階段的管理工作           */          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);  }
Server的實現類為StandardServer,我們分析一下StandardServer.initInternal()方法。該方法用於對Server進行初始化,關鍵的地方就是程式碼最後對services的循環操作,對每個service調用init方法。
【注】:這兒我們只粘貼出這部分程式碼。
StandardServer.initInternal()
@Override  protected void initInternal() throws LifecycleException {      super.initInternal();        // Initialize our defined Services      for (int i = 0; i < services.length; i++) {          services[i].init();      }  }

調用Service子容器的init方法,讓Service組件完成初始化,注意:在同一個Server下面,可能存在多個Service組件.

Service初始化

StandardService和StandardServer都是繼承至LifecycleMBeanBase,因此公共的初始化邏輯都是一樣的,這裡不做過多介紹,我們直接看下initInternal

StandardService.initInternal()

protected void initInternal() throws LifecycleException {        // 往jmx中註冊自己      super.initInternal();        // 初始化Engine      if (engine != null) {          engine.init();      }        // 存在Executor執行緒池,則進行初始化,默認是沒有的      for (Executor executor : findExecutors()) {          if (executor instanceof JmxEnabled) {              ((JmxEnabled) executor).setDomain(getDomain());          }          executor.init();      }        mapperListener.init();        // 初始化Connector,而Connector又會對ProtocolHandler進行初始化,開啟應用埠的監聽,      synchronized (connectorsLock) {          for (Connector connector : connectors) {              try {                  connector.init();              } catch (Exception e) {                  // 省略部分程式碼,logger and throw exception              }          }      }  }
  1. 首先,往jmx中註冊StandardService
  2. 初始化Engine,而Engine初始化過程中會去初始化Realm(許可權相關的組件)
  3. 如果存在Executor執行緒池,還會進行init操作,這個Excecutor是tomcat的介面,繼承至java.util.concurrent.Executor、org.apache.catalina.Lifecycle
  4. 初始化Connector連接器,默認有http1.1、ajp連接器,而這個Connector初始化過程,又會對ProtocolHandler進行初始化,開啟應用埠的監聽,後面會詳細分析

Engine初始化

StandardEngine初始化的程式碼如下:

@Override  protected void initInternal() throws LifecycleException {      getRealm();      super.initInternal();  }    public Realm getRealm() {      Realm configured = super.getRealm();      if (configured == null) {          configured = new NullRealm();          this.setRealm(configured);      }      return configured;  }

StandardEngine繼承至ContainerBase,而ContainerBase重寫了initInternal()方法,用於初始化start、stop執行緒池,這個執行緒池有以下特點:
1. core執行緒和max是相等的,默認為1
2. 允許core執行緒在超時未獲取到任務時退出執行緒
3. 執行緒獲取任務的超時時間是10s,也就是說所有的執行緒(包括core執行緒),超過10s未獲取到任務,那麼這個執行緒就會被銷毀

這麼做的初衷是什麼呢?因為這個執行緒池只需要在容器啟動和停止的時候發揮作用,沒必要時時刻刻處理任務隊列

ContainerBase的程式碼如下所示:

// 默認是1個執行緒  private int startStopThreads = 1;  protected ThreadPoolExecutor startStopExecutor;    @Override  protected void initInternal() throws LifecycleException {      BlockingQueue<Runnable> startStopQueue = new LinkedBlockingQueue<>();      startStopExecutor = new ThreadPoolExecutor(              getStartStopThreadsInternal(),              getStartStopThreadsInternal(), 10, TimeUnit.SECONDS,              startStopQueue,              new StartStopThreadFactory(getName() + "-startStop-"));      // 允許core執行緒超時未獲取任務時退出      startStopExecutor.allowCoreThreadTimeOut(true);      super.initInternal();  }    private int getStartStopThreadsInternal() {      int result = getStartStopThreads();        if (result > 0) {          return result;      }      result = Runtime.getRuntime().availableProcessors() + result;      if (result < 1) {          result = 1;      }      return result;  }

這個startStopExecutor執行緒池有什麼用呢?

  1. 在start的時候,如果發現有子容器,則會把子容器的start操作放在執行緒池中進行處理
  2. 在stop的時候,也會把stop操作放在執行緒池中處理

在前面的文章中我們介紹了Container組件,StandardEngine作為頂層容器,它的直接子容器是StardandHost,但是對StandardEngine的程式碼分析,我們並沒有發現它會對子容器StardandHost進行初始化操作,StandardEngine不按照套路出牌,而是把初始化過程放在start階段。個人認為Host、Context、Wrapper這些容器和具體的webapp應用相關聯了,初始化過程會更加耗時,因此在start階段用多執行緒完成初始化以及start生命周期,否則,像頂層的Server、Service等組件需要等待Host、Context、Wrapper完成初始化才能結束初始化流程,整個初始化過程是具有傳遞性的

Connector初始化

Connector初始化會在後面有專門的Connector文章講解

總結

至此,整個初始化過程便告一段落。整個初始化過程,由parent組件控制child組件的初始化,一層層往下傳遞,直到最後全部初始化OK。下圖描述了整體的傳遞流程 

默認情況下,Server只有一個Service組件,Service組件先後對Engine、Connector進行初始化。而Engine組件並不會在初始化階段對子容器進行初始化,Host、Context、Wrapper容器的初始化是在start階段完成的。tomcat默認會啟用HTTP1.1和AJP的Connector連接器,這兩種協議默認使用Http11NioProtocol、AJPNioProtocol進行處理

Tags: