以前的幾篇文章講了 Tomcat 的啓動過程,在默認的配置下啓動完以後會看到後臺實際上總共有 6 個線程在運行。即 1 個用戶線程,剩下 5 個爲守護線程(下圖中的 Daemon Thread )。 apache
所謂守護線程,是指在程序運行的時候在後臺提供一種通用服務的線程,好比垃圾回收線程。這種線程並不屬於程序中不可或缺的部分,當全部的非守護線程結束時,程序也就終止了,同時會殺死進程中的全部守護線程。反過來講,只要任何非守護線程還在運行,程序就不會終止。編程
用戶線程和守護線程二者幾乎沒有區別,惟一的不一樣之處就在於虛擬機的離開:若是用戶線程已經所有退出運行了,只剩下守護線程存在了,虛擬機也就退出了。由於沒有了被守護者,守護線程也就沒有工做可作了,也就沒有繼續運行程序的必要了。將線程轉換爲守護線程能夠經過調用 Thread 對象的 setDaemon(true) 方法來實現。瀏覽器
Tomcat 的關閉正是利用了這個原理,即只要將那惟一的一個用戶線程關閉,則整個應用就關閉了。tomcat
要研究這個用戶線程怎麼被關閉的得先從這個線程從何產生提及。在前面分析 Tomcat 的啓動時咱們是從org.apache.catalina.startup.Bootstrap
類的 main 方法做爲入口,該類的 453 到 456 行是 Tomcat 啓動時會執行的代碼: bash
daemon.setAwait(true);
這句,它的做用是經過反射調用
org.apache.catalina.startup.Catalina
類的 setAwait(true) 方法,最終將 Catalina 類的實例變量 await 設值爲 true 。
Catalina 類的 setAwait 方法代碼:app
/**
* Set flag.
*/
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);
}
複製代碼
如前文分析,Tomcat 啓動時會調用org.apache.catalina.startup.Catalina
類的 start 方法,看下這個方法的代碼:dom
1 /**
2 * Start a new server instance.
3 */
4 public void start() {
5
6 if (getServer() == null) {
7 load();
8 }
9
10 if (getServer() == null) {
11 log.fatal("Cannot start server. Server instance is not configured.");
12 return;
13 }
14
15 long t1 = System.nanoTime();
16
17 // Start the new server
18 try {
19 getServer().start();
20 } catch (LifecycleException e) {
21 log.fatal(sm.getString("catalina.serverStartFail"), e);
22 try {
23 getServer().destroy();
24 } catch (LifecycleException e1) {
25 log.debug("destroy() failed for failed Server ", e1);
26 }
27 return;
28 }
29
30 long t2 = System.nanoTime();
31 if(log.isInfoEnabled()) {
32 log.info("Server startup in " + ((t2 - t1) / 1000000) + " ms");
33 }
34
35 // Register shutdown hook
36 if (useShutdownHook) {
37 if (shutdownHook == null) {
38 shutdownHook = new CatalinaShutdownHook();
39 }
40 Runtime.getRuntime().addShutdownHook(shutdownHook);
41
42 // If JULI is being used, disable JULI's shutdown hook since
43 // shutdown hooks run in parallel and log messages may be lost
44 // if JULI's hook completes before the CatalinaShutdownHook()
45 LogManager logManager = LogManager.getLogManager();
46 if (logManager instanceof ClassLoaderLogManager) {
47 ((ClassLoaderLogManager) logManager).setUseShutdownHook(
48 false);
49 }
50 }
51
52 if (await) {
53 await();
54 stop();
55 }
56 }
複製代碼
前文分析啓動時發現經過第 19 行 getServer().start() 的此次方法調用,Tomcat 接下來會一步步啓動全部在配置文件中配置的組件。後面的代碼沒有分析,這裏請關注最後第 52 到 55 行,上面說到已經將 Catalina 類的實例變量 await 設值爲 true,因此這裏將會執行 Catalina 類的 await 方法:socket
/**
* Await and shutdown.
*/
public void await() {
getServer().await();
}
複製代碼
該方法就一句話,意思是調用org.apache.catalina.core.StandardServer
類的 await 方法:ide
1 /**
2 * Wait until a proper shutdown command is received, then return.
3 * This keeps the main thread alive - the thread pool listening for http
4 * connections is daemon threads.
5 */
6 @Override
7 public void await() {
8 // Negative values - don't wait on port - tomcat is embedded or we just don't like ports
9 if( port == -2 ) {
10 // undocumented yet - for embedding apps that are around, alive.
11 return;
12 }
13 if( port==-1 ) {
14 try {
15 awaitThread = Thread.currentThread();
16 while(!stopAwait) {
17 try {
18 Thread.sleep( 10000 );
19 } catch( InterruptedException ex ) {
20 // continue and check the flag
21 }
22 }
23 } finally {
24 awaitThread = null;
25 }
26 return;
27 }
28
29 // Set up a server socket to wait on
30 try {
31 awaitSocket = new ServerSocket(port, 1,
32 InetAddress.getByName(address));
33 } catch (IOException e) {
34 log.error("StandardServer.await: create[" + address
35 + ":" + port
36 + "]: ", e);
37 return;
38 }
39
40 try {
41 awaitThread = Thread.currentThread();
42
43 // Loop waiting for a connection and a valid command
44 while (!stopAwait) {
45 ServerSocket serverSocket = awaitSocket;
46 if (serverSocket == null) {
47 break;
48 }
49
50 // Wait for the next connection
51 Socket socket = null;
52 StringBuilder command = new StringBuilder();
53 try {
54 InputStream stream;
55 try {
56 socket = serverSocket.accept();
57 socket.setSoTimeout(10 * 1000); // Ten seconds
58 stream = socket.getInputStream();
59 } catch (AccessControlException ace) {
60 log.warn("StandardServer.accept security exception: "
61 + ace.getMessage(), ace);
62 continue;
63 } catch (IOException e) {
64 if (stopAwait) {
65 // Wait was aborted with socket.close()
66 break;
67 }
68 log.error("StandardServer.await: accept: ", e);
69 break;
70 }
71
72 // Read a set of characters from the socket
73 int expected = 1024; // Cut off to avoid DoS attack
74 while (expected < shutdown.length()) {
75 if (random == null)
76 random = new Random();
77 expected += (random.nextInt() % 1024);
78 }
79 while (expected > 0) {
80 int ch = -1;
81 try {
82 ch = stream.read();
83 } catch (IOException e) {
84 log.warn("StandardServer.await: read: ", e);
85 ch = -1;
86 }
87 if (ch < 32) // Control character or EOF terminates loop
88 break;
89 command.append((char) ch);
90 expected--;
91 }
92 } finally {
93 // Close the socket now that we are done with it
94 try {
95 if (socket != null) {
96 socket.close();
97 }
98 } catch (IOException e) {
99 // Ignore
100 }
101 }
102
103 // Match against our command string
104 boolean match = command.toString().equals(shutdown);
105 if (match) {
106 log.info(sm.getString("standardServer.shutdownViaPort"));
107 break;
108 } else
109 log.warn("StandardServer.await: Invalid command '"
110 + command.toString() + "' received");
111 }
112 } finally {
113 ServerSocket serverSocket = awaitSocket;
114 awaitThread = null;
115 awaitSocket = null;
116
117 // Close the server socket and return
118 if (serverSocket != null) {
119 try {
120 serverSocket.close();
121 } catch (IOException e) {
122 // Ignore
123 }
124 }
125 }
126 }
複製代碼
這段代碼就不一一分析,整體做用如方法前的註釋所說,即「一直等待到接收到一個正確的關閉命令後該方法將會返回。這樣會使主線程一直存活——監聽http鏈接的線程池是守護線程」。oop
熟悉 Java 的 Socket 編程的話對這段代碼就很容易理解,就是默認地址(地址值由實例變量 address 定義,默認爲localhost
)的默認的端口(端口值由實例變量 port 定義,默認爲8005
)上監聽 Socket 鏈接,當發現監聽到的鏈接的輸入流中的內容與默認配置的值匹配(該值默認爲字符串SHUTDOWN
)則跳出循環,該方法返回(第 103 到 107 行)。不然該方法會一直循環執行下去。 通常來講該用戶主線程會阻塞(第 56 行)直到有訪問localhost:8005
的鏈接出現。 正由於如此纔出現開頭看見的主線程一直 Running 的狀況,而由於這個線程一直 Running ,其它守護線程也會一直存在。
說完這個線程的產生,接下來看看這個線程的關閉,按照上面的分析,這個線程提供了一個關閉機制,即只要訪問localhost:8005
,而且發送一個內容爲SHUTDOWN
的字符串,就能夠關閉它了。
Tomcat 正是這麼作的,通常來講關閉 Tomcat 經過執行 shutdown.bat 或 shutdown.sh 腳本,關於這段腳本可參照分析啓動腳本那篇文章,機制相似,最終會執行org.apache.catalina.startup.Bootstrap
類的 main 方法,並傳入入參"stop"
,看下本文第 2 張圖片中org.apache.catalina.startup.Bootstrap
類的第 458 行,接着將調用org.apache.catalina.startup.Catalina
類 stopServer 方法:
1 public void stopServer(String[] arguments) {
2
3 if (arguments != null) {
4 arguments(arguments);
5 }
6
7 Server s = getServer();
8 if( s == null ) {
9 // Create and execute our Digester
10 Digester digester = createStopDigester();
11 digester.setClassLoader(Thread.currentThread().getContextClassLoader());
12 File file = configFile();
13 FileInputStream fis = null;
14 try {
15 InputSource is =
16 new InputSource(file.toURI().toURL().toString());
17 fis = new FileInputStream(file);
18 is.setByteStream(fis);
19 digester.push(this);
20 digester.parse(is);
21 } catch (Exception e) {
22 log.error("Catalina.stop: ", e);
23 System.exit(1);
24 } finally {
25 if (fis != null) {
26 try {
27 fis.close();
28 } catch (IOException e) {
29 // Ignore
30 }
31 }
32 }
33 } else {
34 // Server object already present. Must be running as a service
35 try {
36 s.stop();
37 } catch (LifecycleException e) {
38 log.error("Catalina.stop: ", e);
39 }
40 return;
41 }
42
43 // Stop the existing server
44 s = getServer();
45 if (s.getPort()>0) {
46 Socket socket = null;
47 OutputStream stream = null;
48 try {
49 socket = new Socket(s.getAddress(), s.getPort());
50 stream = socket.getOutputStream();
51 String shutdown = s.getShutdown();
52 for (int i = 0; i < shutdown.length(); i++) {
53 stream.write(shutdown.charAt(i));
54 }
55 stream.flush();
56 } catch (ConnectException ce) {
57 log.error(sm.getString("catalina.stopServer.connectException",
58 s.getAddress(),
59 String.valueOf(s.getPort())));
60 log.error("Catalina.stop: ", ce);
61 System.exit(1);
62 } catch (IOException e) {
63 log.error("Catalina.stop: ", e);
64 System.exit(1);
65 } finally {
66 if (stream != null) {
67 try {
68 stream.close();
69 } catch (IOException e) {
70 // Ignore
71 }
72 }
73 if (socket != null) {
74 try {
75 socket.close();
76 } catch (IOException e) {
77 // Ignore
78 }
79 }
80 }
81 } else {
82 log.error(sm.getString("catalina.stopServer"));
83 System.exit(1);
84 }
85 }
複製代碼
第 8 到 41 行是讀取配置文件,可參照前面分析 Digester 的文章,再也不贅述。從第 49 行開始,即向localhost:8005
發起一個 Socket 鏈接,並寫入SHUTDOWN
字符串。 這樣將會關閉 Tomcat 中的那惟一的一個用戶線程,接着全部守護線程將會退出(由 JVM 保證),以後整個應用關閉。
以上分析 Tomcat 的默認關閉機制,但這是經過運行腳原本關閉,我以爲這樣比較麻煩,那麼能不能經過一種在線訪問的方式關閉 Tomcat 呢?固然能夠,比較暴力的玩法是直接改org.apache.catalina.core.StandardServer
的源碼第 500 行,將
boolean match = command.toString().equals(shutdown);
複製代碼
改爲
boolean match = command.toString().equals(「GET /SHUTDOWN HTTP/1.1」);
複製代碼
或者修改 server.xml 文件,找到 Server 節點,將原來的
<Server port="8005" shutdown="SHUTDOWN">
複製代碼
改爲
<Server port="8005" shutdown="GET /SHUTDOWN HTTP/1.1">
複製代碼
這樣直接在瀏覽器中輸入http://localhost:8005/SHUTDOWN
就能夠關閉 Tomcat 了,原理?看懂了上面的文章,這個應該不難。