第 18 章 捨遺補缺

這一個章節的目的在於捨遺補缺,對於之前章節中有提到但沒說明的 Date 與日誌(Log)的 API,在這個章節中會加以說明,另外也將介紹訊息資源綁定,這讓您可以隨時調整文字訊息而不用重新編譯程式。


18.1 日期、時間

表面上看來,要取得系統的時間只要使用 java.util.Date 類別就可以了,但查閱 Date 後,發現很多方法都被標示為 “deprecated”?這是怎麼回事?事實上有些方法建議您改用 java.util.Calendar 上的一些對應方法了。

18.1.1 使用 Date

如果想要取得系統的時間,可以使用 System.currentTimeMillis() 方法,範例 18.1 可以取得系統目前的時間。

範例 18.1 CurrentTime.java

  1. package onlyfun.caterpillar;
  2. public class CurrentTime {
  3. public static void main(String[] args) {
  4. System.out.println("現在時間 "
  5. + System.currentTimeMillis());
  6. }
  7. }

執行結果:

  1. 現在時間 1118066309062

執行結果是一長串數字,這數字實際上表示從 1970 年 1 月 1 日 0 時 0 分 0 秒開始,到程式執行取得系統時間為止所經過的毫秒數,但這樣的數字讓人來看實在不太有意義,您可以使用 java.util.Date 類別來讓這個數字變的更有意義一些,範例 18.2 示範如何使用 Date 取得系統時間。

範例 18.2 DateDemo.java

  1. package onlyfun.caterpillar;
  2. import java.util.Date;
  3. public class DateDemo {
  4. public static void main(String[] args) {
  5. Date date = new Date();
  6. System.out.println("現在時間 "
  7. + date.toString());
  8. System.out.println("自1970/1/1至今的毫秒數 "
  9. + date.getTime());
  10. }
  11. }

執行結果:

  1. 現在時間 Mon Jun 06 22:03:52 GMT+08:00 2005
  2. 1970/1/1至今的毫秒數 1118066632890

當您生成 Date 物件時,會使用 System.currentTimeMillis() 來取得系統時間,而您使用 Date 實例的 toString() 方法時,會將取得的 1970 年 1 月 1 日至今的毫秒數轉為「星期 月 日 時:分:秒 西元」的格式;使用 Date 的 getTime() 方法則可以取得實際經過的毫秒數。

如果您想要對日期時間作格式設定,則可以使用 java.text.DateFormat 來作格式化,先來看看 DateFormat 的子類別 java.text.SimpleDateFormat 是如何使用的,範例 18.3 是個簡單示範。

範例 18.3 DateFormatDemo.java

  1. package onlyfun.caterpillar;
  2. import java.text.DateFormat;
  3. import java.text.SimpleDateFormat;
  4. import java.util.Date;
  5. public class DateFormatDemo {
  6. public static void main(String[] args) {
  7. Date date = new Date();
  8. DateFormat dateFormat =
  9. new SimpleDateFormat("EE-MM-dd-yyyy");
  10. System.out.println(dateFormat.format(date));
  11. }
  12. }

執行結果:

  1. 星期一-06-06-2005

DateFormat 會依電腦上的區域(Locale)設定顯示時間格式,”EE” 表示星期格式設定,”MM” 表示月份格式設定、”dd” 表示日期格式設定,而 “yyyy” 是西元格式設定,每個字元的設定都各有其意義,您可以參考 java.util.SimpleDateFormat 的 API 說明瞭解每個字元設定的意義。

您也可以直接使用 DateFormat 上的靜態 getDateTimeInstance() 方法來指定格式,getDateTimeInstance() 方法會傳回 DateFormat 的實例,之後可以重複使用這個實例來格式化 Date 物件,如範例 18.4 所示範的。

範例 18.4 DateTimeInstanceDemo.java

  1. package onlyfun.caterpillar;
  2. import java.text.DateFormat;
  3. import java.util.Date;
  4. public class DateTimeInstanceDemo {
  5. public static void main(String[] args) {
  6. // 取得目前時間
  7. Date date = new Date();
  8. // 簡短資訊格式
  9. DateFormat shortFormat =
  10. DateFormat.getDateTimeInstance(
  11. DateFormat.SHORT, DateFormat.SHORT);
  12. // 中等資訊格式
  13. DateFormat mediumFormat =
  14. DateFormat.getDateTimeInstance(
  15. DateFormat.MEDIUM, DateFormat.MEDIUM);
  16. // 長資訊格式
  17. DateFormat longFormat =
  18. DateFormat.getDateTimeInstance(
  19. DateFormat.LONG, DateFormat.LONG);
  20. // 詳細資訊格式
  21. DateFormat fullFormat =
  22. DateFormat.getDateTimeInstance(
  23. DateFormat.FULL, DateFormat.FULL);
  24. System.out.println("簡短資訊格式:" +
  25. shortFormat.format(date));
  26. System.out.println("中等資訊格式:" +
  27. mediumFormat.format(date));
  28. System.out.println("長資訊格式:" +
  29. longFormat.format(date));
  30. System.out.println("詳細資訊格式:" +
  31. fullFormat.format(date));
  32. }
  33. }

在使用 getDateTimeInstance() 取得 DateFormat 實例時,可以指定的引數是日期格式與時間格式,以上所指定的格式依訊息的詳細程度區分,執行結果如下:

  1. 簡短資訊格式:2005/6/6 下午 10:19
  2. 中等資訊格式:2005/6/6 下午 10:19:13
  3. 長資訊格式:200566 下午101913
  4. 詳細資訊格式:200566 星期一 下午101913 GMT+08:00

您也可以使用 getDateInstance() 取得 DateFormat 實例,並同時指定日期的區域顯示方式,指定時要使用一個 java.util.Locale(18.3.3 會介紹)實例作為引數,這邊改寫範例 18.4 為範例 18.5,讓程式改以美國的時間表示方式來顯示。

範例 18.5 DateTimeInstanceDemo2.java

  1. package onlyfun.caterpillar;
  2. import java.text.DateFormat;
  3. import java.util.Date;
  4. import java.util.Locale;
  5. public class DateTimeInstanceDemo2 {
  6. public static void main(String[] args) {
  7. // 取得目前時間
  8. Date date = new Date();
  9. // en: 英語系 US: 美國
  10. Locale locale = new Locale("en", "US");
  11. // 簡短資訊格式
  12. DateFormat shortFormat =
  13. DateFormat.getDateTimeInstance(
  14. DateFormat.SHORT, DateFormat.SHORT, locale);
  15. // 中等資訊格式
  16. DateFormat mediumFormat =
  17. DateFormat.getDateTimeInstance(
  18. DateFormat.MEDIUM, DateFormat.MEDIUM, locale);
  19. // 長資訊格式
  20. DateFormat longFormat =
  21. DateFormat.getDateTimeInstance(
  22. DateFormat.LONG, DateFormat.LONG, locale);
  23. // 詳細資訊格式
  24. DateFormat fullFormat =
  25. DateFormat.getDateTimeInstance(
  26. DateFormat.FULL, DateFormat.FULL, locale);
  27. System.out.println("short format: " +
  28. shortFormat.format(date));
  29. System.out.println("media format: " +
  30. mediumFormat.format(date));
  31. System.out.println("long format: " +
  32. longFormat.format(date));
  33. System.out.println("full format: " +
  34. fullFormat.format(date));
  35. }
  36. }

執行結果:

  1. short format: 6/6/05 10:29 PM
  2. media format: Jun 6, 2005 10:29:30 PM
  3. long format: June 6, 2005 10:29:30 PM GMT+08:00
  4. full format: Monday, June 6, 2005 10:29:30 PM GMT+08:00

18.1.2 使用 Calendar

您可以使用 Date 來取得完整的日期時間並使用 toString() 顯示日期的文字描述,但如果您想要單獨取得某個時間或日期資訊的話該如何進行?例如您想知道現在的時間是 6 月的第幾天?

您要使用 java.util.Calendar 類別,但在使用這個類別上的一些方法之前,您要先知道 Calendar 的一些方法會取回 int 型態數字,而取回的數字其實是對應於 Calendar 中定義的常數,所以並不是您使用某個方法取得 1 這個數字,就會表示取得的時間是星期一。

想要取得現在的時間,首先要使用 Calendar 的 getInstance() 方法取得一個 Calendar 的實例,例如:

  1. Calendar rightNow = Calendar.getInstance();

一個 Calendar 的實例是系統時間的抽象表示,您可以操作這個實例上的 get() 方法來取得您想要知道的時間資訊,例如您想知道現在是西元幾年,則可以使用 get() 方法並指定對應的常數,例如:

  1. System.out.println(rightNow.get(Calendar.YEAR));

如果現在是2005年,則上例會顯示 2005 的數字,依照這個例子,假設現在的時間是 5 月份,而您現在想使用程式取得現在的月份,則下例您可能會有些困惑:

  1. System.out.println(rightNow.get(Calendar.MONTH));

上面的程式片段會顯示 4 這個數字,而不是您預期的 5,事實上傳回的 4 並不是代表目前時間是 4 月份,而是對應於 Calendar.MAY 常數的值,Calendar 在月份上的常數值從一月 Calendar.JANUARY 開始是 0,到十二月 Calendar.DECEMBER 的 11,所以您如果想要顯示傳回值的真正意涵,可以如下撰寫:

  1. String[] months = {"一月", "二月", "三月", "四月",
  2. "五月", "六月", "七月", "八月",
  3. "九月", "十月", "十一月", "十二月"};
  4. Calendar rightNow = Calendar.getInstance();
  5. int monthConstant = rightNow.get(Calendar.MONTH);
  6. System.out.println(months[monthConstant]);

同樣的,如果您想要取得星期資訊,要記得 Calendar 的星期常數從星期日 Calendar.SUNDAY 是 1,到星期六 Calendar.SATURDAY 是 7,由於對應的數並不是從0開始,所以如果要使用陣列來對應的話,第一個陣列的元素值就不包括資訊,例如:

  1. String[] dayOfWeek = {"", "日", "一", "二",
  2. "三", "四", "五", "六"};
  3. Calendar rightNow = Calendar.getInstance();
  4. int dayOfWeekConstant = rightNow.get(Calendar.DAY_OF_WEEK);
  5. System.out.println(dayOfWeek[dayOfWeekConstant]);

總之您要記得 get() 傳回的 int 值是對應於 Calendar 的某個常數,如果怕搞混,使用 switch 來比對是個不錯的作法,範例 18.6 是一個實際應用的參考,可以顯示中文的星期與月份。

範例 18.6 CalendarDemo.java

  1. package onlyfun.caterpillar;
  2. import java.util.Calendar;
  3. public class CalendarDemo {
  4. public static void main(String[] args) {
  5. Calendar rightNow = Calendar.getInstance();
  6. System.out.println("現在時間是:");
  7. System.out.println("西元:" +
  8. rightNow.get(Calendar.YEAR));
  9. System.out.println("月:" +
  10. getChineseMonth(rightNow));
  11. System.out.println("日:" +
  12. rightNow.get(Calendar.DAY_OF_MONTH));
  13. System.out.println("星期:" +
  14. getChineseDayOfWeek(rightNow));
  15. }
  16. public static String getChineseMonth(Calendar rightNow) {
  17. String chineseMonth = null;
  18. switch(rightNow.get(Calendar.MONTH)) {
  19. case Calendar.JANUARY:
  20. chineseMonth = "一";
  21. break;
  22. case Calendar.FEBRUARY:
  23. chineseMonth = "二";
  24. break;
  25. case Calendar.MARCH:
  26. chineseMonth = "三";
  27. break;
  28. case Calendar.APRIL:
  29. chineseMonth = "四";
  30. break;
  31. case Calendar.MAY:
  32. chineseMonth = "五";
  33. break;
  34. case Calendar.JUNE:
  35. chineseMonth = "六";
  36. break;
  37. case Calendar.JULY:
  38. chineseMonth = "七";
  39. break;
  40. case Calendar.AUGUST:
  41. chineseMonth = "八";
  42. break;
  43. case Calendar.SEPTEMBER:
  44. chineseMonth = "九";
  45. break;
  46. case Calendar.OCTOBER:
  47. chineseMonth = "十";
  48. break;
  49. case Calendar.NOVEMBER:
  50. chineseMonth = "十一";
  51. break;
  52. case Calendar.DECEMBER:
  53. chineseMonth = "十二";
  54. break;
  55. }
  56. return chineseMonth;
  57. }
  58. public static String getChineseDayOfWeek(
  59. Calendar rightNow) {
  60. String chineseDayOfWeek = null;
  61. switch(rightNow.get(Calendar.DAY_OF_WEEK)) {
  62. case Calendar.SUNDAY:
  63. chineseDayOfWeek = "日";
  64. break;
  65. case Calendar.MONDAY:
  66. chineseDayOfWeek = "一";
  67. break;
  68. case Calendar.TUESDAY:
  69. chineseDayOfWeek = "二";
  70. break;
  71. case Calendar.WEDNESDAY:
  72. chineseDayOfWeek = "三";
  73. break;
  74. case Calendar.THURSDAY:
  75. chineseDayOfWeek = "四";
  76. break;
  77. case Calendar.FRIDAY:
  78. chineseDayOfWeek = "五";
  79. break;
  80. case Calendar.SATURDAY:
  81. chineseDayOfWeek = "六";
  82. break;
  83. }
  84. return chineseDayOfWeek;
  85. }
  86. }

執行結果:

  1. 現在時間是:
  2. 西元:2005
  3. 月:六
  4. 日:6
  5. 星期:一

事實上在 Java SE 6 中,對於 Calendar 新增了顯示名稱(Display Names),可以讓您直接根據 Locale 物件,顯示相對應的區域化訊息,詳細的介紹請目第 21 章關於 Java SE 6 新功能的介紹。

18.2 日誌(Logging)

如果您只是要作一些簡單的文件日誌(Log),可以考慮內建在 JDK 中的日誌 API,功能或許簡單了一些,但好處是它從 J2SE 1.4 之後就成為 Java SE 標準 API 的一員。

18.2.1 簡介日誌

程式中不免會出現錯誤,當錯誤發生時,您可以使用 System.err.println() 或是 System.out.println() 在文字模式(Console)中顯示訊息給使用者,如果是在視窗程式中,可能是使用訊息方塊,如果是在網頁程式中,則顯示一個錯誤訊息頁面,除了提供錯誤訊息之外,您可能還想將錯誤訊息以某種方式儲存下來,以供使用者或是程式人員除蟲(Debug)時使用。

在 Java SE 中的 java.util.logging 套件提供了一系列的日誌工具類別,如果您只是要簡單的記錄一些日誌訊息,就可以使用這些工具類別,它們在 J2SE 1.4 之後加入了標準 API 中,不必額外配置日誌元件就可以運行於標準的 Java 平台上是它的好處,無論是在文字模式簡單的顯示訊息,或是在檔案中輸出日誌,java.util.logging 下都提供了一些預設的工具類別。

要使用 Java SE 的日誌功能,首先要取得 java.util.logging.Logger 實例,這可以透過 Logger 類別的靜態 getLogger() 方法來取得,看看範例 18.7 的例子,假設您的程式在啟動時必須提供引數給程式,如果使用者沒有提供引數則必須顯示警示訊息。

範例 18.7 LoggingDemo.java

  1. package onlyfun.caterpillar;
  2. import java.util.logging.*;
  3. public class LoggingDemo {
  4. public static void main(String[] args) {
  5. Logger logger = Logger.getLogger("LoggingDemo");
  6. try {
  7. System.out.println(args[0]);
  8. }
  9. catch(ArrayIndexOutOfBoundsException e) {
  10. logger.warning("沒有提供執行時的引數!");
  11. }
  12. }
  13. }

執行結果:

  1. 2005/6/6 下午 11:52:19 onlyfun.caterpillar.LoggingDemo main
  2. 警告: 沒有提供執行時的引數!

您簡單的透過 Logger 的靜態方法 getLogger() 取得 Logger 物件,給 getLogger() 的字串引數被用來作為 Logger 物件的名稱,之後就可以運用 Logger 實例上的方法進行訊息輸出,預設是從文字模式輸出訊息,除了您提供的文字訊息之外,Logger 還自動幫您收集了執行時相關的訊息,像是訊息輸出所在的類別與套件名稱,執行該段程式時的執行緒名稱以及訊息產生的時間等,這比下面這個程式所提供的訊息豐富的多了:

  1. package onlyfun.caterpillar;
  2. public class LoggingDemo {
  3. public static void main(String[] args) {
  4. try {
  5. System.out.println(args[0]);
  6. }
  7. catch(ArrayIndexOutOfBoundsException e) {
  8. System.err.println("沒有提供執行時的引數!");
  9. }
  10. }
  11. }

所以您不用親自實作日誌功能的程式,只要直接使用 java.util.logging 工具,就可以擁有一些預設的日誌功能,對於中大型系統來說,java.util.logging 套件下所提供的工具在功能上可能不足,但作為一個簡單的日誌工具,java.util.logging 套件下所提供的工具是可以考慮的方案。

18.2.2 日誌的等級

在進行訊息的日誌記錄時,依訊息程度的不同,您會設定不同等級的訊息輸出,您可以透過操作 Logger 上的幾個方法來得到不同等級的訊息輸出,範例 18.8 示範了各種等級的訊息輸出。

範例 18.8 LoggingLevelDemo.java

  1. package onlyfun.caterpillar;
  2. import java.util.logging.*;
  3. public class LoggingLevelDemo {
  4. public static void main(String[] args) {
  5. Logger logger = Logger.getLogger("loggingLevelDemo");
  6. logger.severe("嚴重訊息");
  7. logger.warning("警示訊息");
  8. logger.info("一般訊息");
  9. logger.config("設定方面的訊息");
  10. logger.fine("細微的訊息");
  11. logger.finer("更細微的訊息");
  12. logger.finest("最細微的訊息");
  13. }
  14. }

執行結果:

  1. 2005/6/7 上午 09:10:39 onlyfun.caterpillar.LoggingLevelDemo main
  2. 嚴重的: 嚴重訊息
  3. 2005/6/7 上午 09:10:39 onlyfun.caterpillar.LoggingLevelDemo main
  4. 警告: 警示訊息
  5. 2005/6/7 上午 09:10:39 onlyfun.caterpillar.LoggingLevelDemo main
  6. 資訊: 一般訊息

依照不同的等級,您可以採用不同的訊息等級方法,程式中依程度高低撰寫,注意到 config() 方法及以下的訊息並沒有顯示出來,這是因為 Logger 的預設等級是 INFO,比這個等級更低的訊息,Logger 並不會將訊息輸出。

Logger 的預設等級是定義在執行環境的屬性檔 logging.properties 中,這個檔案位於 JRE 安裝目錄的 lib 目錄下,部份內容如下:

  1. handlers= java.util.logging.ConsoleHandler
  2. .level= INFO
  3. java.util.logging.ConsoleHandler.level = INFO

Logger 預設的處理者(Handler)是 java.util.logging.ConsolerHandler,也就是將訊息輸出至文字模式,一個 Logger 可以擁有多個處理者,每個處理者可以有自己的訊息等級,在通過 Logger 的等級限制後,實際上還要再經過處理者的等級限制,所以在範例 18.8 中如果想要看到所有的訊息,則必須同時設定 Logger 與 ConsolerHandler 的等級,範例 18.9 示範了如何設定。

範例 18.9 LoggingLevelDemo2.java

  1. package onlyfun.caterpillar;
  2. import java.util.logging.*;
  3. public class LoggingLevelDemo2 {
  4. public static void main(String[] args) {
  5. Logger logger = Logger.getLogger("loggingLevelDemo2");
  6. // 顯示所有等級的訊息
  7. logger.setLevel(Level.ALL);
  8. ConsoleHandler consoleHandler = new ConsoleHandler();
  9. // 顯示所有等級的訊息
  10. consoleHandler.setLevel(Level.ALL);
  11. // 設定處理者為ConsoleHandler
  12. logger.addHandler(consoleHandler);
  13. logger.severe("嚴重訊息");
  14. logger.warning("警示訊息");
  15. logger.info("一般訊息");
  16. logger.config("設定方面的訊息");
  17. logger.fine("細微的訊息");
  18. logger.finer("更細微的訊息");
  19. logger.finest("最細微的訊息");
  20. }
  21. }

Level.ALL 表示顯示所有的訊息,所以這一次的執行結果可顯示所有等級的訊息,如下所示:

  1. 2005/6/7 上午 09:17:37 onlyfun.caterpillar.LoggingLevelDemo2 main
  2. 嚴重的: 嚴重訊息
  3. 2005/6/7 上午 09:17:37 onlyfun.caterpillar.LoggingLevelDemo2 main
  4. 嚴重的: 嚴重訊息
  5. 2005/6/7 上午 09:17:37 onlyfun.caterpillar.LoggingLevelDemo2 main
  6. 警告: 警示訊息
  7. 2005/6/7 上午 09:17:37 onlyfun.caterpillar.LoggingLevelDemo2 main
  8. 警告: 警示訊息
  9. 2005/6/7 上午 09:17:37 onlyfun.caterpillar.LoggingLevelDemo2 main
  10. 資訊: 一般訊息
  11. 2005/6/7 上午 09:17:37 onlyfun.caterpillar.LoggingLevelDemo2 main
  12. 資訊: 一般訊息
  13. 2005/6/7 上午 09:17:37 onlyfun.caterpillar.LoggingLevelDemo2 main
  14. 配置: 設定方面的訊息
  15. 2005/6/7 上午 09:17:37 onlyfun.caterpillar.LoggingLevelDemo2 main
  16. 細緻: 細微的訊息
  17. 2005/6/7 上午 09:17:37 onlyfun.caterpillar.LoggingLevelDemo2 main
  18. 更細緻: 更細微的訊息
  19. 2005/6/7 上午 09:17:37 onlyfun.caterpillar.LoggingLevelDemo2 main
  20. 最細緻: 最細微的訊息

如果您想要關閉所有的訊息,可以設定為 Level.OFF。

Logger 的 severe()、warning()、info() 等方法,實際上是個便捷的方法,您也可以直接使用 log() 方法並指定等級來執行相同的作用,例如範例 18.10 的執行結果與範例 18.9 是一樣的。

範例 18.10 LoggingLevelDemo3.java

  1. package onlyfun.caterpillar;
  2. import java.util.logging.*;
  3. public class LoggingLevelDemo3 {
  4. public static void main(String[] args) {
  5. Logger logger = Logger.getLogger("loggingLevelDemo3");
  6. logger.setLevel(Level.ALL);
  7. ConsoleHandler consoleHandler = new ConsoleHandler();
  8. consoleHandler.setLevel(Level.ALL);
  9. logger.addHandler(consoleHandler);
  10. logger.log(Level.SEVERE, "嚴重訊息");
  11. logger.log(Level.WARNING, "警示訊息");
  12. logger.log(Level.INFO, "一般訊息");
  13. logger.log(Level.CONFIG, "設定方面的訊息");
  14. logger.log(Level.FINE, "細微的訊息");
  15. logger.log(Level.FINER, "更細微的訊息");
  16. logger.log(Level.FINEST, "最細微的訊息");
  17. }
  18. }

18.2.3 Handler、Formatter

Logger 預設的輸出處理者(Handler)是 ConsolerHandler,ConsolerHandler 的輸出是使用 System.err 物件,而訊息的預設等級是 INFO,這可以在 JRE 安裝目錄下 lib 目錄的 logging.properties 中看到:

  1. handlers= java.util.logging.ConsoleHandler
  2. java.util.logging.ConsoleHandler.level = INFO

Java SE 提供了五個預設的 Handler:

  • java.util.logging.ConsoleHandler

    以System.err輸出日誌。

  • java.util.logging.FileHandler

    將訊息輸出至檔案。

  • java.util.logging.StreamHandler

    以指定的OutputStream實例輸出日誌。

  • java.util.logging.SocketHandler

    將訊息透過Socket傳送至遠端主機。

  • java.util.logging.MemoryHandler

    將訊息暫存在記憶體中。

範例 18.11 示範如何使用 FileHandler 將訊息輸出至檔案中。

範例 18.11 HandlerDemo.java

  1. package onlyfun.caterpillar;
  2. import java.io.IOException;
  3. import java.util.logging.*;
  4. public class HandlerDemo {
  5. public static void main(String[] args) {
  6. Logger logger = Logger.getLogger("handlerDemo");
  7. try {
  8. FileHandler fileHandler =
  9. new FileHandler("%h/myLogger.log");
  10. logger.addHandler(fileHandler);
  11. logger.info("測試訊息");
  12. } catch (SecurityException e) {
  13. e.printStackTrace();
  14. } catch (IOException e) {
  15. e.printStackTrace();
  16. }
  17. }
  18. }

執行的結果會在文字模式顯示訊息,並將結果輸出至檔案中,在指定輸出的檔案名稱時,您可以使用 ‘%h’ 來表示使用者的家(home)目錄(Windows 系統中的使用者家目錄是在 “\Documents and Settings\使用者名稱” 目錄中),您還可以使用 ‘%t’ 取得系統的暫存目錄,或者是加入 ‘%g’ 自動為檔案編號,例如可以設定為 “%h/myLogger%g.log”,表示將 .log 檔儲存在使用者家目錄中,並自動為每一個檔案增加編號,範例 18.11 的檔案輸出的內容如下:

  1. <?xml version="1.0" encoding="x-windows-950" standalone="no"?>
  2. <!DOCTYPE log SYSTEM "logger.dtd">
  3. <log>
  4. <record>
  5. <date>2005-06-07T09:25:52</date>
  6. <millis>1118107552609</millis>
  7. <sequence>0</sequence>
  8. <logger>handlerDemo</logger>
  9. <level>INFO</level>
  10. <class>onlyfun.caterpillar.HandlerDemo</class>
  11. <method>main</method>
  12. <thread>10</thread>
  13. <message>測試訊息</message>
  14. </record>
  15. </log>

預設上 FileHandler 的輸出格式是 XML 格式,輸出格式是由 java.util.logging.Formatter 來控制,例如 FileHandler 的預設格式是 java.util.logging.XMLFormatter,而 ConsolerHandler 的預設格式是 java.util.logging.SimpleFormatter,您可以使用 Handler 實例的 setFormatter() 方法來設定訊息的輸出格式,例如:

  1. fileHandler.setFormatter(new SimpleFormatter());

如果 FileHandler 的 Formatter 設定為 SimpleFormatter,則輸出的日誌檔內容就是簡單的文字訊息,打開檔案看的話是與文字模式下看到的訊息內容相同。

18.2.4 自訂 Formatter

除了 XMLFormatter 與 SimpleFormatter 之外,您也可以自訂日誌的輸出格式,只要繼承抽象類別 Formatter,並重新定義其 format() 方法即可,format() 方法會傳入一個 java.util.logging.LogRecord 物件作為引數,您可以使用它來取得一些與程式執行有關的資訊。

範例 18.12 是個簡單的示範,當中自訂了一個簡單的 TableFormatter。

範例 18.12 TableFormatter.java

  1. package onlyfun.caterpillar;
  2. import java.util.logging.*;
  3. public class TableFormatter extends Formatter {
  4. public String format(LogRecord logRecord) {
  5. return
  6. "LogRecord info: " +
  7. logRecord.getSourceClassName() + "\n" +
  8. "Level\t|\tLoggerName\t|\tMessage\t|\n" +
  9. logRecord.getLevel() + "\t|\t" +
  10. logRecord.getLoggerName() + "\t|\t" +
  11. logRecord.getMessage() + "\t|\n\n";
  12. }
  13. }

自訂好 Formatter 之後,接著就可以使用 Handler 實例的 setFormatter() 方法設定 Formatter 物件,如範例 18.13 所示。

範例 18.13 TableFormatterDemo.java

  1. package onlyfun.caterpillar;
  2. import java.util.logging.*;
  3. public class TableFormatterDemo {
  4. public static void main(String[] args) {
  5. Logger logger = Logger.getLogger("tableFormatter");
  6. try {
  7. for(Handler h : logger.getParent().getHandlers()) {
  8. if(h instanceof ConsoleHandler) {
  9. h.setFormatter(new TableFormatter());
  10. }
  11. }
  12. logger.info("訊息1");
  13. logger.warning("訊息2");
  14. } catch (SecurityException e) {
  15. e.printStackTrace();
  16. }
  17. }
  18. }

您取得預設的根(Root)Logger,並判斷出 ConsoleHandler 實例,之後設定 ConsoleHandler 實例的 Formatter 為您自訂的 TableFormatter,執行結果如下所示:

  1. LogRecord info: onlyfun.caterpillar.TableFormatterDemo
  2. Level | LoggerName | Message |
  3. INFO | tableFormatter | 訊息1 |
  4. LogRecord info: onlyfun.caterpillar.TableFormatterDemo
  5. Level | LoggerName | Message |
  6. WARNING | tableFormatter | 訊息2 |

18.2.5 Logger 階層關係

在使用 Logger 的靜態 getLogger() 方法取得 Logger 實例時,給 getLogger() 方法的名稱是有意義的,如果您給定 “onlyfun”,實際上您將從根(Root)logger 繼承一些特性,像是日誌等級(Level)以及根(Root)logger的處理者(Handler),如果您再取得一個 Logger 實例,並給定名稱 “onlyfun.caterpillar”,則這次取得的 Logger 將繼承 “onlyfun” 這個 Logger 的特性。

範例 18.14 可以看出,三個 Logger(包括根 logger)在名稱上的繼承關係。

範例 18.14 LoggerHierarchyDemo.java

  1. package onlyfun.caterpillar;
  2. import java.util.logging.*;
  3. public class LoggerHierarchyDemo {
  4. public static void main(String[] args) {
  5. Logger onlyfunLogger = Logger.getLogger("onlyfun");
  6. Logger caterpillarLogger =
  7. Logger.getLogger("onlyfun.caterpillar");
  8. System.out.println("root logger: "
  9. + onlyfunLogger.getParent());
  10. System.out.println("onlyfun logger: "
  11. + caterpillarLogger.getParent().getName());
  12. System.out.println("caterpillar logger: "
  13. + caterpillarLogger.getName() + "\n");
  14. onlyfunLogger.setLevel(Level.WARNING);
  15. caterpillarLogger.info("caterpillar' info");
  16. caterpillarLogger.setLevel(Level.INFO);
  17. caterpillarLogger.info("caterpillar' info");
  18. }
  19. }

getParent() 方法可以取得 Logger 的上層父 Logger,根 logger 並沒有名稱,所以您直接呼叫它的 toString() 以取得字串描述,當 Logger 沒有設定等級時,則使用父 Logger 的等級設定,所以在範例 18.14 中,onlyfunLogger 設定等級為 WARNING 時,caterpillarLogger 呼叫 info() 方法時並不會有訊息顯示(因為 WARNING 等級比 INFO 高),只有在 caterpillarLogger 設定了自己的等級為 INFO 之後,才會顯示訊息,執行結果如下:

  1. root logger: java.util.logging.LogManager$RootLogger@757aef
  2. onlyfun logger: onlyfun
  3. caterpillar logger: onlyfun.caterpillar
  4. 2005/6/7 上午 09:50:01 onlyfun.caterpillar.LoggerHierarchyDemo main
  5. 資訊: caterpillar' info

在每一個處理者(Handler)方面,當每一個 Logger 處理完自己的日誌動作之後,它會向父 Logger 傳播,讓父 Logger 的處理者也可以處理日誌,例如 root logger 的處理者是 ConsoleHandler,所以若您的子 Logger 加入了 FileHandler 來處理完日誌之後,向上傳播至父 Logger 時,會再由 ConsolerHandler 來處理,所以一樣會在文字模式上顯示訊息,之前的範例 18.11 就是這樣的例子。

18.3 訊息綁定

程式中的一些文字訊息可以將之定義在一個屬性檔案中,而不一定要寫死在程式碼當中,如此在日後想要更改文字訊息時,只要更改文字檔案的內容而不用重新編譯程式,就可以在程式運行時顯示不同的訊息,這個小節也會有一些國際化(Internationalization)處理的簡單介紹。

18.3.1 使用 ResourceBundle

在程式中有很多字串訊息會被寫死在程式中,如果您想要改變某個字串訊息,您必須修改程式碼然後重新編譯,例如簡單顯示 “Hello!World!” 的程式就是如此:

  1. public class Hello {
  2. public static void main(String[] args) {
  3. System.out.println("Hello!World!");
  4. }
  5. }

就這個程式來說,如果日後想要改變 “Hello!World!” 為 “Hello!Java!”,您就要修改程式碼中的文字訊息並重新編譯。

對於日後可能變動的文字訊息,您可以考慮將訊息移至程式之外,方法是使用 java.util.ResourceBundle 來作訊息綁定,首先您要先準備一個 .properties 檔案,例如 messages.properties,而檔案內容如下:

  1. onlyfun.caterpillar.welcome=Hello
  2. onlyfun.caterpillar.name=World

.properties 檔案必須放置在 Classpath 的路徑設定下,檔案中撰寫的是鍵(Key)、值(Value)配對,之後在程式中您可以使用鍵來取得對應的值,接著改寫 Hello 類別為範例 18.15。

範例 18.15 ResourceBundleDemo.java

  1. package onlyfun.caterpillar;
  2. import java.util.ResourceBundle;
  3. public class ResourceBundleDemo {
  4. public static void main(String[] args) {
  5. // 綁定messages.properties
  6. ResourceBundle resource =
  7. ResourceBundle.getBundle("messages");
  8. // 取得對應訊息
  9. System.out.print(resource.getString(
  10. "onlyfun.caterpillar.welcome") + "!");
  11. System.out.println(resource.getString(
  12. "onlyfun.caterpillar.name") + "!");
  13. }
  14. }

ResourceBundle 的靜態 getBundle() 方法會取得一個 ResourceBundle 的實例,所給定的引數名稱是訊息檔案的主檔名,getBundle() 會自動找到對應的 .properties 檔案,取得 ResourceBundle 實例後,可以使用 getString() 指定鍵來取得檔案中對應的值,執行結果如下:

  1. Hello!World!

如果您日後想要改變顯示的訊息,只要改變 .properties 檔案的內容就可以了,例如可以將 messages.properties 檔案內容改為:

  1. onlyfun.caterpillar.welcome=Oh
  2. onlyfun.caterpillar.name=Java

無需重新編譯範例 18.15,直接執行程式,則輸出訊息如下:

  1. Oh!Java!

良葛格的話匣子 在撰寫 .properties 檔案的內容時,鍵的命名前可以加上單位、公司或是網址名,例如我的網址是 openhome.cc,則我可以將鍵命名為 cc.openhome.XXX,如此在訊息增多時可以減少鍵名的名稱衝突問題。

18.3.2 格式化訊息

程式在運行的過程中,有些訊息可能必須動態決定,而之前介紹訊息綁定時,.properties 檔案中的訊息則是靜態的,也就是固定的文字內容,除非修改 .properties 檔案內容並重新啟動程式,不然的話一些訊息內容無法隨著程式動態顯示。

您可以使用 java.text.MessageFormat 類別來輔助訊息的格式化,MessageFormat 接受一個字串模式(Pattern)指定,對於文字訊息中可能變動的部份,可以使用引數索引(Argument Index)先佔住文字位置,引數索引是 {0} 到 {9} 的非負整數,之後在使用 MessageFormat 實例的 format() 方法時,可以提供真正的引數來填充引數索引處的訊息。

舉個例子來說,您可以如下格式化文字訊息:

  1. String message = "Hello! {0}! This is your first {1}!";
  2. Object[] params =
  3. new Object[] {"caterpillar", "Java"};
  4. MessageFormat formatter =
  5. new MessageFormat(message);
  6. // 顯示格式化後的訊息
  7. System.out.println(formatter.format(params));

MessageFormat 實例的 format() 方法會使用 params 陣列中的物件之 toString() 方法,將取得的 String 訊息依索引位置分別填入 {0} 到 {9} 的對應位置,執行 format() 方法後傳回的就是格式化後的訊息內容,就上面的程式片段而言,會顯示 “Hello! caterpillar! This is your first Java!” 訊息。

依照這樣的作法,如果您想在訊息綁定時也能進行訊息的格式化,讓一些訊息在程式運行過程中動態決定,則您可以在 .properties 中如下撰寫,例如撰寫 messages2.properties:

  1. onlyfun.caterpillar.greeting=Hello! {0}! This is your first {1}!

接著您可以綁定 messages2.properties,並在程式中進行訊息的格式化,如範例 18.17 所示。

範例 18.17 MessageFormatDemo.java

  1. package onlyfun.caterpillar;
  2. import java.util.ResourceBundle;
  3. import java.text.MessageFormat;
  4. public class MessageFormatDemo {
  5. public static void main(String[] args) {
  6. try {
  7. // 綁定messages.properties
  8. ResourceBundle resource =
  9. ResourceBundle.getBundle("messages2");
  10. String message = resource.getString(
  11. "onlyfun.caterpillar.greeting");
  12. Object[] params =
  13. new Object[] {args[0], args[1]};
  14. MessageFormat formatter =
  15. new MessageFormat(message);
  16. // 顯示格式化後的訊息
  17. System.out.println(formatter.format(params));
  18. }
  19. catch(ArrayIndexOutOfBoundsException e) {
  20. System.out.println("沒有指定引數");
  21. }
  22. }
  23. }

程式運行時您必須給它兩個引數,分別作為 {0} 與 {1} 的訊息填充,執行時的一個例子如下:

  1. java onlyfun.caterpillar.MessageFormatDemo caterpillar Java
  2. Hello! caterpillar! This is your first Java.

18.3.3 國際化訊息

國際化的英文是 Internationalization,因為單字中總共有18個字母而首尾字元分別為 ‘I’ 與 ‘N’,所以簡稱 I18N,國際化的目的是讓應用程式可以依地區不同而顯示不同的訊息,最基本的就是讓不同語系的使用者可以看到屬於自己語系的訊息,像是英文語系的看到英文內容,而中文語系的可以看到中文的內容。

為了在應用程式中表示一個區域,Java 提供有 java.util.Locale 類,一個 Locale 實例包括了語系資訊與區域資訊,例如 “en” 表示英文語系的國家,這個字母組合是在 ISO 639 中定義的,而區域資訊則是像 “US” 表示美國,這個字母組合則是在 ISO 3166 中定義的。

您可以這麼新增一個 Locale 的實例,用以表示中文語系 “zh”、台灣 “TW”:

  1. Locale locale = new Locale("zh", "TW");

如何將 Locale 用於訊息綁定呢?當您使用 ResourceBundle.getBundle() 方法時,預設就會自動取得電腦上的語系與區域訊息,而事實上訊息檔案的名稱由 basename 加上語系與地區來組成,例如:

  • basename.properties (預設)
  • basename_en.properties
  • basename_zh_TW.properties

沒有指定語言與地區的 basename 是預設的資源檔名稱,當沒有提供專用的語系、區域訊息檔案時,就會找尋預設的資源檔案。

如果您想要提供繁體中文的訊息,由於訊息資源檔必須是 ISO-8859-1 編碼,所以對於非西方語系的處理,必須先將之轉換為 Java Unicode Escape 格式,例如您可以先在 messages3_zh_TW.txt 中撰寫以下的內容:

  1. onlyfun.caterpillar.welcome=哈囉
  2. onlyfun.caterpillar.name=世界

然後使用 JDK 的工具程式 native2ascii 來轉換,例如:

  1. native2ascii -encoding Big5 messages3_zh_TW.txt messages3_zh_TW.properties

轉換後的 messages3_zh_TW.properties 檔案內容會如下:

  1. onlyfun.caterpillar.welcome=\u54c8\u56c9
  2. onlyfun.caterpillar.name=\u4e16\u754c

將這個檔案放於 Classpath 可以存取的到的路徑位置,您也可以提供預設的訊息檔案 messages3.properties:

  1. onlyfun.caterpillar.welcome=Hello
  2. onlyfun.caterpillar.name=World

來測試一下訊息檔案,我所使用的作業系統是語系設定是中文,區域設定是台灣,來撰寫範例 18.18 進行測試。

範例 18.18 I18NDemo.java

  1. package onlyfun.caterpillar;
  2. import java.util.ResourceBundle;
  3. public class I18NDemo {
  4. public static void main(String[] args) {
  5. ResourceBundle resource =
  6. ResourceBundle.getBundle("messages3");
  7. System.out.print(resource.getString(
  8. "onlyfun.caterpillar.welcome") + "!");
  9. System.out.println(resource.getString(
  10. "onlyfun.caterpillar.name") + "!");
  11. }
  12. }

根據我作業系統的設定,執行程式時會使用預設的語系 “zh” 與區域設定 “TW”,所以就會找尋 messages_zh_TW.properties 的內容,結果會顯示以下的訊息:

  1. 哈囉!世界!

在使用 ResourceBundle.getBundle() 時可以給定 Locale 實例作為參數,例如若您想提供 messages_en_US.properties 檔案,並想要 ResourceBundle.getBundle() 取得這個檔案的內容,則可以如下撰寫:

  1. Locale locale = new Locale("en", "US");
  2. ResourceBundle resource =
  3. ResourceBundle.getBundle("messages", locale);

根據 Locale 物件的設定,這個程式片段將會取得 messages_ en_US.properties 檔案的訊息內容。

18.4 接下來的主題

到這邊為止,都使用一些簡單的範例來為您示範每個API的功能要如何使用,為了讓您實際了解到這些獨立的 API 如何組成一個完整的應用程式,下一個章節將實際製作一個文字編輯器,由於文字編輯器將使用到 Swing 來作為視窗介面,所以您也可以在下一個章節中了解到一些 Swing 視窗程式的基本概念。