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

說到Tomcat的啓動,咱們都知道,咱們每次須要運行tomcat/bin/startup.sh這個腳本,而這個腳本的內容究竟是什麼呢?咱們來看看。java

啓動腳本

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個重要的變量:web

  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 方法:apache

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();的部分代碼bootstrap

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);tomcat

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()app

@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組件.webapp

Service初始化

StandardService和StandardServer都是繼承至LifecycleMBeanBase,所以公共的初始化邏輯都是同樣的,這裏不作過多介紹,咱們直接看下initInternalide

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.一、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進行處理

相關文章
相關標籤/搜索