在很多实际应用环境中,当用户关了应用程序时,需要做一些善后清理工作,但问题是,用户有时并不会按照推荐的方法关闭应用程序,很有可能不做清理工作,例如在Tomcat的部署应用中,通过实例化一个Server对象来启动servlet容器,并调用其start方法,然后逐个调用组件的start方法,正常情况下,为了让Server对象能够关闭这些已经启动的组件,你应该向指定的端口发送关闭命令,如果你只是简单的突然退出,例如在应用程序过程中关闭控制台,可能会发生一些意想不到的事情。
幸运的是,java为程序员提供了一种优雅的方法可以在关闭过程中执行一些代码,这样就能确保那些负责善后处理的代码肯定能够执行,下面将展示如何关闭钩子来确保清理代码总是能够执行,无论用户如何终止程序。
在java中,虚拟机会对两类事件进行响应,然后执行关闭操作,
- 当调用System.exit()方法或者程序的最后一个非守护进程线程退出时,应用程序正常退出
- 用户突然强制虚拟机中断运行,例如用户按CTRL+C快捷键或在为关闭Java程序的情况下,从系统中退出。
虚拟机在执行关闭操作时,会经过以下两个阶段
- 虚拟机启动所有已经注册的关闭钩子,如果有的话,关闭钩子是先前已经通过Runtime类注册的线程,所有的关闭钩子会并发执行,直到完成任务
- 虚拟机根据情况调用所有没有被调用过的终结器(finalizer)
下面重点说明第一个阶段,因为该阶段允许程序员告诉虚拟机在应用程序中执行一些清理代码。关闭钩子 很简单,只是 java.lang.Thread类的一个子类实例,创建关闭钩子也很简单:
- 创建Thread类的一个子类
- 实现你自己的run方法,当应用程序(正常或者突然)关闭时,会调用此方法
- 在应用程序中,实例化 关闭钩子类
- 使用当前Runtime类的addShutdownHook方法注册关闭钩子
也许你已经注意到了,不需要像启动线程一样调用关闭钩子的start方法,虚拟机会在它运行其关闭程序时启动并执行关闭钩子。
下面定义了一个简单的ShutdownHookDemo类和一个Thread类(ShutdownHookDemo类)的子类,,注意,ShutdownHook类的run方法只是简单的将字符串“Shutting down”输出到控制台上,但是可以插入想在应用程序关闭之前的任何代码。
1 package myex16.pyrmont.shutdownhook; 2 3 import java.io.IOException; 4 5 /** 6 * <p> 7 * <b>Title:ShutdownHookDemo.java</b> 8 * </p> 9 * <p> 10 * Copyright:ChenDong 2018 11 * </p> 12 * <p> 13 * Company:仅学习时使用 14 * </p> 15 * <p> 16 * 类功能描述: 演示 关闭钩子 的简单实用 17 * </p> 18 * 19 * @author 陈东 20 * @date 2018年12月24日 下午8:01:14 21 * @version 1.0 22 */ 23 public class ShutdownHookDemo { 24 25 public void start() { 26 System.out.println("Demo start"); 27 // 创建关闭钩子 就是线程 28 ShutdownHook shutdownHook = new ShutdownHook(); 29 // 像虚拟机中注册关闭钩子 30 Runtime.getRuntime().addShutdownHook(shutdownHook); 31 } 32 33 /** 34 * 35 * <p> 36 * Title: main 37 * </p> 38 * 39 * @date 2018年12月24日 下午8:01:15 40 * 41 * <p> 42 * 功能描述: 43 * </p> 44 * 45 * @param args 46 * 47 */ 48 public static void main(String[] args) { 49 ShutdownHookDemo demo = new ShutdownHookDemo(); 50 demo.start(); 51 52 try { 53 // 等待输入 这样 线程就不会走完 然后只要随便输入东西 就会 走完流程 测试在线程运行完之后 虚拟机启动我们注册的关闭钩子 并运行 54 System.in.read(); 55 } catch (IOException e) { 56 // TODO Auto-generated catch block 57 e.printStackTrace(); 58 } 59 } 60 61 } 62 63 class ShutdownHook extends Thread { 64 public void run() { 65 System.out.println("Shutting down"); 66 } 67 }
运行结果
Demo start
输入了东西
Shutting down
在实例化ShutdownHookDemo类后,main方法会调用start方法,start方法会创建一个关闭钩子,并通过RunTime来注册它:
// 创建关闭钩子 就是线程 28 ShutdownHook shutdownHook = new ShutdownHook(); 29 // 像虚拟机中注册关闭钩子 30 Runtime.getRuntime().addShutdownHook(shutdownHook);
然后,应用程序会等待用户输入
System.in.read();
当用户按Enter键时,应用程序退出,但是虚拟机会执行关闭钩子,效果是输出字符串“Shutting down”。
关闭钩子的例子
现在看另一个例子,这是一个简单的Swing应用程序,其类的名字MySwingApp,效果如图
该应用程序会在它启动时创建一个临时文件,并在关闭时删除该临时文件。
1 package myex16.pyrmont.shutdownhook; 2 3 import java.awt.Rectangle; 4 import java.awt.event.ActionEvent; 5 import java.io.File; 6 import java.io.IOException; 7 8 import javax.swing.JButton; 9 import javax.swing.JFrame; 10 import javax.swing.JTextArea; 11 12 /** 13 * <p> 14 * <b>Title:MySwingApp.java</b> 15 * </p> 16 * <p> 17 * Copyright:ChenDong 2018 18 * </p> 19 * <p> 20 * Company:仅学习时使用 21 * </p> 22 * <p> 23 * 类功能描述:演示 关闭钩子的使用 24 * </p> 25 * 26 * @author 陈东 27 * @date 2018年12月24日 下午8:27:54 28 * @version 1.0 29 */ 30 public class MySwingApp extends JFrame { 31 32 JButton exitButton = new JButton(); 33 34 JTextArea jTextArea1 = new JTextArea(); 35 36 String dir = System.getProperty("user.dir"); 37 String filename = "temp.txt"; 38 39 public MySwingApp() { 40 exitButton.setText("Exit"); 41 exitButton.setBounds(new Rectangle(304, 248, 76, 37)); 42 exitButton.addActionListener(new java.awt.event.ActionListener() { 43 44 @Override 45 public void actionPerformed(ActionEvent e) { 46 exitButton_actionPerformed(e); 47 } 48 49 }); 50 51 this.getContentPane().setLayout(null); 52 jTextArea1.setText("Click the Exit button to quit"); 53 jTextArea1.setBounds(new Rectangle(9, 7, 371, 235)); 54 this.getContentPane().add(exitButton, null); 55 this.getContentPane().add(jTextArea1, null); 56 this.setDefaultCloseOperation(EXIT_ON_CLOSE); 57 this.setBounds(0, 0, 400, 330); 58 this.setVisible(true); 59 initialize(); 60 } 61 62 private void initialize() { 63 // 创建一个temp.txt文件 64 File file = new File(dir, filename); 65 try { 66 System.out.println("Creating temporary file"); 67 file.createNewFile(); 68 } catch (IOException e) { 69 System.out.println("Failed creating temporary file."); 70 } 71 } 72 73 /** 74 * 75 * <p> 76 * Title: main 77 * </p> 78 * 79 * @date 2018年12月24日 下午8:27:54 80 * 81 * <p> 82 * 功能描述: 83 * </p> 84 * 85 * @param args 86 * 87 */ 88 public static void main(String[] args) { 89 90 MySwingApp mySwingApp = new MySwingApp(); 91 } 92 93 private void shutdown() { 94 // 删除这个文件 95 File file = new File(dir, filename); 96 if (file.exists()) { 97 System.out.println("Deleting temporary file."); 98 file.delete(); 99 } 100 } 101 102 void exitButton_actionPerformed(ActionEvent e) { 103 shutdown(); 104 System.exit(0); 105 } 106 107 }
在实例化这个类时,应用程序会调用其initialize方法,然后initialize方法会在用户目录下创建一个临时文件,名为"temp.txt"
private void initialize() { // 创建一个temp.txt文件 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方法,也就可以删除临时文件了,但是如果用户是通过点击右上角的关闭按钮或者是通过其他方法退出的,临时文件就无法删除了,
下面给的类提供了这个问题的解决方案,使用关闭钩子来删除临时文件,关闭钩子的类是一个内部类,这样它就访问主类的所有方法了,在下面代码中,关闭钩子的run方法会调用shutdown方法,保证在虚拟机关闭时会调用shutdown方法。
package myex16.pyrmont.shutdownhook; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.io.File; import java.io.IOException; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JTextArea; /** * <p> * <b>Title:MySwingApp.java</b> * </p> * <p> * Copyright:ChenDong 2018 * </p> * <p> * Company:仅学习时使用 * </p> * <p> * 类功能描述:演示 关闭钩子的使用 * </p> * * @author 陈东 * @date 2018年12月24日 下午8:27:54 * @version 1.0 */ 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() { @Override 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() { MyShutdownHook hook = new MyShutdownHook(); Runtime.getRuntime().addShutdownHook(hook); // 创建一个temp.txt文件 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."); } } /** * * <p> * Title: main * </p> * * @date 2018年12月24日 下午8:27:54 * * <p> * 功能描述: * </p> * * @param args * */ @SuppressWarnings("unused") public static void main(String[] args) { MySwingApp mySwingApp = new MySwingApp(); } private void shutdown() { // 删除这个文件 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); } @SuppressWarnings("unused") private class MyShutdownHook extends Thread { public void run() { shutdown(); } } }
注意 这次的initialize方法,他首先会创建内部类MyShutdownHook的一个实例,该类继承自java.lang.Thread类
MyShutdownHook hook = new MyShutdownHook();
一旦获得了MyShutdownHook类的实例后。就需要将其值传给Rutime类的addShutDownhook方法
Runtime.getRuntime().addShutdownHook(hook);
initialize方法剩余代码就与上一个示例类似了 创建临时文件,
现在启动上面代码,检查一下,当突然关闭应用程序时,是否总是删除临时文件。
注意:关闭钩子 的run方法总会执行,
将上面例子中的关闭钩子的run方法替换一下
@SuppressWarnings("unused")
private class MyShutdownHook extends Thread {
public void run() {
System.out.println("关闭钩子执行了");
shutdown();
}
}
然后在执行示例,在通过点击按钮正常退出时输出如下
Creating temporary file
Deleting temporary file.
关闭钩子执行了
通过点击 右上角的 X 关闭输出如下
Creating temporary file
关闭钩子执行了
Deleting temporary file.
注意一下 钩子run执行的 顺序
- 第一种情况:正常关闭时 是在 执行完shutdown方法之后 虚拟机执行的 关闭钩子
- 第二种: 非正常时,是在发生被点击X 之后, 虚拟机执行的关闭钩子
只要有关闭钩子 那么除非进行注销 否则一定会被执行
Tomcat中的关闭钩子
那么重点来了 既然在Tomcat学习中将这个肯定是 ,Tomcat也用到了关闭钩子来完成退出过程的,在 org,apache.catalina.startup.Catalina类中,可以找到这样的代码,Catalina类负责启动管理其他组件的Srver对象。一个名为CatalinaShutdownHook的内部类继承自Thread类,提供了run方法的实现,它调用server对象的stop方法,执行关闭操作,
/** * * 关闭钩,这将执行清洁关闭 Catalina */ 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实例启动时,会实例化关闭钩子,并在一个阶段将其添加到Rutime类中,
有时候,应用程序在关闭之前应该执行一些代码清理工作,但是你不能价设定用户总是正常退出,那么这次介绍的关闭钩子提供了一种解决方案,确保无论用户如何关闭应用程序,清理代码总是能得到执行。