概述
在很多环境下,当用户在关闭应用程序时我们需要做一些清理工作。问题在于,用户并不是经常的按照预推荐流程来退出。例如,在 Tomcat 部署中通过初始化一个服务器(Server)并调用它的 start()方法来启动一个 servlet 容器,该方法又调用其它组件的start()方法。正常情况下,可以通过一个关闭命令来让服务器关闭所有组件,犹如第14 章中介绍。如果突发地关闭程序,如在程序还在运行时关闭其控制台,可能会发生一些想不到的事情。
幸运的是,Java 为程序员提供了一种优雅的方式,保证清理代码总会执行。本章将会介绍如何使用一个关闭钩子(shutdown hook)来保证清理代码一定会被执行,不管用户怎么关闭应用。
在Java中,虚拟机响应两种类型的事件而自行关闭虚拟机:
•应用程序正常退出如System.exit()方法被调用或最后一个非守护线程退出时。
•用户突然强制终止虚拟机,例如键入 CTRL+C 或在关闭 Java程序之前从系统注销。
幸运的是,当关闭的时候,虚拟机会执行以下两个步骤:
1.虚拟机启动所有已注册的关闭钩子。关闭钩子是通过Runtime 注册的线程。所有关闭钩子会被同时执行直到完成;
2.如果需要,虚拟机则调用所有的未被调用的 finalizers。
在本章中,我们主要介绍第一个步骤,因为它允许程序员告诉虚拟机在程序中执行一些清理代码。一个关闭钩子是 java.lang.Thread 类的子类,可以如下创建一个关闭钩子:
•写一个类继承 Thread 类;
•实现这个类的run()方法。该方法是应用程序被关闭时要执行的代码,无论是正常还是非正常关闭;
•在我们的应用程序中,初始化一个关闭钩子;
•在当前的 Runtime上使用 addShutdownHook()方法来注册该关闭钩子。
你可能已经注意到,我们并没有把关闭钩子作为独立线程启动。虚拟机会在它的关闭步骤中启动该线程。
Listing16.1 提供了一简单关闭钩子例子。例子中仅仅打印出一些语句到控制台上,实际上,我们可以在其中插入任何想要执行代码。
Listing 16.1: Using Shutdown Hook
package ex16.pyrmont.shutdownhook;
public class ShutdownHookDemo {
public void start() {
System.out.println("Demo");
ShutdownHook ShutdownHook = new ShutdownHook();
Runtime.getRuntime().addShutdownHook(ShutdownHook);
}
public static void main(String[] args) {
ShutdownHookDemo demo = new ShutdownHookDemo();
demo.start();
try {
System.in.read();
}catch(Exception e) {}
}
}
class ShutdownHook extends Thread {
public void run() {
System.out.println("Shutting down");
}
}
在初始化ShutdownHookDemo 对象后,调用 start()方法。start()方法创建一个关闭钩子并在当前 Runtime 中注册:
ShutdownHook shutdownHook = new ShutdownHook();
Runtime.getRuntime().addShutdownHook(shutdownHook);
然后该程序等待用户键入回车键:
System.in.read();
用户键入 Enter 键后,应用程序退出。但是虚拟机会运行关闭钩子,看到的效果就是打印出语句“Shutting down”。
16.1 一个关闭钩子实例
另一个例子,考虑一个简单的 Swing 应用程序,名为MySwingApp,界面如图16.1。该应用程序启动时创建一个临时文件,在它关闭时必须要删除此临时文件。
图16.1: A Swing application
该类的代码如 Listing16.2 所示:
Listing 16.2: A simple Swing application
package ex16.pyrmont.shutdownhook;
import java.awt.*;
import javax.swing.*;
import java.awt.event.*;
import java.io.File;
import java.io.IOException;
public class MySwingApp extends JFrame {
JButton exitButton = new JButton();
JTextArea jTextArea1 = new JTextArea();
String dir = System.getProperty("user.dir");
String filename = "temp.txt";
public MySwingApp() {
exitButton.setText("Exit");
exitButton.setBounds(new Rectangle(304, 248, 76, 37));
exitButton.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(ActionEvent e) {
exitButton_actionPerformed(e);
}
});
this.getContentPane().setLayout(null);
jTextArea1.setText("Click the Exit button to quit");
jTextArea1.setBounds(new Rectangle(9, 7, 371, 235));
this.getContentPane().add(exitButton, null);
this.getContentPane().add(jTextArea1, null);
this.setDefaultCloseOperation(EXIT_ON_CLOSE);
this.setBounds(0,0, 400, 330);
this.setVisible(true);
initialize();
}
private void initialize() {
// create a temp file
File file = new File(dir, filename);
try {
System.out.println("Creating temporary file");
file.createNewFile();
}
catch (IOException e) {
System.out.println("Failed creating temporary file.");
}
}
private void shutdown() {
// delete the temp file
File file = new File(dir, filename);
if (file.exists()) {
System.out.println("Deleting temporary file.");
file.delete();
}
}
void exitButton_actionPerformed(ActionEvent e) {
shutdown();
System.exit(0);
}
public static void main(String[] args) {
MySwingApp mySwingApp = new MySwingApp();
}
}
当程序启动时,调用initialize()方法创建一temp.txt临时文件:
private void initialize() {
// create a temp file
File file = new File(dir, filename);
try {
System.out.println("Creating temporary file");
file.createNewFile();
}
catch (IOException e) {
System.out.println("Failed creating temporary file.");
}
}
当用户关闭该应用程序时,程序必须删除该临时文件。我们希望用户总是点击 Exit 按钮,因为这样在 shutdown()方法中就可以总是删除临时文件。但是,当用户点击窗口右上方X按钮非正常退出时,临时文件却不会被删除。
Listing16.3 所示的类提供了一种解决方案。修改Listing16.2代码提供了一个关闭钩子。该关闭钩子作为一个内部类来声明,以便于它可以访问主类的所有方法。在 Listing16.3中,关闭钩子的 run()方法调用了shutdown()方法,保证了当Java虚拟机关闭时该方法一定会被执行。
Listing 16.3: Using a shutdown hook in the Swing application
package ex16.pyrmont.shutdownhook;
import java.awt.*;
import javax.swing.*;
import java.awt.event.*;
import java.io.File;
import java.io.IOException;
public class MySwingAppWithShutdownHook extends JFrame {
JButton exitButton = new JButton();
JTextArea jTextArea1 = new JTextArea();
String dir = System.getProperty("user.dir");
String filename = "temp.txt";
public MySwingAppWithShutdownHook() {
exitButton.setText("Exit");
exitButton.setBounds(new Rectangle(304, 248, 76, 37));
exitButton.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(ActionEvent e) {
exitButton_actionPerformed(e);
}
});
this.getContentPane().setLayout(null);
jTextArea1.setText("Click the Exit button to quit");
jTextArea1.setBounds(new Rectangle(9, 7, 371, 235));
this.getContentPane().add(exitButton, null);
this.getContentPane().add(jTextArea1, null);
this.setDefaultCloseOperation(EXIT_ON_CLOSE);
this.setBounds(0, 0, 400, 330);
this.setVisible(true);
initialize();
}
private void initialize() {
// add shutdown hook
MyShutdownHook shutdownHook = new MyShutdownHook();
Runtime.getRuntime().addShutdownHook(shutdownHook);
// create a temp file
File file = new File(dir, filename);
try {
System.out.println("Creating temporary file");
file.createNewFile();
} catch (IOException e) {
System.out.println("Failed creating temporary file.");
}
}
private void shutdown() {
// delete the temp file
File file = new File(dir, filename);
if (file.exists()) {
System.out.println("Deleting temporary file.");
file.delete();
}
}
void exitButton_actionPerformed(ActionEvent e) {
shutdown();
System.exit(0);
}
public static void main(String[] args) {
MySwingAppWithShutdownHook mySwingApp = new MySwingAppWithShutdownHook();
}
private class MyShutdownHook extends Thread {
public void run() {
shutdown();
}
}
}
注意该类的 initialize()方法,它首先就是创建一MyshutdownHook内部类对象:
// add shutdown hook
MyShutdownHook shutdownHook = new MyShutdownHook();
我们一旦获得MyShutdownHook的实例,就可以通过Runtime 的 addShutDownHook()方法注册钩子,如下:
Runtime.getRuntime().addShutdownHook(shutdownHook);
initialize()方法的其它部分就跟 Listing16.2 所示的相同。它创建临时文件并打印出“Creating temporary file”:
// create a temp file
File file = new File(dir, filename);
try {
System.out.println("Creating temporary file");
file.createNewFile();
}catch (IOException e) {
System.out.println("Failed creating temporary file.");
}
接下来启动该应用程序,检查是否是不管怎么关闭应用,临时文件都总是会被删除。
16.2 Tomcat中关闭钩子
如你所料,Tomcat 也有自己的关闭钩子。我们可以在org.apache.catalina.startup.Catalina 类中找到它,该类负责启动服务器对象以及管理其它组件。在该类中有一个内部类 CatalinaShutdownHook 继承了java.lang.Thread 类(如Listing 16.4所示),在该类的run()方法中调用了服务器的 stop()方法。
Listing 16.4: Catalina shutdown hook
protected class CatalinaShutdownHook extends Thread {
public void run() {
if (server != null) {
try {
((Lifecycle) server).stop();
}catch (LifecycleException e) {
System.out.println("Catalina.stop: " + e);
e.printStackTrace(System.out);
if (e.getThrowable() != null) {
System.out.println("----- Root Cause -----");
e.getThrowable().printStackTrace(System.out);
}
}
}
}
}
该关闭钩子在 Catalina 实例启动时被初始化并添加到 Runtime 中。我们可以在第 17 章学习到Catalina更多的细节。
16.3 小结
有时我们希望应用程序在关闭之前运行一些清理代码。然而,不可能依靠用户总是正常退出。本章提供了“关闭钩子”这种解决方案,保证了无论用户如何关闭应用程序,清理代码都会执行。