Tomcat源碼分析三:Tomcat啟動載入過程(一)的源碼解析

  • 2019 年 10 月 25 日
  • 筆記

Tomcat啟動載入過程(一)的源碼解析

今天,我將分享用源碼的方式講解Tomcat啟動的載入過程,關於Tomcat的架構請參閱《Tomcat源碼分析二:先看看Tomcat的整體架構》一文。

先看看應用情況

在《Servlet與Tomcat運行示例》一文中,我詳細的記錄了Tomcat是如何啟動一個Servlet的程式的步驟。其中,第6步驟是啟動Tomcat,也就是在windows系統上執行startup.bat, 在linux作業系統上執行startup.sh的腳本。那麼,我們就從這個腳本出發,走進Tomcat,看看它是如何啟動的?這裡,我們以startup.sh為例,windows端的startup.bat類似。

startup.sh的內容是什麼?

我們先看看tomcat的啟動腳本startup.sh的內容是什麼,先看看其腳本內容(省略部分注釋),如下:

#!/bin/sh    # -----------------------------------------------------------------------------  # Start Script for the CATALINA Server  # -----------------------------------------------------------------------------    # Better OS/400 detection: see Bugzilla 31132  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 "$@"  

提取其中主要的幾句:

PRGDIR=`dirname "$PRG"`  EXECUTABLE=catalina.sh  exec "$PRGDIR"/"$EXECUTABLE" start "$@"

簡而概之,該腳本的執行內容為:調用catalina.sh腳本。下面,我們繼續來看下catalina.sh腳本的內容

catalina.sh腳本

由於catalina.sh腳本內容比較多,這裡提取一些重要的內容,然後解釋其用途:

再簡要的描述下在catalina.sh中作用:完成環境檢查、環境初始化、參數初始化、啟動操作步驟。注意一下上圖中被綠色框出來的內容,可以看到其調用執行的是org.apache.catalina.startup.Bootstrap類,並且傳輸過去的command指令為start。

回歸Java程式碼

Bootstrap類進行了什麼操作呢?

接下來,我們帶著這幾個問題來去探索一下Bootstrap類:

  • Bootstrap類在接收到start指令後要去幹什麼?
  • Bootstrap類在啟動過程中的職責是什麼?

下面,我們帶著上面的幾個問題來具體的探討一下Tomcat的源碼。先來看看Bootstrap類的main方法:

    public static void main(String args[]) {            synchronized (daemonLock) {              if (daemon == null) {                  // Don't set daemon until init() has completed                  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);                  daemon.load(args);                  daemon.start();                  if (null == daemon.getServer()) {                      System.exit(1);                  }              } 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);          }      }

從這段程式碼中,可以看出,其主要實現了兩個功能:

  • 初始化一個守護進程變數daemon
  • 載入catalina.sh傳遞過來的參數,解析catalina.sh傳遞過來的指令,並按照指令執行程式,控制守護進程daemon的啟停等操作

bootstrap.init();有什麼操作呢?

針對上面的兩個功能,我們進入到 init()方法看下有什麼操作,先看下init()方法的程式碼:

    public void init() throws Exception {            initClassLoaders();            Thread.currentThread().setContextClassLoader(catalinaLoader);            SecurityClassLoad.securityClassLoad(catalinaLoader);            // Load our startup class and call its process() method          if (log.isDebugEnabled())              log.debug("Loading startup class");          Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");          Object startupInstance = startupClass.getConstructor().newInstance();            // Set the shared extensions class loader          if (log.isDebugEnabled())              log.debug("Setting startup class properties");          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);            catalinaDaemon = startupInstance;      }

在init()方法中,首先執行的方法initClassLoaders()的作用是初始化三個類載入器,程式碼如下:

    /**       * Daemon reference.       */      private Object catalinaDaemon = null;        ClassLoader commonLoader = null;      ClassLoader catalinaLoader = null;      ClassLoader sharedLoader = null;        private void initClassLoaders() {          try {              commonLoader = createClassLoader("common", null);              if (commonLoader == null) {                  // no config file, default to this loader - we might be in a 'single' env.                  commonLoader = this.getClass().getClassLoader();              }              catalinaLoader = createClassLoader("server", commonLoader);              sharedLoader = createClassLoader("shared", commonLoader);          } catch (Throwable t) {              handleThrowable(t);              log.error("Class loader creation threw exception", t);              System.exit(1);          }      }        private ClassLoader createClassLoader(String name, ClassLoader parent)              throws Exception {                String value = CatalinaProperties.getProperty(name + ".loader");              if ((value == null) || (value.equals("")))                      return parent;                value = replace(value);                List<Repository> repositories = new ArrayList<>();                String[] repositoryPaths = getPaths(value);                for (String repository : repositoryPaths) {                      // Check for a JAR URL repository                      try {                              @SuppressWarnings("unused")                              URL url = new URL(repository);                              repositories.add(new Repository(repository, RepositoryType.URL));                              continue;                      } catch (MalformedURLException e) {                              // Ignore                      }                        // Local repository                      if (repository.endsWith("*.jar")) {                              repository = repository.substring                                      (0, repository.length() - "*.jar".length());                              repositories.add(new Repository(repository, RepositoryType.GLOB));                      } else if (repository.endsWith(".jar")) {                              repositories.add(new Repository(repository, RepositoryType.JAR));                      } else {                              repositories.add(new Repository(repository, RepositoryType.DIR));                      }              }                return ClassLoaderFactory.createClassLoader(repositories, parent);      }  
//  catalina.properties  common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"
  • commonLoader: 根據common.loader屬性的配置(通過程式碼CatalinaProperties.getProperty(name + ".loader");讀取:catalina.properties), 創建commonLoader類載入器, 默認情況下順序載入 ${catalina.base}/lib, ${catalina.base}/lib/.jar, ${catalina.home}/lib, ${catalina.home}/lib/.jar 四個目錄下的class和jar.
  • catalinaLoader: 根據server.loader屬性的配置, 創建catalinaLoader類載入器,其父類載入其為commonLoader, 默認server.loader屬性為空, 直接使用commonLoader.
  • sharedLoader:根據shared.loader屬性配置,創建sharedLoader類載入器,其父類載入其為commonLoader, 默認shared.loader屬性為空, 直接使用commonLoader.

當執行完initClassLoaders()方法之後,調用Thread.currentThread().setContextClassLoader(catalinaLoader);設置上下文類載入器為catalinaLoader,從上面解析的情況看,其實設置的上下文類載入器為catalinaLoader的父類commonLoader。

SecurityClassLoad.securityClassLoad(catalinaLoader) 的作用是如果有SecurityManager,提前載入部分類。

之後,通過使用catalinaLoader載入org.apache.catalina.startup.Catalina類,創建實例Catalina並利用反射調用方法setParentClassLoader(),設置Catalina實例的parentClassLoader屬性為sharedLoader類載入器(也就是commonLoader)。

最後,設置daemon為新創建的實例Bootstrap。接下來,看一下main()方法下的指令處理。

傳遞過來的command指令是如何處理的呢?

我們觀察一下main()方法的後半段,這裡貼一下程式碼:

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();              if (null == daemon.getServer()) {                      System.exit(1);              }      } 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) {      // ...... 省略  }

可以看到,其默認指令為start, 然後,其根據接收到的參數區分為startd、stopd、start、stop、configtest和其他6種指令情況。這裡我們主要看一下start指令的執行邏輯。

  • daemon.setAwait(true) :這句程式碼有什麼含義呢,下面我們來具體的分析一下:
    /**       * Set flag.       * @param await <code>true</code> if the daemon should block       * @throws Exception Reflection error       */      public void setAwait(boolean await)          throws Exception {            Class<?> paramTypes[] = new Class[1];          paramTypes[0] = Boolean.TYPE;          Object paramValues[] = new Object[1];          paramValues[0] = Boolean.valueOf(await);          Method method =              catalinaDaemon.getClass().getMethod("setAwait", paramTypes);          method.invoke(catalinaDaemon, paramValues);      }

這段程式碼的主要作用是通過反射調用Catalina.setAwait(true),主要目的是當啟動完成後, 阻塞main執行緒,等待stop命令到來。 如果不設置daemon.setAwait(true), 則main執行緒執行完之後就 直接退出了。

  • daemon.load(args)
    daemon.load(args);其實是最終執行的Catalina.load(),在Catalina.load()方法中,主要功能是首先初始化temp目錄,然後再初始化naming的一些系統屬性,然後獲取server.xml配置文件, 創建Digester實例, 開始解析server.xml的操作。
    /**       * Start a new server instance.       */      public void load() {            if (loaded) {              return;          }          loaded = true;            long t1 = System.nanoTime();            initDirs();            // Before digester - it may be needed          initNaming();            // Set configuration source          ConfigFileLoader.setSource(new CatalinaBaseConfigurationSource(Bootstrap.getCatalinaBaseFile(), getConfigFile()));          File file = configFile();            // Create and execute our Digester          Digester digester = createStartDigester();            try (ConfigurationSource.Resource resource = ConfigFileLoader.getSource().getServerXml()) {              InputStream inputStream = resource.getInputStream();              InputSource inputSource = new InputSource(resource.getURI().toURL().toString());              inputSource.setByteStream(inputStream);              digester.push(this);              digester.parse(inputSource);          } catch (Exception e) {              log.warn(sm.getString("catalina.configFail", file.getAbsolutePath()), e);              if (file.exists() && !file.canRead()) {                  log.warn(sm.getString("catalina.incorrectPermissions"));              }              return;          }            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(sm.getString("catalina.initError"), e);              }          }            long t2 = System.nanoTime();          if(log.isInfoEnabled()) {              log.info(sm.getString("catalina.init", Long.valueOf((t2 - t1) / 1000000)));          }      }  
  • daemon.start(): 啟動Tomcat

通過調用daemon.start()啟動Tomcat,其內容如下:

    /**       * Start the Catalina daemon.       * @throws Exception Fatal start error       */      public void start() throws Exception {          if (catalinaDaemon == null) {              init();          }            Method method = catalinaDaemon.getClass().getMethod("start", (Class [])null);          method.invoke(catalinaDaemon, (Object [])null);      }

程式通過反射的方式調用Catalina.start()方式啟動Tomcat,下面,我們看下Catalina.start()方法的實現邏輯:

    /**       * Start a new server instance.       */      public void start() {            if (getServer() == null) {              load();          }            if (getServer() == null) {              log.fatal(sm.getString("catalina.noServer"));              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(sm.getString("catalina.startup", Long.valueOf((t2 - t1) / 1000000)));          }            // 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();          }      }  

可以看出,程式調用getServer().start()啟動,getServer()方法返回的是一個StandardServer類,繼而其調用的是StandardServer.startInternal()方法,在StandardServer中,又調用到StandardService.startInternal()方法。

    // StandardServer.java      protected void startInternal() throws LifecycleException {            fireLifecycleEvent(CONFIGURE_START_EVENT, null);          setState(LifecycleState.STARTING);            globalNamingResources.start();            // Start our defined Services          synchronized (servicesLock) {              for (int i = 0; i < services.length; i++) {                  services[i].start();              }          }          // ......省略部分程式碼      }        protected void startInternal() throws LifecycleException {            if(log.isInfoEnabled())              log.info(sm.getString("standardService.start.name", this.name));          setState(LifecycleState.STARTING);            // Start our defined Container first          if (engine != null) {              synchronized (engine) {                  engine.start();              }          }            synchronized (executors) {              for (Executor executor: executors) {                  executor.start();              }          }            mapperListener.start();            // Start our defined Connectors second          synchronized (connectorsLock) {              for (Connector connector: connectors) {                  // If it has already failed, don't try and start it                  if (connector.getState() != LifecycleState.FAILED) {                      connector.start();                  }              }          }      }

注意,這裡為什麼不是start()方法,而是startInternal()方法呢?原因是StandardServer和StandService類都繼承了LifecycleMBeanBase類,而LifecycleMBeanBase類又繼承了LifecycleBase類。下面看下LifecycleBase類的start()方法:

    public final synchronized void start() throws LifecycleException {            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;          }            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);          }            try {              setStateInternal(LifecycleState.STARTING_PREP, null, false);              startInternal();              if (state.equals(LifecycleState.FAILED)) {                  // This is a 'controlled' failure. The component put itself into the                  // FAILED state so call stop() to complete the clean-up.                  stop();              } else if (!state.equals(LifecycleState.STARTING)) {                  // Shouldn't be necessary but acts as a check that sub-classes are                  // doing what they are supposed to.                  invalidTransition(Lifecycle.AFTER_START_EVENT);              } else {                  setStateInternal(LifecycleState.STARTED, null, false);              }          } catch (Throwable t) {              // This is an 'uncontrolled' failure so put the component into the              // FAILED state and throw an exception.              handleSubClassException(t, "lifecycleBase.startFail", toString());          }      }  

可以看出,調用start()方法,最終都會調用到startInternal()方法。在下篇文章中,我們將詳細看下StandardService.java中的engine.start()、executor.start()、connector.start()都分別啟動了什麼?敬請期待!

微信公眾號: 源碼灣

歡迎關注本人微信公眾號: 源碼灣。 本公眾號將不定期進行相關源碼及相關開發技術的分享,共同成長,共同進步~


Blog:

  • 簡書: https://www.jianshu.com/u/91378a397ffe
  • csdn: https://blog.csdn.net/ZhiyouWu
  • 開源中國: https://my.oschina.net/u/3204088
  • 掘金: https://juejin.im/user/5b5979efe51d451949094265
  • 部落格園: https://www.cnblogs.com/zhiyouwu/
  • 微信公眾號: 源碼灣
  • 微信: WZY1782357529 (歡迎溝通交流)