在很多實際應用環境中,當用戶關了應用程序時,需要做一些善后清理工作,但問題是,用戶有時並不會按照推薦的方法關閉應用程序,很有可能不做清理工作,例如在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 之后, 虛擬機執行的關閉鈎子
只要有關閉鈎子 那么除非進行注銷 否則一定會被執行
Runtime.getRuntime().removeShutdownHook(shutdownHook);
從Rutime中移除 關閉鈎子,
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類中,
有時候,應用程序在關閉之前應該執行一些代碼清理工作,但是你不能價設定用戶總是正常退出,那么這次介紹的關閉鈎子提供了一種解決方案,確保無論用戶如何關閉應用程序,清理代碼總是能得到執行。