說到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個重要的變量:
- PRGDIR:表示當前腳本所在的路徑
- 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初始化
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方法。
【注】:這兒我們只粘貼出這部分代碼。
@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 } } } }
- 首先,往jmx中注冊StandardService
- 初始化Engine,而Engine初始化過程中會去初始化Realm(權限相關的組件)
- 如果存在Executor線程池,還會進行init操作,這個Excecutor是tomcat的接口,繼承至java.util.concurrent.Executor、org.apache.catalina.Lifecycle
- 初始化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線程池有什么用呢?
- 在start的時候,如果發現有子容器,則會把子容器的start操作放在線程池中進行處理
- 在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進行處理