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

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

posted @ 2013-08-21 13:53  潜台词  阅读(1431)  评论(0)    收藏  举报