Tomcat 7 服务器关闭原理

2,414 阅读8分钟

之前的几篇文章讲了 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 方法,看下这个方法的代码:

     
     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 方法:

    /**
     * Await and shutdown.
     */
    public void await() {
    
        getServer().await();
        
    }

该方法就一句话,意思是调用org.apache.catalina.core.StandardServer类的 await 方法:


     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连接的线程池是守护线程”。

熟悉 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 了,原理?看懂了上面的文章,这个应该不难。