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方法代码:
/** * 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方法,看下这个方法的代码:
/** * 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.fatal(sm.getString("catalina.serverStartFail"), e); try { getServer().destroy(); } catch (LifecycleException e1) { log.debug("destroy() failed for failed Server ", e1); } return; } long t2 = System.nanoTime(); if(log.isInfoEnabled()) { log.info("Server startup in " + ((t2 - t1) / 1000000) + " ms"); } // 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); } } if (await) { await(); stop(); } }前文分析启动时发现通过第19行getServer().start()的这次方法调用,Tomcat接下来会一步步启动所有在配置文件中配置的组件。后面的代码没有分析,这里请关注最后第52到55行,上面说到已经将Catalina类的实例变量await设值为true,所以这里将会执行Catalina类的await方法:
/** * Await and shutdown. */ public void await() { getServer().await(); }该方法就一句话,意思是调用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连接的线程池是守护线程”。
public void stopServer(String[] arguments) { if (arguments != null) { arguments(arguments); } Server s = getServer(); if( s == null ) { // Create and execute our Digester Digester digester = createStopDigester(); digester.setClassLoader(Thread.currentThread().getContextClassLoader()); File file = configFile(); FileInputStream fis = null; try { InputSource is = new InputSource(file.toURI().toURL().toString()); fis = new FileInputStream(file); is.setByteStream(fis); digester.push(this); digester.parse(is); } catch (Exception e) { log.error("Catalina.stop: ", e); System.exit(1); } finally { if (fis != null) { try { fis.close(); } catch (IOException e) { // Ignore } } } } else { // Server object already present. Must be running as a service try { s.stop(); } catch (LifecycleException e) { log.error("Catalina.stop: ", e); } return; } // Stop the existing server s = getServer(); if (s.getPort()>0) { Socket socket = null; OutputStream stream = null; try { socket = new Socket(s.getAddress(), s.getPort()); stream = socket.getOutputStream(); String shutdown = s.getShutdown(); for (int i = 0; i < shutdown.length(); i++) { stream.write(shutdown.charAt(i)); } stream.flush(); } catch (ConnectException ce) { log.error(sm.getString("catalina.stopServer.connectException", s.getAddress(), String.valueOf(s.getPort()))); log.error("Catalina.stop: ", ce); System.exit(1); } catch (IOException e) { log.error("Catalina.stop: ", e); System.exit(1); } finally { if (stream != null) { try { stream.close(); } catch (IOException e) { // Ignore } } if (socket != null) { try { socket.close(); } catch (IOException e) { // Ignore } } } } else { log.error(sm.getString("catalina.stopServer")); System.exit(1); } }第8到41行是读取配置文件,可参照前面分析Digester的文章,不再赘述。从第49行开始,即向localhost:8005发起一个Socket连接,并写入SHUTDOWN字符串。
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了,原理?看懂了上面的文章,这个应该不难。