Java 7 NIO.2 实现文件系统监视

关于安装测试,需要关注什么

软件安装测试(Installation Testing)是一项重要的软件质量保证工作,它确保客户拿到软件产品后能够成功安装和部署新的软件环境。按照安装类型,需要对完全安装、自定义安装、 升级安装和卸载等分别进行测试。安装测试还需要评测在系统异常情况下产品的安装表现行为,例如,在安装过程中,若遇到磁盘空间不足、缺少目录创建权限等场 景,软件产品需要展现信息给客户,指导下一步如何正确操作。

按照安装向导一步一步地进行安装,安装结束时没有任何异常信息出现,显示安装成功。安装的软件能正常启动,也能通过简单的功能测试。这种情 况下,可以认为通过安装测试了吗?测试人员拿到可测试版本之后,执行软件安装测试只是软件质量保证的第一步,通过了安装测试的版本,很显然并不意味着软件 实现了所有的功能,接下来需要进行功能测试来验证。但是,大量的测试实践证明,成功通过安装测试的软件还是会有一些功能缺陷,而这些缺陷是由安装过程中缺 少一些文件,或者使用了不正确的文件等导致的。完成安装之后,遗漏文件、安装路径错误,甚至安装路径是正确的,但是文件的权限不够,比如部分文件缺少执行 权限(在 Linux、Unix 环境下多次遇到),这些安装问题都会导致软件不能正常运行。在功能测试中可以发现这些缺陷,但是能不能在安装测试中就尽早发现这些问题呢?这是本文要考虑 的问题,我们期望对安装目录进行监控,需要知道文件更改情况。

对其进一步解释,对于完全安装和自定义安装,可以对安装目录进行备份,如果通过了功能测试,就把备份的安装目录作为基准,在以后的每日构建 (daily build)版本安装之后,可以用新安装的目录和备份的基准目录进行文件系统比较,通过评估差异考虑新增的文件是否为新功能所需要,删除的文件对功能是否 有影响。通过这些评估,可以尽早发现软件功能问题。也可以比较自定义安装和完全安装的目录有何差异,并对这些差异进行评估。使用文件夹文件比对工具,比如 Beyond Compare,很容易发现文件系统的差异。对于升级测试、补丁测试,在安装主版本之后安装补丁版本;或者在一个软件主版本上安装其他扩展软件。如果主版 本比较大,占用了很多磁盘空间,而我们关心的只是新安装的补丁部分,就只需要评估新安装的那些文件对软件功能的影响。在这种情况下可以考虑使用 Java 7 的文件变更监视服务来完成这些工作。

图 1 是 IBM® SPSS® Decision Management Extension 安装在 IBM® SPSS® Modeler Server 上的截图,Extension 会在 Modeler Server 的安装目录添加新的文件来增强 Modeler Server 的功能,本文以此为例来演示如何监控这些变更的文件。读者可以用这些代码来监控任何其他软件安装过程中的文件变更情况。


图 1. Extension 安装截图

现在我们对需要考虑的问题进行以下概括:

  1. 在补丁或者升级安装测试中,哪些文件发生了变化(新增,修改或删除),如何得到一个文件变更的文本清单。
  2. 如何把这些变更文件复制到一个独立的备份目录,以便在不同的每日构建版本间进行比对。如果上一个构建升级测试时,软件功能正常,当前构建升级之后工作不正常,就需要探究发生变更的文件有什么区别。通过备份目录的比对,可以很方便的找出那些差异。

每日构建的安装版本,往往在打包的时候,缺少一些文件,类库等等,在优化升级打包流程的时候更容易出现这些问题。Java 7 NIO.2 新特性可以帮助我们监视安装过程中的文件系统变化,从而解决这些问题。

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

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 类图:

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

  1.  
    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 evt = cast(event); 
		 Path name = evt.context(); 
		 Path child = path.resolve(name); 
		 System.out.format(new SimpleDateFormat("yyyy-MM-dd hh:mm: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 类型的变量赋值给 WatchEvent 类型的变量时出现的。cast(Event event) 方法见清单 4。


清单 4. cast(Event event) 方法

				 
 @SuppressWarnings("unchecked") 
 static  WatchEvent cast(WatchEvent event) { 
    return (WatchEvent)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(); 
			 } 
	 } 

 

总结

通过上面的介绍,我们了解到如何基于 Java 7 新特性实现对安装过程进行监视,通过评估安装目录的差异,尽早发现软件质量问题。

你可能感兴趣的:(Java,7,NIO.2,实现文件系统监视)