Java 7 NIO.2 文件监视服务简介

loyal 发布于 2011/10/08 20:13
阅读 8K+
收藏 12

Java 7 是即将发布的下一代 Java 开发平台,现在已经完成了所有的功能开发。Java 官方站点 http://jdk7.java.net/ 提供了预览版本(Developer Preview Release)供开发测试人员进行下载和测试。本文需要下载安装 Java 7,设置 JAVA_HOME, PATH 和 CLASSPATH 等环境变量以使用命令行来编译和运行 Java 7 程序。

在 Java 7 新引入的 java.nio.file 包中有一套监视文件系统变更的 Watch Service API。可以使用这些 API 把一个目录注册到监视服务上。在注册的时候需要指定我们感兴趣的事件类型,比如文件创建、文件修改、文件删除等。当监视的事件发生时,监视服务会根据需要 处理这些事件。这些 API 主要包括:

  • java.nio.file.WatchService

    文件系统监视服务的接口类,它的具体实现由监视服务提供者负责加载。比如在 Windows 系统上,它的实现类为 sun.nio.fs.WindowsWatchService

  • java.nio.file.Watchable

    实现了 java.nio.file.Watchable 的对象才能注册监视服务 WatchService。java.nio.file.Path实现了 watchable 接口,后文使用 Path 对象注册监视服务。

  • java.nio.file.WatchKey

    该类代表着 Watchable 对象和监视服务 WatchService 的注册关系。WatchKey 在 Watchable 对象向 WatchService 注册的时候被创建。它是 Watchable 和 WatchService 之间的关联类。它们的类图关系参见图 2。
    图 2. Watch Service 类图:
    图 2. Watch Service 类图:

    为实现文件变更监视服务,我们需要完成以下工作:

    1. 创建 WatchService 的一个实例变量 "watcher"。
    2. 使用 watcher 注册每一个想要监视的目录。注册目录到监视服务时,需要指定想要接收文件更改通知的事件类型。注册目录会返回一个 WatchKey 实例 key。
    3. 执行一个无限循环来监控要到来的事件。当一个事件发生时,对实例 key 发出信号通知并且将它放到 watcher 的队列中。
    4. 从 watcher 的队列中重新得到 key 实例。Key 实例包含发生变更的文件名。
    5. 从 key 实例中得到挂起的事件,然后根据需要对这些事件进行处理。
    6. 重置 key 实例并重新开始监控事件。
    7. 监控完毕,关掉监视服务。

    现在来探讨如何把 Java 7 的 NIO.2 新特性用于安装测试。需要完成的任务有以下几点:

    1. 如何利用文件监视新特性生成文件变更清单:

    以安装测试的一个实际例子来演示如何使用文件监视新特性生成文件变更清单。下文会详细解释如何监视安装过程中文件系统的变化,监视安装完成之后会得到文件变更清单。

    2. 备份变更的文件并进行比对:

    利用 Java 7 中 File I/O API 读取文件变更清单,然后把新增的、修改过的等文件复制到一个新的目录下进行备份。备份之后我们可以和基准版本进行比对,来评估安装目录文件系统差异。

    安装过程中监视文件变更

    在本文参考资料中给出了两个 Java 文件,WatchInstall.java 和 BackupInstall.java。WatchInstall.java 用于监视安装过程中安装目录文件系统的变化,可以用该程序输出文件变更的清单。BackupInstall.java 用于把变更的文件复制到备份目录进行保存,为以后的文件比对查看文件差异做准备。我们应该如何应用附件中的 WatchInstall.java 呢 ? 这些 Java 显然是独立于操作系统平台的,以 Windows 环境为例进行分析。下载所附的源文件,进行编译。打开一个 CMD 命令行窗口,运行 java – version 确认一下环境变量已正确设置。使用编译命令 javac *.java来编译 Java 文件。

    以安装 IBM® SPSS® Decision Management Extension 到 IBM® SPSS® Modeler Server 14.2 为例,只考虑 Extension 的安装测试,假定 Modeler Server 已经安装到路径 C:\Moderer\14.2。打开一个 CMD 命令行窗口,运行编译得到的 WatchInstall.class 文件:

    java WatchInstall – r C:\moderer\14.2 >> C:\watchinstall.txt 2>&1 

    这个进程会一直监视 C:\Moderer\14.2 文件夹下的文件更改情况,该程序的输出会重定向到清单文件 C:\watchinstall.txt。在安装 Extension 到 Modeler Server 的过程中,安装目录 C:\Moderer\14.2 下的文件夹、文件的新增,删除、修改等操作都会记录到这个清单文件里面的。安装完毕,关闭监视进程。清单 1 就是得到的变更文件清单,通过阅读该文件,可以了解文件的增减删等变化情况:

    清单 1. WatchInstall.txt 清单

     Scanning C:\Modeler\14.2 ... 
     Done. 
     2011-06-12 12:34:37  ENTRY_MODIFY|C:\Modeler\14.2\Demos\japanese_ja 
     2011-06-12 12:34:37  ENTRY_MODIFY|C:\Modeler\14.2\Demos 
     2011-06-12 12:34:37  ENTRY_MODIFY|C:\Modeler\14.2\eclipse 
     2011-06-12 12:34:37  ENTRY_MODIFY|C:\Modeler\14.2\ext 
     2011-06-12 12:34:37  ENTRY_MODIFY|C:\Modeler\14.2\jre 
     2011-06-12 12:34:37  ENTRY_MODIFY|C:\Modeler\14.2\Media 
    。。。。

    下面来看 WatchInstall.java 中的代码如何监视文件系统。首先需要创建监视服务实例并注册监视目录到监视服务上,使用 FileSystems 中的 newWatchService() 方法创建 WatchService 实例。

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

    我们已经知道 WatchService 是一个接口,在不同的操作系统上有不同的实现,比如在 Windows 系统上,具体的实现为 sun.nio.fs.WindowsWatchService,在 Linux 平台上具体实现为 sun.nio.fs.LinuxFileSystem。接着把一个或者若干个监视对象注册到监控服务上,任何实现 Watchable 接口的对象都可以注册。我们使用实现该接口的 Path 类来注册监控服务,Path 类实现了接口的 register(WatchService, WatchEvent.Kind<?>...) 方法。可以看出,在注册的时候需要指定想要监视的事件类型,所支持的事件类型如下:

  • ENTRY_CREATE:创建条目时返回的事件类型
  • ENTRY_DELETE:删除条目时返回的事件类型
  • ENTRY_MODIFY:修改条目时返回的事件类型
  • OVERFLOW:表示事件丢失或者被丢弃,不必要注册该事件类型

清单 2 中的代码演示如何注册监控事件:
清单 2. 注册监控事件清单

 /** 
    注册给定的目录到监视服务
	 */ 
	 private void register(Path dir) throws IOException { 
 WatchKey key = dir.register(watcher, ENTRY_CREATE,ENTRY_DELETE,ENTRY_MODIFY); 
		 if (trace) { 
			 Path existing = keys.get(key); 
			 if (existing == null) { 
				 System.out.format("register: %s\n", dir); 
			 } else { 
				 if (!dir.equals(existing)) { 
			 system.out.format("update: %s -> %s\n", 
                                                     existing, dir); 
				                            } 
		        } 
		 } 
		 keys.put(key, dir); 
	 } 

当监视服务监视到文件变更事件时,会按照下述步骤处理监视到的事件:

  1. 通过 watcher.take() 方法获得一个 WatchKey 实例 key.take() 方法取队列中的一个的 key 返回,如果无可用的,该方法会等待。
  2. 处理 key 的挂起事件。通过 pollEvents() 方法获得 WatchEvents 事件列表。
  3. 通过 kind() 方法获取事件的类型。
  4. 文件名存在事件 event 的上下文中,可以通过 context() 方法来获取文件名。
  5. System.out.format() 语句输出文件系统变更信息
  6. 当 key 实例包含的事件全部处理完毕,需要调用 reset() 方法来恢复 key 的状态为 ready,如果 reset() 方法返回 false,这个实例 key 不再有效,循环可以退出。如果不调用 reset() 方法,这个实例不会接收其他事件。

WatchKey 实例 key 都有一个状态,这些状态可能是:

  • Ready:表示 key 可以接收事件。第一次创建时,key 的状态为 ready。
  • Signaled :表示一个或多个事件在排队。可以调用 reset() 方法从 signaled 状态改成 ready 状态。
  • Invalid :表示 key 实例不是活动状态。

在监视过程中,当新目录或者文件创建、删除或者修改的时候,会打印一条消息到清单文件中。打印的信息包括时间戳,事件类型和全路径文件名。该功能是由 System.out.format()语句行来实现的,在调用编译后的文件时需要使用重定向输出消息到清单文件中。源代码示例见清单 3。


清单 3. 处理监控事件清单
 // 等待监视事件发生
      WatchKey key; 
	 try { 
		 key = watcher.take(); 
		 // System.out.println(key.getClass().getName()); 
	 } catch (InterruptedException x) { 
		 return; 
	 } 
	 Path path = keys.get(key); 
	 if (path == null) { 
		 continue; 
	 } 
	 for (WatchEvent<?> event : key.pollEvents()) { 
		 WatchEvent.Kind kind = event.kind(); 
		 if (kind == OVERFLOW) { 
			 continue; 
		 } 
 // 目录监视事件的上下文是文件名
   WatchEvent<Path> evt = cast(event); 
		 Path name = evt.context(); 
		 Path child = path.resolve(name); 
		 System.out.format(new SimpleDateFormat("yyyy-MM-dd hh🇲🇲ss") 
				 .format(new Date()) 
				 + "  %s|%s\n", event.kind().name(), child); 
 // 递归地注册到监视服务
		 if (recursive && (kind == ENTRY_CREATE)) { 
			 try { 
				 if (Files.isDirectory(child, NOFOLLOW_LINKS)) { 
					 registerAll(child); 
				 } 
			 } catch (IOException x) { 
			 } 
		 } 
	 } 
 // 重置 key 
	 boolean valid = key.reset(); 
	 if (!valid) { 
		 keys.remove(key); 
		 if (keys.isEmpty()) { 
			 break; 
 } 
	 } 

上文源码中,cast(Event event) 方法用来避免如下异常:

 Note: WatchInstall.java uses unchecked or unsafe operations. 
 Note: Recompile with -Xlint:unchecked for details. 

这个异常是当我们试图把 WatchEvent<T> 类型的变量赋值给 WatchEvent<Path> 类型的变量时出现的。cast(Event event)方法见清单 4。


清单 4. cast(Event event) 方法
				 
 @SuppressWarnings("unchecked") 
 static <T> WatchEvent<T> cast(WatchEvent<?> event) { 
    return (WatchEvent<Path>)event; 
 } 

到此,我们了解了在安装过程中如何监视文件变更和如何生成文件变更清单的基本思路。读者可以根据自己的需求,对代码进行完善。

回页首

如何复制备份变更的文件

通过阅读文件变更的清单文件,可以简地识别出哪些文件发生了更改。除了简单地阅读这个文件,还可以通过编程的方式,把这些更改过的文件复制 到一个独立的备份目录下。这个目录只包括发生变更的文件,比如在我们的例子中,就只包括 Extension 的文件。可以使用文件系统比对工具比对这个备份目录和上一个构建的备份目录,评估其中的差异对软件功能的影响。

现在我们来看如何使用附件中的 BackInstall.java 文件读取变更清单文件并复制变更文件到独立的备份目录。下载 BackupInstall.java 源文件,完成编译。只需要简单的运行命令 java BackupInstall C:\WatchInstall.txt即可完成相关的操作。

运行完毕之后,一个名称类似 C:\Modeler\14.2_backup_20110612_14_05 的备份目录会生成到文件系统 C:\Modeler\14.2 的同级目录下。生成的这个目录里的文件都是安装 Extension 发生变更的文件,现在就可以通过使用 Beyond Compare 等工具和上一个稳定版本的备份目录进行比对来了解不同的构建版本之间的文件变更情况。下面详细的来看代码是如何工作的,代码见清单 5。首先 , 需要判断哪些文件需要复制备份,过滤掉不需要复制的文件。我们需要考虑的工作包括:

  1. 从文本清单里,获取我们正在监视的目录 rootDir, 这个一般是第一行输出的条目,在这里我们需要得到的是 C:\Modeler\14.2。
  2. 我们不需要复制到备份目录里面的有:以 register、update、Scanning、Done 开头的;清单消息内容中包含的事件类型为 ENTRY_DELETE 的。一般为在安装的过程中产生的临时文件,在安装之后被删除。如果不对这些行进行过滤,会由于找不到要复制的源文件而抛出异常信息。
  3. 变更清单文件里的变更记录包含时间戳和文件变更事件类型。我们需要截去时间戳和事件类型,获得变更文件的路径。使用 HashMap 对象来存储已经执行过复制的文件,执行过复制的文件路径会放置在 map 里面,就不需要再次复制。如果源文件不存在,不执行复制操作。

清单 5. ReadAndCopy(Path p) 方法
     inputStream = Files.newInputStream(p); 
     BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); 
		 String line = null; 
		 while ((line = reader.readLine()) != null) { 
			 if (line.startsWith("Scanning")) { 
				 rootDir = line.split(" "); 
			 } 

			 if (line.startsWith("register") 
                              || line.startsWith("update") 
			      || line.startsWith("Scanning") 
			      || line.startsWith("Done") 
			      || line.indexOf("ENTRY_DELETE") > 0) { 
					 continue; 
			 } 

			 line = line.substring(line.indexOf("|") + 1); 
			 if (Files.notExists(Paths.get(line)) 
				 || map.containsKey(Paths.get(line))) { 
				 continue; 
			 } 

		 map.put(Paths.get(line), "test"); 


现在我们已经知道需要复制的源文件,下面来看如何进行复制备份。首先得到源文件相对于监视目录的相对路径 relativePath。再得到备份目录 backDir,该目录由监视目录 +“_backup”+ 复制时时间戳组成。变更文件会被复制到这个备份目录下。然后根据 relativePath 和 backDir 目录,得到目标文件的路径。如果目标文件的父目录不存在,程序会自动创建父目录,然后再执行复制备份操作。如果目标文件或者文件夹已经存在,跳过复制,不 需要再次操作。在例子中,如果源文件为 C:\Modeler\14.2\Demos\,那么目标文件路径为 C:\Modeler\14.2_backup_20110612_14_05 \Demos\。代码见清单 6。


清单 6. 复制操作方法 
public void copy(Path rootDir, Path source, Path target, CopyOption option) { 

		 Path relativePath = source.subpath(rootDir.getNameCount(), source 
				 .getNameCount()); 
		 Path backDir = rootDir.getParent().resolve( 
				 rootDir.getFileName() + "_backup_" + times); 
		 if (target == null) { 
			 target = backDir.resolve(relativePath); 
		 } 
		 System.out.println("rootdir=" + rootDir + " source=" + source 
				 + "  target=" + target); 
		 if (Files.notExists(target.getParent())) { 
			 try { 
				 Files.createDirectories(target.getParent()); 
			 } catch (IOException e) { 
                         e.printStackTrace(); 
			 } 
		 } 

		 if (Files.exists(target)) { 
		 } else    
                  try { 
			 Files.copy(source, target, option); 
			 } catch (IOException e) { 
                             e.printStackTrace(); 
			 } 
	 } 



加载中
0
浪客Dandy
浪客Dandy

Java 7 的 Watch Service 已被证实为无能

事件太少,而且触发极为不可靠

Brin想写程序
Brin想写程序
@Xiao_f 我已经用了3个月了,每个触发都有,没啥问题啊?
X
Xiao_f
此话怎讲?刚要在程序中用到这个API,被你这么说有点不敢用了
返回顶部
顶部