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

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

#建立WatchService - DocumentWatchService.java
public class DocumentWatchService {
    private WatchService watchService = null;
    private static DocumentWatchService INSTANCE = null;
    private Map<Path, List<Path>> dirMapPath = new HashMap<>();
    private Map<Integer, WatchKey> idMapWatchKey = new TreeMap<>();
    private boolean enabled = false;
    private DocumentWatchService() {
    }
 
    public static DocumentWatchService getInstance() {
       if(INSTANCE == null) {
          INSTANCE = new DocumentWatchService();
       }
       return INSTANCE;
    }
 
    public void register(File dir) {
       try {
          if(watchService == null) {
             watchService = FileSystems.getDefault().newWatchService();
             enableService();
          }
          Path path = Paths.get(dir.getAbsolutePath());
          if(!dirMapPath.containsKey(path)) {
             WatchKey watchkey = path.register(watchService, new Kind[] {ENTRY_CREATE, ENTRY_DELETE}, ExtendedWatchEventModifier.FILE_TREE);
             dirMapPath.put(path, new ArrayList<>());
             idMapWatchKey.put(dirMapPath.size(), watchkey);
          }else{
             System.out.println("The directory you selected has been monitored");
          }
       } catch (IOException e) {
         // TODO Auto-generated catch block
         e.printStackTrace();
       }
    }
 
    public void unregister(int id) {
       WatchKey key = idMapWatchKey.get(id);
       if(key != null) {
          key.cancel();
       }else {
          System.out.println("The ID you entered does not exist");
       }
    }
 
    public boolean listCurrentRegisters() {
       boolean exist = false;
       System.out.println("==========List Current Registers==========");
       for(Integer id : idMapWatchKey.keySet()) {
          WatchKey key = idMapWatchKey.get(id);
          String status = key.isValid() ? "processing" : "stop";
          Path path = (Path) key.watchable();
          System.out.println("("+id+")"+path.toAbsolutePath()+"("+status+")");
          if(key.isValid()) {
             exist = true;
          }
       }
       return exist;
    }
 
    public void listCurrentDocuments() {
       System.out.println("==========List Current Documents==========");
       for(Integer id : idMapWatchKey.keySet()) {
          WatchKey key = idMapWatchKey.get(id);
          Path path = (Path) key.watchable();
          System.out.println("("+id+")"+path.toAbsolutePath());
          File dir = path.toFile();
          List<Path> mapPaths = dirMapPath.get(path);
          traceFolder(dir, mapPaths, "");
       }
       if(dirMapPath.size() == 0) {
          System.out.println("No registration exist");
       }
    }
 
    private void traceFolder(File f, List paths, String tab) {
       if(f.isFile()) {
          return;
       }else {
          for(File file : f.listFiles()) {
             String selfAdd = paths.contains(file.toPath()) ? "(*)" : "";
             System.out.println(tab + file.getName() + selfAdd);
             traceFolder(file, paths, tab + "  ");
          }
       } 
    }
 
    private void enableService() {
       if(!enabled) {
          new Thread("Documents Watch Service") {
             @SuppressWarnings("unchecked")
             @Override
             public void run() {
                while (true) {
                   try {
                      WatchKey key = watchService.take();
                      Path dir = (Path)key.watchable();
                      List<WatchEvent<?>> lists = key.pollEvents();
                      System.out.println();
                      System.out.println();
                      System.out.println("***********Document Watch Service***********");
                      for (WatchEvent<?> watchEvent : lists) {
                         WatchEvent<Path> watchEventPath = (WatchEvent<Path>) watchEvent;
                         Kind<Path> kind = (Kind<Path>) watchEvent.kind();
                         String relativePath = watchEventPath.context().toString();
                         Path self = dir.resolve(relativePath);
                         File file = new File(relativePath);
                         System.out.println("***Kind: " + kind);
                         System.out.println("***Watch Directory: " + dir);
                         System.out.println("***RelativePath: " + relativePath);
                         System.out.println("***Filename: " + file.getName());
                         boolean hidden = file.isHidden();
                         if (!hidden) {
                            List pathList = dirMapPath.get(dir);
                            if(ENTRY_CREATE == kind) {
                               pathList.add(self);
                            }else if(ENTRY_DELETE == kind) {
                               pathList.remove(self);
                            }
                         }
                      }
                      System.out.println("********************************************");
                      key.reset();
                 } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                 } catch (ClosedWatchServiceException e) {
                    //e.printStackTrace();
                    break;
                 }
              }
           }
        }.start();
     }
     enabled = true;
  }
 
  public void closeService() {
     if(watchService != null) {
        try {
           watchService.close();
           watchService = null;
        } catch (IOException e) {
          // TODO Auto-generated catch block
          e.printStackTrace();
        }
    }
  }
}
+新增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

留言