前言
熟悉Tomcat的工程師們,肯定都知道Tomcat是如何啟動與停止的。對於startup.sh、startup.bat、shutdown.sh、shutdown.bat等腳本或者批處理命令,大家一定知道改如何使用它,但是它們究竟是如何實現的,尤其是shutdown.sh腳本(或者shutdown.bat)究竟是如何和Tomcat進程通信的呢?本文將通過對Tomcat7.0的源碼閱讀,深入剖析這一過程。
由於在生產環境中,Tomcat一般部署在Linux系統下,所以本文將以startup.sh和shutdown.sh等shell腳本為准,對Tomcat的啟動與停止進行分析。
啟動過程分析
我們啟動Tomcat的命令如下:
sh startup.sh
所以,將從shell腳本startup.sh開始分析Tomcat的啟動過程。startup.sh的腳本代碼見代碼清單1。
代碼清單1
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 "$@"
代碼清單1中有兩個主要的變量,分別是:
- PRGDIR:當前shell腳本所在的路徑;
- EXECUTABLE:腳本catalina.sh。
根據最后一行代碼:exec "$PRGDIR"/"$EXECUTABLE" start "$@",我們知道執行了shell腳本catalina.sh,並且傳遞參數start。catalina.sh中接收到start參數后的執行的腳本分支見代碼清單2。
代碼清單2
elif [ "$1" = "start" ] ; then # 此處省略參數校驗的腳本 shift touch "$CATALINA_OUT" if [ "$1" = "-security" ] ; then if [ $have_tty -eq 1 ]; then echo "Using Security Manager" fi shift eval "\"$_RUNJAVA\"" "\"$LOGGING_CONFIG\"" $LOGGING_MANAGER $JAVA_OPTS $CATALINA_OPTS \ -Djava.endorsed.dirs="\"$JAVA_ENDORSED_DIRS\"" -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 "\"$_RUNJAVA\"" "\"$LOGGING_CONFIG\"" $LOGGING_MANAGER $JAVA_OPTS $CATALINA_OPTS \ -Djava.endorsed.dirs="\"$JAVA_ENDORSED_DIRS\"" -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."
從代碼清單2可以看出,最終使用java命令執行了org.apache.catalina.startup.Bootstrap類中的main方法,參數也是start。Bootstrap的main方法的實現見代碼清單3。
代碼清單3
/** * Main method, used for testing only. * * @param args Command line arguments to be processed */ public static void main(String args[]) { if (daemon == null) { // Don't set daemon until init() has completed Bootstrap bootstrap = new Bootstrap(); try { bootstrap.init(); } catch (Throwable t) { t.printStackTrace(); return; } daemon = bootstrap; } 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(); } else if (command.equals("stop")) { daemon.stopServer(args); } else { log.warn("Bootstrap: command \"" + command + "\" does not exist."); } } catch (Throwable t) { t.printStackTrace(); } }
從代碼清單3可以看出,當傳遞參數start的時候,command等於start,此時main方法的執行步驟如下:
步驟一 初始化Bootstrap
Bootstrap的init方法(見代碼清單4)的執行步驟如下:
- 設置Catalina路徑,默認為Tomcat的根目錄;
- 初始化Tomcat的類加載器,並設置線程上下文類加載器(具體實現細節,讀者可以參考《TOMCAT源碼分析——類加載體系》一文);
- 用反射實例化org.apache.catalina.startup.Catalina對象,並且使用反射調用其setParentClassLoader方法,給Catalina對象設置Tomcat類加載體系的頂級加載器(Java自帶的三種類加載器除外)。
代碼清單4
/** * Initialize daemon. */ public void init() throws Exception { // Set Catalina path setCatalinaHome(); setCatalinaBase(); 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.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; }
步驟二 加載、解析server.xml配置文件
當傳遞參數start的時候,會調用Bootstrap的load方法(見代碼清單5),其作用是用反射調用catalinaDaemon(類型是Catalina)的load方法加載和解析server.xml配置文件,具體細節已在《TOMCAT源碼分析——SERVER.XML文件的加載與解析》一文中詳細介紹,有興趣的朋友可以選擇閱讀。
代碼清單5
/** * Load daemon. */ 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); method.invoke(catalinaDaemon, param); }
步驟三 啟動Tomcat
當傳遞參數start的時候,調用Bootstrap的load方法之后會接着調用start方法(見代碼清單6)啟動Tomcat,此方法實際是用反射調用了catalinaDaemon(類型是Catalina)的start方法。
代碼清單6
/** * Start the Catalina daemon. */ public void start() throws Exception { if( catalinaDaemon==null ) init(); Method method = catalinaDaemon.getClass().getMethod("start", (Class [] )null); method.invoke(catalinaDaemon, (Object [])null); }
Catalina的start方法(見代碼清單7)的執行步驟如下:
- 驗證Server容器是否已經實例化。如果沒有實例化Server容器,還會再次調用Catalina的load方法加載和解析server.xml,這也說明Tomcat只允許Server容器通過配置在server.xml的方式生成,用戶也可以自己實現Server接口創建自定義的Server容器以取代默認的StandardServer。
- 啟動Server容器,有關容器的啟動過程的分析可以參考《TOMCAT源碼分析——生命周期管理》一文的內容。
- 設置關閉鈎子。這么說可能有些不好理解,那就換個說法。Tomcat本身可能由於所在機器斷點,程序bug甚至內存溢出導致進程退出,但是Tomcat可能需要在退出的時候做一些清理工作,比如:內存清理、對象銷毀等。這些清理動作需要封裝在一個Thread的實現中,然后將此Thread對象作為參數傳遞給Runtime的addShutdownHook方法即可。
- 最后調用Catalina的await方法循環等待接收Tomcat的shutdown命令。
- 如果Tomcat運行正常且沒有收到shutdown命令,是不會向下執行stop方法的,當接收到shutdown命令,Catalina的await方法會退出循環等待,然后順序執行stop方法停止Tomcat。
代碼清單7
/** * Start a new server instance. */ public void start() { if (getServer() == null) { load(); } if (getServer() == null) { log.fatal("Cannot start server. Server instance is not configured."); return; } long t1 = System.nanoTime(); // Start the new server try { getServer().start(); } catch (LifecycleException e) { log.error("Catalina.start: ", e); } long t2 = System.nanoTime(); if(log.isInfoEnabled()) log.info("Server startup in " + ((t2 - t1) / 1000000) + " ms"); try { // 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); } } } catch (Throwable t) { // This will fail on JDK 1.2. Ignoring, as Tomcat can run // fine without the shutdown hook. } if (await) { await(); stop(); } }
Catalina的await方法(見代碼清單8)實際只是代理執行了Server容器的await方法。
代碼清單8
/** * Await and shutdown. */ public void await() { getServer().await(); }
以Server的默認實現StandardServer為例,其await方法(見代碼清單9)的執行步驟如下:
- 創建socket連接的服務端對象ServerSocket;
- 循環等待接收客戶端發出的命令,如果接收到的命令與SHUTDOWN匹配(由於使用了equals,所以shutdown命令必須是大寫的),那么退出循環等待。
代碼清單9
public void await() { // Negative values - don't wait on port - tomcat is embedded or we just don't like ports gja if( port == -2 ) { // undocumented yet - for embedding apps that are around, alive. return; } if( port==-1 ) { while( true ) { try { Thread.sleep( 10000 ); } catch( InterruptedException ex ) { } if( stopAwait ) return; } } // Set up a server socket to wait on ServerSocket serverSocket = null; try { serverSocket = new ServerSocket(port, 1, InetAddress.getByName(address)); } catch (IOException e) { log.error("StandardServer.await: create[" + address + ":" + port + "]: ", e); System.exit(1); } // Loop waiting for a connection and a valid command while (true) { // Wait for the next connection Socket socket = null; InputStream stream = null; try { socket = serverSocket.accept(); socket.setSoTimeout(10 * 1000); // Ten seconds stream = socket.getInputStream(); } catch (AccessControlException ace) { log.warn("StandardServer.accept security exception: " + ace.getMessage(), ace); continue; } catch (IOException e) { log.error("StandardServer.await: accept: ", e); System.exit(1); } // Read a set of characters from the socket StringBuilder command = new StringBuilder(); int expected = 1024; // Cut off to avoid DoS attack while (expected < shutdown.length()) { if (random == null) random = new Random(); expected += (random.nextInt() % 1024); } while (expected > 0) { int ch = -1; try { ch = stream.read(); } catch (IOException e) { log.warn("StandardServer.await: read: ", e); ch = -1; } if (ch < 32) // Control character or EOF terminates loop break; command.append((char) ch); expected--; } // Close the socket now that we are done with it try { socket.close(); } catch (IOException e) { // Ignore } // Match against our command string boolean match = command.toString().equals(shutdown); if (match) { log.info(sm.getString("standardServer.shutdownViaPort")); break; } else log.warn("StandardServer.await: Invalid command '" + command.toString() + "' received"); } // Close the server socket and return try { serverSocket.close(); } catch (IOException e) { // Ignore } }
至此,Tomcat啟動完畢。很多人可能會問,執行sh shutdown.sh腳本時,是如何與Tomcat進程通信的呢?如果要與Tomcat的ServerSocket通信,socket客戶端如何知道服務端的連接地址與端口呢?下面會慢慢說明。
停止過程分析
我們停止Tomcat的命令如下:
sh shutdown.sh
所以,將從shell腳本shutdown.sh開始分析Tomcat的停止過程。shutdown.sh的腳本代碼見代碼清單10。
代碼清單10
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" stop "$@"
代碼清單10和代碼清單1非常相似,其中也有兩個主要的變量,分別是:
- PRGDIR:當前shell腳本所在的路徑;
- EXECUTABLE:腳本catalina.sh。
根據最后一行代碼:exec "$PRGDIR"/"$EXECUTABLE" stop "$@",我們知道執行了shell腳本catalina.sh,並且傳遞參數stop。catalina.sh中接收到stop參數后的執行的腳本分支見代碼清單11。
代碼清單11
elif [ "$1" = "stop" ] ; then #省略參數校驗腳本 eval "\"$_RUNJAVA\"" $LOGGING_MANAGER $JAVA_OPTS \ -Djava.endorsed.dirs="\"$JAVA_ENDORSED_DIRS\"" -classpath "\"$CLASSPATH\"" \ -Dcatalina.base="\"$CATALINA_BASE\"" \ -Dcatalina.home="\"$CATALINA_HOME\"" \ -Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \ org.apache.catalina.startup.Bootstrap "$@" stop
從代碼清單11可以看出,最終使用java命令執行了org.apache.catalina.startup.Bootstrap類中的main方法,參數是stop。從代碼清單3可以看出,當傳遞參數stop的時候,command等於stop,此時main方法的執行步驟如下:
步驟一 初始化Bootstrap
已經在啟動過程分析中介紹, 不再贅述。
步驟二 停止服務
通過調用Bootstrap的stopServer方法(見代碼清單12)停止Tomcat,其實質是用反射調用catalinaDaemon(類型是Catalina)的stopServer方法。
代碼清單12
/** * Stop the standalone server. */ public void stopServer(String[] arguments) throws Exception { 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("stopServer", paramTypes); method.invoke(catalinaDaemon, param); }
Catalina的stopServer方法(見代碼清單13)的執行步驟如下:
- 創建Digester解析server.xml文件(此處只解析<Server>標簽),以構造出Server容器(此時Server容器的子容器沒有被實例化);
- 從實例化的Server容器獲取Server的socket監聽端口和地址,然后創建Socket對象連接啟動Tomcat時創建的ServerSocket,最后向ServerSocket發送SHUTDOWN命令。根據代碼清單9的內容,ServerSocket循環等待接收到SHUTDOWN命令后,最終調用stop方法停止Tomcat。
代碼清單13
public void stopServer() { stopServer(null); } public void stopServer(String[] arguments) { if (arguments != null) { arguments(arguments); } if( getServer() == null ) { // Create and execute our Digester Digester digester = createStopDigester(); digester.setClassLoader(Thread.currentThread().getContextClassLoader()); File file = configFile(); try { InputSource is = new InputSource("file://" + file.getAbsolutePath()); FileInputStream fis = new FileInputStream(file); is.setByteStream(fis); digester.push(this); digester.parse(is); fis.close(); } catch (Exception e) { log.error("Catalina.stop: ", e); System.exit(1); } } // Stop the existing server try { if (getServer().getPort()>0) { Socket socket = new Socket(getServer().getAddress(), getServer().getPort()); OutputStream stream = socket.getOutputStream(); String shutdown = getServer().getShutdown(); for (int i = 0; i < shutdown.length(); i++) stream.write(shutdown.charAt(i)); stream.flush(); stream.close(); socket.close(); } else { log.error(sm.getString("catalina.stopServer")); System.exit(1); } } catch (IOException e) { log.error("Catalina.stop: ", e); System.exit(1); } }
最后,我們看看Catalina的stop方法(見代碼清單14)的實現,其執行步驟如下:
- 將啟動過程中添加的關閉鈎子移除。Tomcat啟動過程辛辛苦苦添加的關閉鈎子為什么又要去掉呢?因為關閉鈎子是為了在JVM異常退出后,進行資源的回收工作。主動停止Tomcat時調用的stop方法里已經包含了資源回收的內容,所以不再需要這個鈎子了。
- 停止Server容器。有關容器的停止內容,請閱讀《TOMCAT源碼分析——生命周期管理》一文。
代碼清單14
/** * Stop an existing server instance. */ public void stop() { try { // Remove the ShutdownHook first so that server.stop() // doesn't get invoked twice if (useShutdownHook) { Runtime.getRuntime().removeShutdownHook(shutdownHook); // If JULI is being used, re-enable JULI's shutdown to ensure // log messages are not lost jiaan LogManager logManager = LogManager.getLogManager(); if (logManager instanceof ClassLoaderLogManager) { ((ClassLoaderLogManager) logManager).setUseShutdownHook( true); } } } catch (Throwable t) { // This will fail on JDK 1.2. Ignoring, as Tomcat can run // fine without the shutdown hook. } // Shut down the server try { getServer().stop(); } catch (LifecycleException e) { log.error("Catalina.stop", e); } }
總結
通過對Tomcat源碼的分析我們了解到Tomcat的啟動和停止都離不開org.apache.catalina.startup.Bootstrap。當停止Tomcat時,已經啟動的Tomcat作為socket服務端,停止腳本啟動的Bootstrap進程作為socket客戶端向服務端發送shutdown命令,兩個進程通過共享server.xml里Server標簽的端口以及地址信息打通了socket的通信。
如需轉載,請標明本文作者及出處——作者:jiaan.gja,本文原創首發:博客園,原文鏈接:http://www.cnblogs.com/jiaan-geng/p/4872550.html