欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

[NIO.2] 第三十九篇 实现文件监控服务

程序员文章站 2022-03-02 11:16:36
...
要实现监控服务需要有几个步骤。在本文中,你将会看到实现监控服务的主要步骤,并且开发监控目录创建、删除和修改的应用。每个步骤都会有代码和实例进行讲解,文章的最后将会把所有步骤合在一起,编写一个完整的应用。

创建 WatchService

最开始,需要创建 WatchService 用于监控文件系统。可以调用 FileSystem.newWatchService() 方法来创建这个对象:

WatchService watchService = FileSystems.getDefault().newWatchService();


现在我们有了 WatchService 对象。

注册监控对象

每个被监控对象都需要注册后才能被监控。任何实现 Watchable 接口的对象都可以被注册。在我们的例子中,将会注册目录的 Path 对象。除了监控对象外,监控服务还需要设置需要监控的事件类型。所有支持的监控事件类型都映射到  StandardWatchEventKinds 类中的 Kind<Path> 类型的常量上。

  •     StandardWatchEventKinds.ENTRY_CREATE:创建目录实体。当目录中的文件重命名和移动文件到此目录时,也会触发 ENTRY_CREATE  事件。
  •     StandardWatchEventKinds.ENTRY_DELETE:删除目录实体。当目录中的文件重命名和移动文件出此目录时,也会触发 ENTRY_DELETE 事件。
  •     StandardWatchEventKinds.ENTRY_MODIFY:修改目录实体。修改事件在具有平台相关性,但是无论什么平台,只要修改了文件内容,就会触发这个事件。在有的平台上,改变文件属性也会触发这个事件。
  •     StandardWatchEventKinds.OVERFLOW:表示事件可能丢失或遗弃。在注册的时候不需要监听这个事件。


Path 类实现了 Watchable 接口,因此可以调用 Watchable.register() 方法。这个方法有两个重载方法,第一个重载方法接受两个参数,第一个参数是 WatchService 对象,第二个是可变长参数,用于设置监控哪些事件。第二个重载方法除了这两个参数外,还接受一个参数,用于设置目录如何注册的限制符,但是目前 NIO.2 还没有提供标准限制符。

下面的代码将会注册  C:\rafaelnadal 目录,并监控创建、删除和修改事件:

import static java.nio.file.StandardWatchEventKinds.*; 
… 
final Path path = Paths.get("C:/rafaelnadal"); 
WatchService watchService = FileSystems.getDefault().newWatchService(); 
… 
path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,  
              StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE); 
… 
watchService.close(); 
…


你将会收到注册目录的  WatchKey 对象,这是使用 WatchService 注册后被监控对象的唯一标识。你可以选择是否保留这个引用。在事件被触发后,WatchService 将会返回相关的 WatchKey。具体内容参看下文。

等待传入事件

等待传入事件需要一个无限循环。当事件被触发,监控服务将会把 WatchKey 放入监控队列中。无限循环可能是下面的类型:

while(true){ 
//接收并处理传入事件 
… 
}


或者可能是下面的类型:

for(;;){ 
//接收并处理传入事件 
… 
}


获取 WatchKey

从队列中获取 WatchKey 可以调用 WatchService 中的三个方法其中一个。三个方法功能都是获取队列中的下一个 Key 并且将它从队列中删除。不同的地方是如果没有可用的 Key 的处理方式。下面看看这三个方法:

  •     poll():如果没有可用的 key,将立即返回 null 值。
  •     poll(long, TimeUnit):如果没有可用的 key,则等待一个时间之后再试一次,如果依然没有可用的 key 则返回 null 值。等待时间是第一个参数,第二个参数是时间的单位(秒、分钟、小时或其它)。
  •     take():如果没有可用的 key,将一直等待,直到队列中有可用的 key或者无限循环结束。


下面的代码将演示如何在无限循环中调用这三个方法:

//没有参数的 poll 方法
while (true) { 
       //retrieve and remove the next watch key 
       final WatchKey key = watchService.poll(); 
       //the thread flow gets here immediately with an available key or a null value 
… 
}

//有参数的 poll 方法
while (true) { 
       //retrieve and remove the next watch key 
       final WatchKey key = watchService.poll(10, TimeUnit.SECONDS); 
       //the thread flow gets here immediately if a key is available, or after 10 seconds  
       //with an available key or null value 
… 
}

//take 方法
while (true) { 
       //retrieve and remove the next watch key 
       final WatchKey key = watchService.take(); 
       //the thread flow gets here immediately if a key is available, or it will wait until a     
       //key is available, or the loop breaks 
… 
}


记住,每个 key 都有自己的状态,分别是就绪(ready),信号(signaled),无效(invalid):

  •     就绪:当初次创建的时候,key 就处于就绪状态,意味着已经准备好接收事件。
  •     信号:当 key 处于信号状态,表示至少有一个事件发生,并且 key 已在队列中等待。如果 key 处于信号状态,那么将会一直保持这个状态,直到调用 reset() 方法将 key 设置为就绪状态。如果 key 在信号状态的同时又发生了多个事件,那么这些事件将进入等待队列,key 本身所在的队列并不会重排。
  •     无效:当 key 处于无效状态时,表示它不再是活跃状态。key 会一直保持有效状态,直到显式调用 cancel() 方法、或者目录变得不可访问、或者监控服务关闭,key 将会变为无效状态。可以调用  WatchKey.isValid() 方法来测试 key 是否处于有效状态。


注意,WatchKey 是线程安全的。

获取事件

当一个 key 处于信号状态,表示上面有一个或多个事件需要关注。可以调用  WatchKey.pollEvents() 方法获取并删除全部事件。这个方法不需要参数,调用后返回包含事件的 List 对象,List 中的类型是 WatchEvent<T>,它表示注册到  WatchService 中的对象上的事件。

如果没有获取到事件,pollEvents() 并不会等待,而是直接返回一个空的 List。

下面的代码将会从 key 获取事件,并遍历所有事件:

… 
while (true) { 
      //retrieve and remove the next watch key 
      final WatchKey key = watchService.take(); 
 
      //get list of pending events for the watch key 
      for (WatchEvent<?> watchEvent : key.pollEvents()) { 
… 
      } 
      … 
} 
…

WatchEvent 是不可变对象并且是线程安全的。

获取事件属性

WatchEvent<T> 中保存了事件的属性,可以调用  WatchEvent.kind() 方法获取事件类型,这个方法将返回 Kind<T> 类型的对象。

下面的代码演示了如何获取事件属性:

… 
//get list of pending events for the watch key 
for (WatchEvent<?> watchEvent : key.pollEvents()) { 
 
     //get the kind of event (create, modify, delete) 
     final Kind<?> kind = watchEvent.kind(); 
 
     //handle OVERFLOW event 
     if (kind == StandardWatchEventKinds.OVERFLOW) { 
            continue; 
     } 
      
     System.out.println(kind); 
} 
…


除了事件类型,还可以调用  WatchEvent.count() 方法获取事件被观察次数(重复事件)。返回值是 int:

System.out.println(watchEvent.count());


获取事件的文件名


当文件的删除、创建或修改事件被触发后,我们可以通过事件的上下文获取文件名称(文件名存储在事件上下文中)。可以调用 WatchEvent.context() 方法来实现这个功能:

… 
final WatchEvent<Path> watchEventPath = (WatchEvent<Path>) watchEvent; 
final Path filename = watchEventPath.context(); 
 
System.out.println(filename); 
…


将 Key 重置为就绪状态

如果 key 处于信号状态,那么将一直保持,直到调用 reset() 将其重置为就绪状态。如果 reset() 方法返回 true,表示 key 有效并且已经被重置,返回 false 表示重置失败,原因有可能是 key 不再有效。有的时候,如果 key 不再有效的话,我们需要跳出循环,例如,只有一个 key 的时候,没有必要再继续留在无限循环中,如下代码所示:

… 
while(true){ 
   … 
  //reset the key 
  boolean valid = key.reset(); 
 
  //exit loop if the key is not valid (if the directory was deleted, for example) 
  if (!valid) { 
          break; 
  } 
} 
…


注意,如果忘记调用 reset() 或者调用 reset() 失败,那么 Key 将不再接收任何事件!

关闭 WatchService

当线程结束或监控服务关闭的时候,需要显式的调用  WatchService.close() 方法,或者将  WatchService 放入 try 语句块中:
try (WatchService watchService = FileSystems.getDefault().newWatchService()) { 
… 
}


当 WatchService 关闭后,当前的操作将会取消并且失效。如果服务关闭,任何操作都会抛出 ClosedWatchServiceException 异常。如果服务已经关闭,再调用这个方法不会有任何效果。

综合运用

下面的代码将会监控 C:\rafaelnadal 目录的创建、删除和修改事件。并且打印出事件类型和文件名。当程序运行后,可以手动创建、删除和修改此目录下的文件或目录,看看程序运行的效果。注意,此程序只对 C:\rafaelnadal 目录实现监控,其下的子目录并没有被监控。

import java.io.IOException; 
import java.nio.file.FileSystems; 
import java.nio.file.Path; 
import java.nio.file.Paths; 
import java.nio.file.StandardWatchEventKinds; 
import java.nio.file.WatchEvent; 
import java.nio.file.WatchEvent.Kind; 
import java.nio.file.WatchKey; 
import java.nio.file.WatchService; 
 
class WatchRafaelNadal { 
 
    public void watchRNDir(Path path) throws IOException, InterruptedException { 
        try (WatchService watchService = FileSystems.getDefault().newWatchService()) { 
            path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,  
                  StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE); 
 
            //start an infinite loop 
            while (true) { 
 
                //retrieve and remove the next watch key 
                final WatchKey key = watchService.take(); 
 
                //get list of pending events for the watch key 
                for (WatchEvent<?> watchEvent : key.pollEvents()) { 
                    //get the kind of event (create, modify, delete) 
                    final Kind<?> kind = watchEvent.kind(); 
                    //handle OVERFLOW event 
                    if (kind == StandardWatchEventKinds.OVERFLOW) { 
                        continue; 
                    }                                         
                    //get the filename for the event 
                    final WatchEvent<Path> watchEventPath = (WatchEvent<Path>) watchEvent; 
                    final Path filename = watchEventPath.context(); 
                    //print it out 
                    System.out.println(kind + " -> " + filename); 
                } 
                //reset the key 
                boolean valid = key.reset(); 
                //exit loop if the key is not valid (if the directory was deleted, for 
example) 
                if (!valid) { 
                    break; 
                } 
            } 
        } 
    } 
} 
public class Main { 
    public static void main(String[] args) { 
        final Path path = Paths.get("C:/rafaelnadal"); 
        WatchRafaelNadal watch = new WatchRafaelNadal(); 
        try { 
            watch.watchRNDir(path); 
        } catch (IOException | InterruptedException ex) { 
            System.err.println(ex); 
        } 
    } 
} 


注意,这段代码使用了无限循环,因此需要手动结束,或者自行编写一个结束机制。

文章来源:http://www.aptusource.org/2014/04/nio-2-implementing-a-watch-service/
相关标签: Java NIO.2