Java - 利用WatchService即時監控檔案的新增與刪除

由於最近剛好有實作在UI上面顯示Tree結構,而Tree是依據指定目錄的檔案結構做呈現,當我們在目錄內放進其他檔案後,UI上的Tree結構需要做相對應的即時更新,因此在這邊就利用了Java WatchService來實作,透過WatchService您可以先register您要監控的目錄,同時assign要監控的相對應動作,如新增、刪除等,最後就是做相對應的回饋! 而在這裡呈現的範例捨棄UI改用文字做簡單的顯示,主要專注在WatchService的使用上做說明。
#主程式 - DocumentMonitorSystem.java
先列出主程式的code
  1. public static void main(String[] args) {
  2. // TODO Auto-generated method stub
  3. DocumentWatchService service = DocumentWatchService.getInstance();
  4. Scanner input = new Scanner(System.in);
  5. boolean processing = true;
  6. System.out.println("*****Welcone to Document Monitor System*****");
  7. JFileChooser fc = new JFileChooser("D:\\workspace\\Java program");
  8. fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
  9. while(processing) {
  10. System.out.println("Please enter ID:");
  11. System.out.println("(1)List Current Documents (2)Register (3)Unregister (4)Shut down");
  12. int select = input.nextInt();
  13. switch(select) {
  14. case 1:{
  15. service.listCurrentDocuments();
  16. break;
  17. }
  18. case 2:{
  19. int returnValue = fc.showOpenDialog(null);
  20. if (returnValue == JFileChooser.APPROVE_OPTION) {
  21. File selectedFile = fc.getSelectedFile();
  22. if(selectedFile.isDirectory()) {
  23. service.register(selectedFile);
  24. service.listCurrentRegisters();
  25. }
  26. }
  27. break;
  28. }
  29. case 3:{
  30. if(service.listCurrentRegisters()) {
  31. System.out.println("Please enter ID to unregister:");
  32. int id = input.nextInt();
  33. service.unregister(id);
  34. }else
  35. System.out.println("A valid registration does not exist.");
  36. break;
  37. }
  38. case 4:{
  39. service.closeService();
  40. input.close();
  41. processing = false;
  42. System.out.println("Shut down...");
  43. break;
  44. }
  45. default:{
  46. System.out.println("Option '"+select+"' dose not exist");
  47. }
  48. }
  49. System.out.println();
  50. }
  51. }
這邊透過menu的選擇讓我們可以動態新增register或採取unregister哪個bind的目錄,同時可以列出目前register目錄的檔案結構做呈現,之中會再區分哪些是透過WatchService所監控到的檔案做標記!

#建立WatchService - DocumentWatchService.java
  1. public class DocumentWatchService {
  2. private WatchService watchService = null;
  3. private static DocumentWatchService INSTANCE = null;
  4. private Map<Path, List<Path>> dirMapPath = new HashMap<>();
  5. private Map<Integer, WatchKey> idMapWatchKey = new TreeMap<>();
  6. private boolean enabled = false;
  7. private DocumentWatchService() {
  8. }
  9. public static DocumentWatchService getInstance() {
  10. if(INSTANCE == null) {
  11. INSTANCE = new DocumentWatchService();
  12. }
  13. return INSTANCE;
  14. }
  15. public void register(File dir) {
  16. try {
  17. if(watchService == null) {
  18. watchService = FileSystems.getDefault().newWatchService();
  19. enableService();
  20. }
  21. Path path = Paths.get(dir.getAbsolutePath());
  22. if(!dirMapPath.containsKey(path)) {
  23. WatchKey watchkey = path.register(watchService, new Kind[] {ENTRY_CREATE, ENTRY_DELETE}, ExtendedWatchEventModifier.FILE_TREE);
  24. dirMapPath.put(path, new ArrayList<>());
  25. idMapWatchKey.put(dirMapPath.size(), watchkey);
  26. }else{
  27. System.out.println("The directory you selected has been monitored");
  28. }
  29. } catch (IOException e) {
  30. // TODO Auto-generated catch block
  31. e.printStackTrace();
  32. }
  33. }
  34. public void unregister(int id) {
  35. WatchKey key = idMapWatchKey.get(id);
  36. if(key != null) {
  37. key.cancel();
  38. }else {
  39. System.out.println("The ID you entered does not exist");
  40. }
  41. }
  42. public boolean listCurrentRegisters() {
  43. boolean exist = false;
  44. System.out.println("==========List Current Registers==========");
  45. for(Integer id : idMapWatchKey.keySet()) {
  46. WatchKey key = idMapWatchKey.get(id);
  47. String status = key.isValid() ? "processing" : "stop";
  48. Path path = (Path) key.watchable();
  49. System.out.println("("+id+")"+path.toAbsolutePath()+"("+status+")");
  50. if(key.isValid()) {
  51. exist = true;
  52. }
  53. }
  54. return exist;
  55. }
  56. public void listCurrentDocuments() {
  57. System.out.println("==========List Current Documents==========");
  58. for(Integer id : idMapWatchKey.keySet()) {
  59. WatchKey key = idMapWatchKey.get(id);
  60. Path path = (Path) key.watchable();
  61. System.out.println("("+id+")"+path.toAbsolutePath());
  62. File dir = path.toFile();
  63. List<Path> mapPaths = dirMapPath.get(path);
  64. traceFolder(dir, mapPaths, "");
  65. }
  66. if(dirMapPath.size() == 0) {
  67. System.out.println("No registration exist");
  68. }
  69. }
  70. private void traceFolder(File f, List paths, String tab) {
  71. if(f.isFile()) {
  72. return;
  73. }else {
  74. for(File file : f.listFiles()) {
  75. String selfAdd = paths.contains(file.toPath()) ? "(*)" : "";
  76. System.out.println(tab + file.getName() + selfAdd);
  77. traceFolder(file, paths, tab + " ");
  78. }
  79. }
  80. }
  81. private void enableService() {
  82. if(!enabled) {
  83. new Thread("Documents Watch Service") {
  84. @SuppressWarnings("unchecked")
  85. @Override
  86. public void run() {
  87. while (true) {
  88. try {
  89. WatchKey key = watchService.take();
  90. Path dir = (Path)key.watchable();
  91. List<WatchEvent<?>> lists = key.pollEvents();
  92. System.out.println();
  93. System.out.println();
  94. System.out.println("***********Document Watch Service***********");
  95. for (WatchEvent<?> watchEvent : lists) {
  96. WatchEvent<Path> watchEventPath = (WatchEvent<Path>) watchEvent;
  97. Kind<Path> kind = (Kind<Path>) watchEvent.kind();
  98. String relativePath = watchEventPath.context().toString();
  99. Path self = dir.resolve(relativePath);
  100. File file = new File(relativePath);
  101. System.out.println("***Kind: " + kind);
  102. System.out.println("***Watch Directory: " + dir);
  103. System.out.println("***RelativePath: " + relativePath);
  104. System.out.println("***Filename: " + file.getName());
  105. boolean hidden = file.isHidden();
  106. if (!hidden) {
  107. List pathList = dirMapPath.get(dir);
  108. if(ENTRY_CREATE == kind) {
  109. pathList.add(self);
  110. }else if(ENTRY_DELETE == kind) {
  111. pathList.remove(self);
  112. }
  113. }
  114. }
  115. System.out.println("********************************************");
  116. key.reset();
  117. } catch (InterruptedException e) {
  118. // TODO Auto-generated catch block
  119. e.printStackTrace();
  120. } catch (ClosedWatchServiceException e) {
  121. //e.printStackTrace();
  122. break;
  123. }
  124. }
  125. }
  126. }.start();
  127. }
  128. enabled = true;
  129. }
  130. public void closeService() {
  131. if(watchService != null) {
  132. try {
  133. watchService.close();
  134. watchService = null;
  135. } catch (IOException e) {
  136. // TODO Auto-generated catch block
  137. e.printStackTrace();
  138. }
  139. }
  140. }
  141. }
+新增register -- 選擇2
首先透過主程式register一個目錄做監控後,隨即呼叫enableService(); 此時啟用執行緒開始WatchService的服務,當下程式會停在watchService.take();這一行!
另外這裡要特別提到的是ExtendedWatchEventModifier.FILE_TREE這個參數,它可以解決access denied的error。對照下面的檔案結構,當WatchService監控了A目錄,它只會針對這層目錄下的內容發生監控的event,若A目錄下的B目錄有檔案的增減還得另外再register這B目錄,如此類推! 可是當您在Windows欲刪除A目錄時,若B目錄被register,這時候Windows就會跳出access denied的error! 因此在register時指定這個參數,只要註冊A目錄後就可以了,其他下層目錄都會發生新增與刪除的event,也不會跳出access denied的error!
A folder
   -- B folder
      -- BB file
   -- C file
PS. 相關說明請見Stack Overflow,這樣看來它只會發生在Windows且是乎是Java的bug

再來要說明的是register當下
#變數dirMapPath會記錄被註冊目錄對應的List,List紀錄新增檔案的Path
#變數idMapWatchKey會記錄當下ID編號對應的WatchKey,註冊產生的key都是唯一的

+執行unregister -- 選擇3
這邊只要呼叫WatchKey的cancel();即可,以後呼叫isValid()就會是false

+列出Register目錄下的Document -- 選擇1
若要取得當時註冊的Path,可以透過WatchKey呼叫watchable();取得
在此呼叫traceFolder做遞迴印出現在的檔案結構,若有標註*表示透過WatchService監控所增加的檔案

+新增、刪除檔案 in 監控的目錄
新增目錄檔案,此時List<WatchEvent<?>> lists = key.pollEvents();會回傳一組lists,進入迴圈後在WatchEvent可以得到event's kind,另event's context會得到檔案相對路徑,目錄裡還有檔案的話會依序進行追蹤產生相對應event!

刪除目錄檔案,這比較單純,它並不會依序追蹤被你刪的內容有哪些,如刪除A目錄,若目錄下還有很多層目錄,這裡WatchEvent也僅有一筆事件,那就是得知A目錄被刪除了而已!

比較特別的是若您是更新檔案名稱移動檔案,此時會先產生delete event,再來是create event,並沒有單純的modify event。而若您有註冊ENTRY_MODIFY,這時會產生三筆event,對照前面所述,最後會產生modify event,由於當您更新檔案,它所存在的目錄修改日期就會進行更新,因此它並不是針對檔案本身,而是檔案所在的目錄! 從這裡可以看出,我們無從得知這筆delete event與create event是否有關係,我們只單純的知道一個檔案被刪掉,另一個檔案被產生出來!

最後DEMO如下:
#選擇register的目錄
#列出目前被監控的目錄&手動新增檔案至目錄內
#列出目前的Document分佈
#手動刪除aaaa目錄
此時不會產生刪除bbbb.txt檔案的event
#修改檔名from cccc.txt to cccc222.txt

留言