說到Tomcat的啓動,咱們都知道,咱們每次須要運行tomcat/bin/startup.sh這個腳本,而這個腳本的內容究竟是什麼呢?咱們來看看。java
#!/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
exec "$PRGDIR"/"$EXECUTABLE" start "$@"
,表示執行了腳本catalina.sh,參數是start。而後咱們看看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方法是如何實現的。
首先咱們啓動 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); }
咱們能夠看到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) { // ...... } // ...... }
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方法。
@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
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 } } } }
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線程池有什麼用呢?
在前面的文章中咱們介紹了Container組件,StandardEngine做爲頂層容器,它的直接子容器是StardandHost,可是對StandardEngine的代碼分析,咱們並無發現它會對子容器StardandHost進行初始化操做,StandardEngine不按照套路出牌,而是把初始化過程放在start階段。我的認爲Host、Context、Wrapper這些容器和具體的webapp應用相關聯了,初始化過程會更加耗時,所以在start階段用多線程完成初始化以及start生命週期,不然,像頂層的Server、Service等組件須要等待Host、Context、Wrapper完成初始化才能結束初始化流程,整個初始化過程是具備傳遞性的
Connector初始化會在後面有專門的Connector文章講解
至此,整個初始化過程便告一段落。整個初始化過程,由parent組件控制child組件的初始化,一層層往下傳遞,直到最後所有初始化OK。下圖描述了總體的傳遞流程
默認狀況下,Server只有一個Service組件,Service組件前後對Engine、Connector進行初始化。而Engine組件並不會在初始化階段對子容器進行初始化,Host、Context、Wrapper容器的初始化是在start階段完成的。tomcat默認會啓用HTTP1.1和AJP的Connector鏈接器,這兩種協議默認使用Http11NioProtocol、AJPNioProtocol進行處理