Tomcat7服务器关闭原理
之前的几篇文章讲了Tomcat的启动过程,在默认的配置下启动完之后会看到后台实际上总共有6个线程在运行。即1个用户线程,剩下5个为守护线程(下图中的Daemon Thread)。

如果你对什么叫守护线程的概念比较陌生,这里再重复一下:
所谓守护线程,是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程。这种线程并不属于程序中不可或缺的部分,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。
用户线程和守护线程两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。将线程转换为守护线程可以通过调用Thread对象的setDaemon(true)方法来实现。
Tomcat的关闭正是利用了这个原理,即只要将那唯一的一个用户线程关闭,则整个应用就关闭了。
要研究这个用户线程怎么被关闭的得先从这个线程从何产生说起。在前面分析Tomcat的启动时我们是从org.apache.catalina.startup.Bootstrap类的main方法作为入口,该类的453到456行是Tomcat启动时会执行的代码:

前面的文章里分析了daemon.load和daemon.start方法,这里请注意daemon.setAwait(true);这句,它的作用是通过反射调用org.apache.catalina.startup.Catalina类的setAwait(true)方法,最终将Catalina类的实例变量await设值为true。
Catalina类的setAwait方法代码:
1 /** 2 * Set flag. 3 */ 4 public void setAwait(boolean await) 5 throws Exception { 6 7 Class<?> paramTypes[] = new Class[1]; 8 paramTypes[0] = Boolean.TYPE; 9 Object paramValues[] = new Object[1]; 10 paramValues[0] = Boolean.valueOf(await); 11 Method method = 12 catalinaDaemon.getClass().getMethod("setAwait", paramTypes); 13 method.invoke(catalinaDaemon, paramValues); 14 15 }
如前文分析,Tomcat启动时会调用org.apache.catalina.startup.Catalina类的start方法,看下这个方法的代码:
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方法:
1 /** 2 * Await and shutdown. 3 */ 4 public void await() { 5 6 getServer().await(); 7 8 }
该方法就一句话,意思是调用org.apache.catalina.core.StandardServer类的await方法:
/** * Wait until a proper shutdown command is received, then return. * This keeps the main thread alive - the thread pool listening for http * connections is daemon threads. */ @Override public void await() { // Negative values - don't wait on port - tomcat is embedded or we just don't like ports if( port == -2 ) { // undocumented yet - for embedding apps that are around, alive. return; } if( port==-1 ) { try { awaitThread = Thread.currentThread(); while(!stopAwait) { try { Thread.sleep( 10000 ); } catch( InterruptedException ex ) { // continue and check the flag } } } finally { awaitThread = null; } return; } // Set up a server socket to wait on try { awaitSocket = new ServerSocket(port, 1, InetAddress.getByName(address)); } catch (IOException e) { log.error("StandardServer.await: create[" + address + ":" + port + "]: ", e); return; } try { awaitThread = Thread.currentThread(); // Loop waiting for a connection and a valid command while (!stopAwait) { ServerSocket serverSocket = awaitSocket; if (serverSocket == null) { break; } // Wait for the next connection Socket socket = null; StringBuilder command = new StringBuilder(); try { InputStream stream; 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) { if (stopAwait) { // Wait was aborted with socket.close() break; } log.error("StandardServer.await: accept: ", e); break; } // Read a set of characters from the socket 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--; } } finally { // Close the socket now that we are done with it try { if (socket != null) { 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"); } } finally { ServerSocket serverSocket = awaitSocket; awaitThread = null; awaitSocket = null; // Close the server socket and return if (serverSocket != null) { try { serverSocket.close(); } catch (IOException e) { // Ignore } } } }
这段代码就不一一分析,总体作用如方法前的注释所说,即“一直等待到接收到一个正确的关闭命令后该方法将会返回。这样会使主线程一直存活——监听http连接的线程池是守护线程”。
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字符串。
1 boolean match = command.toString().equals(shutdown);
改成
1 <Server port="8005" shutdown="SHUTDOWN">
或者修改server.xml文件,找到Server节点,将原来的
1 <Server port="8005" shutdown="SHUTDOWN">
改成
1 <Server port="8005" shutdown="GET /SHUTDOWN HTTP/1.1">
这样直接在浏览器中输入http://localhost:8005/SHUTDOWN就可以关闭Tomcat了,原理?看懂了上面的文章,这个应该不难。

浙公网安备 33010602011771号