Java7-NIO2-高级教程-二-

48 阅读44分钟

Java7 NIO2 高级教程(二)

协议:CC BY-NC-SA 4.0

五、递归运算:遍历

您可能知道,递归编程是一种有争议的技术,因为它通常需要大量内存,但它简化了一些编程任务。基本上,递归编程是这样一种情况:过程调用自身,传入一个或多个参数的修改值,这些参数被传入到过程的当前迭代中。计算阶乘、斐波那契数、字谜和 Sierpinski 地毯等编程任务只是可以通过递归编程技术完成的一些众所周知的任务。下面的代码片段使用这种技术来计算阶乘(n!= 1 * 2 * 3 …… n)—请注意该过程如何调用自己:

/**
  * Calculate the factorial of n (n! = 1 * 2 * 3 * … * n).
  *
  * @param n the number to calculate the factorial of.
  * @return n! - the factorial of n.
  */
static int fact(int n) {

   // Base Case:
   // If n <= 1 then n! = 1.
   if (n <= 1) {
       return 1;
   }
   // Recursive Case:  
   // If n > 1 then n! = n * (n-1)!
   else {
        return n * fact(n-1);
        }
   }

如果您已经熟悉这种编程技术,那么请继续阅读本章,看看 NIO.2 如何利用它。否则,在您继续阅读一些致力于递归编程的教程之前,这是一个好主意,例如 Jonathan Bartlett 的“掌握递归编程”,可在[www.ibm.com/developerworks/linux/library/l-recurs/index.html](http://www.ibm.com/developerworks/linux/library/l-recurs/index.html)获得。

许多涉及使用文件的编程任务需要访问文件树中的所有文件,这是使用递归编程机制的好机会,因为每个文件都应该单独“接触”。在执行删除、复制或移动文件树等任务时,这是一种非常常见的方法。基于这种机制,NIO.2 将文件树的遍历过程封装在一个名为FileVisitor的接口中,在java.nio.file包中。

本章首先介绍FileVisitor的范围和方法。一旦您熟悉了FileVisitor,本章将帮助您开发一套应用,您可以用它来执行涉及遍历文件树的任务,比如查找、复制、删除和移动文件。

file visitor 接口

如前所述,FileVisitor接口支持递归遍历文件树。该接口的方法表示遍历过程中的关键点,使您能够在访问文件时、访问目录前、访问目录后以及发生故障时进行控制;换句话说,这个接口在访问文件之前、期间和之后都有挂钩,在出现故障时也有挂钩。一旦您拥有了控制权(在这些关键点的任何一点上),您就可以选择如何处理被访问的文件,并通过FileVisitResult枚举指示访问结果来决定接下来应该对它做什么,该枚举包含四个枚举常量:

  • FileVisitResult.CONTINUE:这个访问结果表示遍历过程应该继续。根据返回哪个FileVisitor方法,它可以被翻译成不同的动作。例如,遍历过程可以通过访问下一个文件、访问目录条目或跳过失败来继续。
  • FileVisitResult.SKIP_SIBLINGS:这个访问结果表示遍历过程应该继续,而不访问这个文件或目录的兄弟。
  • FileVisitResult.SKIP_SUBTREE:这个访问结果表示遍历过程应该继续,而不访问这个目录中的其余条目。
  • FileVisitResult.TERMINATE:该访问结果表示遍历过程应该终止。

此枚举类型的常数可以迭代如下:

for (FileVisitResult constant : FileVisitResult.values())
    System.out.println(constant);

以下小节讨论了如何通过实现各种FileVisitor方法来控制遍历过程。

FileVisitor.visitFile()方法

为目录中的文件调用visitFile()方法。通常,这个方法返回一个CONTINUE结果或者一个TERMINATE结果。例如,当搜索一个文件时,该方法应该返回CONTINUE直到找到该文件(或者完全遍历该树),并在找到该文件后返回TERMINATE

当这个方法被调用时,它接收对文件的引用和文件的基本属性。如果发生 I/O 错误,那么它抛出一个IOException异常。以下是此方法的签名:

FileVisitResult visitFile(T file, BasicFileAttributes attrs) throws IOException
file visitor . previsitdirectory()方法

在访问目录条目之前,为目录调用preVisitDirectory()方法。如果方法返回CONTINUE将访问条目,如果返回SKIP_SUBTREE将不访问条目(后一个访问的结果只有从该方法返回时才有意义)。此外,通过返回SKIP_SIBLINGS结果,您可以跳过访问该文件或目录的同级(以及任何后代)。

当这个方法被调用时,它获得对目录和目录的基本属性的引用。如果发生 I/O 错误,那么它抛出一个IOException异常。这个方法的特征是

FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs) throws IOException
file visitor . postvisit directory()方法

在已经访问了目录中的所有条目(以及任何后代)或者访问突然结束(即,发生了 I/O 错误或者访问以编程方式中止)之后,调用postVisitDirectory()方法。当这个方法被调用时,它获得一个对目录和IOException对象的引用——如果访问期间没有发生错误,它将是null,如果发生错误,它将返回相应的错误。如果发生 I/O 错误,那么它抛出一个IOException异常。下面是这个方法的签名

FileVisitResult postVisitDirectory(T dir, IOException exc) throws IOException
FileVisitor.visitFileFailed()方法

当文件由于各种不同的原因而无法访问时,例如无法读取文件的属性或无法打开目录,就会调用visitFileFailed()方法。当调用此方法时,它会获取对该文件的引用以及在尝试访问该文件时发生的异常。如果发生 I/O 错误,那么它抛出一个IOException异常。以下是此方法的签名:

FileVisitResult visitFileFailed(T file, IOException exc) throws IOException

简单文件访问者类

实现FileVisitor接口需要实现它的所有方法,如果您只需要实现其中的一个或几个方法,这可能是不可取的。在这种情况下,扩展实现了FileVisitor接口的SimpleFileVisitor类就简单多了。这种方法只需要覆盖所需的方法。

例如,您可能想要遍历文件树并列出所有目录的名称。要实现这一点,只使用postVisitDirectory()visitFileFailed()方法就足够了,如下面的代码片段所示(起始文件树在下一节中给出):

`class ListTree extends SimpleFileVisitor {

    @Override     public FileVisitResult postVisitDirectory(Path dir, IOException exc) {

        System.out.println("Visited directory: " + dir.toString());

        return FileVisitResult.CONTINUE;     }

    @Override     public FileVisitResult visitFileFailed(Path file, IOException exc) { System.out.println(exc);

        return FileVisitResult.CONTINUE;     } }`

如您所见,跳过了preVisitDirectory()visitFile()方法。

开始递归过程

一旦创建了递归机制(通过实现FileVisitor接口或扩展SimpleFileVisitor类),就可以通过调用两个Files.walkFileTree()方法之一来启动这个过程。最简单的walkFileTree()方法获取起始文件(这通常是文件树根)和每个文件要调用的文件访问者(这是递归机制类的一个实例)。例如,您可以通过如下方式调用walkFileTree()方法来启动上一节中的代码示例(传递的文件树是C:\rafaelnadal):

Path listDir = Paths.get("C:/rafaelnadal"); //define the starting file tree
ListTree walk = new ListTree();             //instantiate the walk

try{
   Files.walkFileTree(listDir, walk);       //start the walk
   } catch(IOException e){
     System.err.println(e);
   }

第二个walkFileTree()方法获取开始文件、定制遍历的选项、要访问的最大目录级别数(为了确保遍历所有级别,可以为最大深度参数指定Integer.MAX_VALUE)和遍历实例。接受的选项是FileVisitOption枚举的常量。实际上,这个枚举包含一个名为FOLLOW_LINKS的常量,表示遍历中遵循了符号链接(默认情况下,它们没有被遵循)。

为前面的遍历调用此方法可能如下所示:

Path listDir = Paths.get("C:/rafaelnadal");              //define the starting file
ListTree walk = new ListTree();                          //instantiate the walk
EnumSet opts = EnumSet.of(FileVisitOption.FOLLOW_LINKS); //follow links

try{
   Files.walkFileTree(listDir, opts, Integer.MAX_VALUE, walk); //start the walk
   } catch(IOException e){
     System.err.println(e);
   }

Image 呼叫walkFileTree(start, visitor)与呼叫walkFileTree(start, EnumSet.noneOf(FileVisitOption.class), Integer.MAX_VALUE, visitor)效果相同。

以下几行是前面示例的输出:


Visited directory: C:\rafaelnadal\equipment

Visited directory: C:\rafaelnadal\grandslam\AustralianOpen

Visited directory: C:\rafaelnadal\grandslam\RolandGarros

Visited directory: C:\rafaelnadal\grandslam\USOpen

Visited directory: C:\rafaelnadal\grandslam\Wimbledon

Visited directory: C:\rafaelnadal\grandslam

…

Visited directory: C:\rafaelnadal

常见的散步

有一组常见的遍历,您可以通过FileVisitor接口轻松实现。本节向您展示如何编写和实现应用来执行文件搜索、递归复制、递归移动和递归删除。

编写文件搜索应用

大多数操作系统都提供了搜索文件的专用工具(例如,Linux 有find命令,而 Windows 有文件搜索工具)。从简单搜索到高级搜索,所有工具通常都以相同的方式工作:指定搜索条件,然后等待工具找到匹配的文件。但是,如果您需要以编程方式完成搜索,那么FileVisitor可以帮助您完成遍历过程。无论您是按名称、扩展名或 glob 模式查找文件,还是在文件内部查找一些文本或代码,方法总是访问文件存储中的每个文件并执行一些检查,以确定文件是否符合您的搜索标准。

当您基于FileVisitor编写文件搜索工具时,您需要记住以下几点:

  • visitFile()方法是在当前文件和您的搜索标准之间进行比较的最佳位置。此时,您可以提取每个文件名、扩展名或属性,或者打开文件进行阅读。您可以使用文件名、扩展名等来确定所访问的文件是否是所搜索的文件。有时,您会将这些信息混合到复杂的搜索标准中。这种方法找不到目录。
  • 如果您想要查找目录,那么比较必须在preVisitDirectory()postVisitDirectory()方法中进行,这取决于大小写。
  • 如果一个文件不能被访问,visitFileFailed()方法应该返回FileVisitResult.CONTINUE,因为这个问题不需要停止整个搜索过程。
  • 如果您按名称搜索一个文件,并且您知道在文件树中有一个同名的文件,那么一旦visitFile()方法找到它,您就可以返回FileVisitResult.TERMINATE。否则,FileVisitResult.CONTINUE应该被退回。
  • 搜索过程可以遵循符号链接,这可能是一个好主意,因为遵循符号链接可以在遍历符号链接的目标子树之前定位所搜索的文件。跟随符号链接并不总是一个好主意;例如,删除文件是不可取的。
按名称搜索文件

可以将前面的列表合并到下面的代码片段中,以生成一个按名称搜索文件的应用。该应用将在整个默认文件系统中搜索文件rafa_1.jpg,并在找到时停止搜索。

`import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.FileVisitOption; import java.nio.file.FileVisitResult; import java.nio.file.FileVisitor; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.BasicFileAttributes; import java.util.EnumSet;

class Search implements FileVisitor {

    private final Path searchedFile;     public boolean found;

    public Search(Path searchedFile) {         this.searchedFile = searchedFile;         this.found = false;     }

void search(Path file) throws IOException {         Path name = file.getFileName();         if (name != null && name.equals(searchedFile)) {             System.out.println("Searched file was found: " + searchedFile +                                                    " in " + file.toRealPath().toString());             found = true;         }     }

    @Override     public FileVisitResult postVisitDirectory(Object dir, IOException exc)                                               throws IOException {         System.out.println("Visited: " + (Path) dir);         return FileVisitResult.CONTINUE;     }

    @Override     public FileVisitResult preVisitDirectory(Object dir, BasicFileAttributes attrs)                                              throws IOException {         return FileVisitResult.CONTINUE;     }

    @Override     public FileVisitResult visitFile(Object file, BasicFileAttributes attrs)                                      throws IOException {         search((Path) file);         if (!found) {             return FileVisitResult.CONTINUE;         } else {             return FileVisitResult.TERMINATE;         }     }

    @Override     public FileVisitResult visitFileFailed(Object file, IOException exc)                                            throws IOException {         //report an error if necessary         return FileVisitResult.CONTINUE;     } }

class Main {

    public static void main(String[] args) throws IOException {

        Path searchFile = Paths.get("rafa_1.jpg");         Search walk = new Search(searchFile);         EnumSet opts = EnumSet.of(FileVisitOption.FOLLOW_LINKS);

        Iterable dirs = FileSystems.getDefault().getRootDirectories();         for (Path root : dirs) {             if (!walk.found) {                 Files.walkFileTree(root, opts, Integer.MAX_VALUE, walk);             }         }

        if (!walk.found) {             System.out.println("The file " + searchFile + " was not found!");         }     } }`

输出的一个片段可能如下所示:


…

Visited: C:\Python25\Tools\webchecker

Visited: C:\Python25\Tools

Visited: C:\Python25

…

Visited: C:\rafaelnadal\equipment

Visited: C:\rafaelnadal\grandslam\AustralianOpen

Visited: C:\rafaelnadal\grandslam\RolandGarros

Visited: C:\rafaelnadal\grandslam\USOpen

Visited: C:\rafaelnadal\grandslam\Wimbledon

Visited: C:\rafaelnadal\grandslam

-------------------------------------------------------------

Searched file was found: rafa_1.jpg in C:\rafaelnadal\photos\rafa_1.jpg

通过全局模式搜索文件

有时,您可能只有关于您要搜索的文件的部分信息,例如只有它的名称或扩展名,或者甚至可能只有它的名称或扩展名的一部分。基于这一小段信息,您可以编写一个 glob 模式,如在第四章“通过应用 Glob 模式列出内容”一节中所描述的搜索将在文件存储中定位所有匹配 glob 模式的文件,从结果中您可能能够找到您需要定位的文件。

下面的代码片段在C:\rafaelnadal文件树中搜索所有类型为*.jpg的文件。只有遍历完整个树后,该过程才会停止。

import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.FileVisitOption; import java.nio.file.FileVisitResult; import java.nio.file.FileVisitor; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.PathMatcher; import java.nio.file.Paths; `import java.nio.file.attribute.BasicFileAttributes; import java.util.EnumSet;

class Search implements FileVisitor {

    private final PathMatcher matcher;

    public Search(String glob) {         matcher = FileSystems.getDefault().getPathMatcher("glob:" + glob);             }

    void search(Path file) throws IOException {         Path name = file.getFileName();         if (name != null && matcher.matches(name)) {             System.out.println("Searched file was found: " + name +                                                      " in " + file.toRealPath().toString());         }     }

    @Override     public FileVisitResult postVisitDirectory(Object dir, IOException exc)                                                                       throws IOException {         System.out.println("Visited: " + (Path) dir);         return FileVisitResult.CONTINUE;     }

    @Override     public FileVisitResult preVisitDirectory(Object dir, BasicFileAttributes attrs)                                                                       throws IOException {         return FileVisitResult.CONTINUE;     }

    @Override     public FileVisitResult visitFile(Object file, BasicFileAttributes attrs)                                                                       throws IOException {         search((Path) file);         return FileVisitResult.CONTINUE;     }

    @Override     public FileVisitResult visitFileFailed(Object file, IOException exc)                                                                       throws IOException {         //report an error if necessary         return FileVisitResult.CONTINUE;     } }

class Main {

    public static void main(String[] args) throws IOException {         String glob = "*.jpg";         Path fileTree = Paths.get("C:/rafaelnadal/");         Search walk = new Search(glob);         EnumSet opts = EnumSet.of(FileVisitOption.FOLLOW_LINKS);

        Files.walkFileTree(fileTree, opts, Integer.MAX_VALUE, walk);

    } }`

输出片段显示了找到的文件:


Searched file was found: rafa_1.jpg in C:\rafaelnadal\photos\rafa_1.jpg

Searched file was found: rafa_winner.jpg in C:\rafaelnadal\photos\rafa_winner.jpg

…

如果您有关于要查找的文件的附加信息,那么您可以创建一个更复杂的搜索。例如,除了关于文件名和类型的小块信息之外,也许您知道文件大小小于某个千字节数,或者也许您知道诸如文件创建时间、文件最后修改时间、文件是隐藏的还是只读的,或者谁拥有它。附加信息可能是文件属性的一部分,如下面的代码片段所示,该代码片段将*.jpg glob 模式与小于 100KB 的文件大小相结合(您可能知道,大小是一个基本属性):

`import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.FileVisitOption; import java.nio.file.FileVisitResult; import java.nio.file.FileVisitor; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.PathMatcher; import java.nio.file.Paths; import java.nio.file.attribute.BasicFileAttributes; import java.util.EnumSet;

class Search implements FileVisitor {

    private final PathMatcher matcher;     private final long accepted_size;

    public Search(String glob, long accepted_size) {         matcher = FileSystems.getDefault().getPathMatcher("glob:" + glob);         this.accepted_size = accepted_size;             }

    void search(Path file) throws IOException {         Path name = file.getFileName();         long size = (Long) Files.getAttribute(file, "basic:size");

        if (name != null && matcher.matches(name) && size <= accepted_size) {             System.out.println("Searched file was found: " + name + " in " +                                   file.toRealPath().toString() + " size (bytes):" + size);         }     }

    @Override     public FileVisitResult postVisitDirectory(Object dir, IOException exc)                                                                       throws IOException {         System.out.println("Visited: " + (Path) dir);         return FileVisitResult.CONTINUE;     }

    @Override     public FileVisitResult preVisitDirectory(Object dir, BasicFileAttributes attrs)                                                                       throws IOException {         return FileVisitResult.CONTINUE;     }

    @Override     public FileVisitResult visitFile(Object file, BasicFileAttributes attrs)                                                                       throws IOException {         search((Path) file);         return FileVisitResult.CONTINUE;     }

    @Override     public FileVisitResult visitFileFailed(Object file, IOException exc)                                                                       throws IOException {         //report an error if necessary         return FileVisitResult.CONTINUE;     } }

class Main {

    public static void main(String[] args) throws IOException {

        String glob = "*.jpg";         long size = 102400; //100 kilobytes in bytes         Path fileTree = Paths.get("C:/rafaelnadal/");         Search walk = new Search(glob, size);         EnumSet opts = EnumSet.of(FileVisitOption.FOLLOW_LINKS);

        Files.walkFileTree(fileTree, opts, Integer.MAX_VALUE, walk);           } }`

以下是找到的文件输出的片段:


Searched file was found: rafa_winner.jpg in C:\rafaelnadal\photos\rafa_winner.jpg size (bytes):77718

按内容搜索文件

其中一种高级文件搜索包括按内容查找文件。您传递一系列单词或句子,搜索只返回包含该文本的文件。这是最耗时的文件搜索任务,因为它需要在每个访问过的文件中搜索文本,这意味着打开文件,阅读文件,最后关闭文件。此外,还有许多支持文本的文件格式,如 PDF、Microsoft Word、Excel 和 PowerPoint、简单文本文件、XML、HTML、XHTML 等等。每种格式都有不同的读取方式,这需要能够从中提取文本文件的专用代码。

在这一节中,我们将开发一个根据内容搜索文件的应用。要搜索的文本作为包含由逗号分隔的单词或句子序列的String传递;比如:“拉斐尔·纳达尔,网球,罗兰·加洛斯冠军,巴黎银行锦标赛平局”。使用StringTokenizer类,逗号作为分隔符,下面的示例将每个单词和句子提取到一个ArrayList中:

…
String words="Rafael Nadal,tennis,winner of Roland Garros,BNP Paribas tournament draws";
ArrayList<String> wordsarray = new ArrayList<>();
…
StringTokenizer st = new StringTokenizer(words, ",");
while (st.hasMoreTokens()) {
       wordsarray.add(st.nextToken());
}

下面的代码循环这个ArrayList,并将每个单词和句子与从被访问文件中提取的文本进行比较。注意在searchText()方法中,提取的文本作为参数传递。

//search text
private boolean searchText(String text) {

   boolean flag = false;
   for (int j = 0; j < wordsarray.size(); j++) {
        if ((text.toLowerCase()).contains(wordsarray.get(j).toLowerCase())) {
            flag = true;
            break;
       }
   }
   return flag;
}

下面的小节集中在从一些最常见的文件格式中提取文本并进行比较的一组方法。因为我们不打算在这里重新发明轮子,我们将利用一些专门为理解特定文件格式而编写的第三方库。然后我们将把我们开发的每一种方法组合成一个完整的搜索程序。

在 pdf 中搜索

为了阅读 PDF 文件,我们将使用两个最流行的第三方开源库,iText 和 Apache PDFBox。您可以从[itextpdf.com/](http://itextpdf.com/)下载 iText 库,从[pdfbox.apache.org/](http://pdfbox.apache.org/)下载 PDFBox 库。出于本章的目的,我使用了 iText 的 5.1.2 版本和 PDFBox 的 1.6.0 版本。基于 iText 文档,我编写了下面的方法来从 PDF 中提取文本。第一步是在被访问的文件上创建一个PdfReader。继续提取 PDF 的页数,从每一页提取文本,并将提取的文本传递给searchText()方法。如果在提取的文本中找到其中一个标记,那么在当前文件中的搜索将停止,该文件将被视为有效的搜索结果,其路径和名称将被存储,以便我们可以在整个搜索结束后将其打印出来。

//search in PDF files using iText library
boolean searchInPDF_iText(String file) {

   PdfReader reader = null;
   boolean flag = false;

   try {
       reader = new PdfReader(file);
       int n = reader.getNumberOfPages();

       OUTERMOST:
       for (int i = 1; i <= n; i++) {
         String str = PdfTextExtractor.getTextFromPage(reader, i);

         flag = searchText(str);
         if (flag) {
               break OUTERMOST;
         }
       }

   } catch (Exception e) {
      } finally {
          if (reader != null) {
              reader.close();
            }
            return flag;
      }
}

如果你对 PDFBox 比对 iText 更熟悉,那么试试下面的方法。首先在 PDF 文件上创建一个PDFParser,然后提取页数,最后提取每页的文本并将其传递给searchText()方法。

`boolean searchInPDF_PDFBox(String file) {

   PDFParser parser = null;    String parsedText = null;    PDFTextStripper pdfStripper = null;    PDDocument pdDoc = null;    COSDocument cosDoc = null;    boolean flag = false;    int page = 0;

   File pdf = new File(file);

   try {        parser = new PDFParser(new FileInputStream(pdf));        parser.parse();

       cosDoc = parser.getDocument();        pdfStripper = new PDFTextStripper();        pdDoc = new PDDocument(cosDoc);

       OUTERMOST:        while (page < pdDoc.getNumberOfPages()) {             page++;             pdfStripper.setStartPage(page);             pdfStripper.setEndPage(page + 1);             parsedText = pdfStripper.getText(pdDoc);

            flag = searchText(parsedText);             if (flag) {                  break OUTERMOST;             }        }    } catch (Exception e) {    } finally {           try {              if (cosDoc != null) {                   cosDoc.close();              }              if (pdDoc != null) {                   pdDoc.close();              }           } catch (Exception e) {}    return flag;    } }`

在 Microsoft Word、Excel 和 PowerPoint 文件中搜索

Microsoft Office 套件的文件可以通过 Apache POI 库进行操作,Apache POI 库是 Microsoft 文档最常用的 Java API。你可以从[poi.apache.org/](http://poi.apache.org/)下载这个库。出于本章的目的,我使用了 3.7 版本。根据开发人员指南,我编写了以下从 Word 文档中提取文本的方法。Apache POI 提取一个包含 Word 文档所有段落的数组String。数组可以循环,每一段都可以传递给searchText()方法。

boolean searchInWord(String file) {

   POIFSFileSystem fs = null;
   boolean flag = false;

   try {
       fs = new POIFSFileSystem(new FileInputStream(file));

       HWPFDocument doc = new HWPFDocument(fs);
       WordExtractor we = new WordExtractor(doc);
       String[] paragraphs = we.getParagraphText();

       OUTERMOST:
       for (int i = 0; i < paragraphs.length; i++) {

             flag = searchText(paragraphs[i]);
             if (flag) {
                   break OUTERMOST;
             }
       }

     } catch (Exception e) {
     } finally {
            return flag;
     }
}

我们可以从 Excel 文件中提取文本,如下例所示。在为 Excel 文档创建了一个HSSFWorkbook之后,基本思想是遍历工作表,然后遍历行,最后遍历单元格。单元格应该包含我们要查找的特定文本。

`boolean searchInExcel(String file) {

   Row row;    Cell cell;    String text;    boolean flag = false;    InputStream xls = null;

   try {        xls = new FileInputStream(file);        HSSFWorkbook wb = new HSSFWorkbook(xls);

       int sheets = wb.getNumberOfSheets();

       OUTERMOST:        for (int i = 0; i < sheets; i++) {             HSSFSheet sheet = wb.getSheetAt(i);

            Iterator row_iterator = sheet.rowIterator();             while (row_iterator.hasNext()) {                 row = (Row) row_iterator.next();                 Iterator cell_iterator = row.cellIterator();                 while (cell_iterator.hasNext()) {                     cell = cell_iterator.next();                     int type = cell.getCellType();                     if (type == HSSFCell.CELL_TYPE_STRING) {                           text = cell.getStringCellValue();                           flag = searchText(text);                           if (flag) {                                 break OUTERMOST;                           }                     }                 }             }        }

   } catch (IOException e) {    } finally {          try {              if (xls != null) {                     xls.close();              }          } catch (IOException e) {}    return flag;    } }`

最后,我们可以从 PowerPoint 文件中提取文本,如下例所示;每张幻灯片可能包含文本和注释:

`boolean searchInPPT(String file) {

   boolean flag = false;    InputStream fis = null;    String text;

   try {        fis = new FileInputStream(new File(file));        POIFSFileSystem fs = new POIFSFileSystem(fis);        HSLFSlideShow show = new HSLFSlideShow(fs);

       SlideShow ss = new SlideShow(show);        Slide[] slides = ss.getSlides();

       OUTERMOST:        for (int i = 0; i < slides.length; i++) {

          TextRun[] runs = slides[i].getTextRuns();           for (int j = 0; j < runs.length; j++) {              TextRun run = runs[j];              if (run.getRunType() == TextHeaderAtom.TITLE_TYPE) {                  text = run.getText();              } else {                  text = run.getRunType() + " " + run.getText();              }

             flag = searchText(text);              if (flag) {                     break OUTERMOST;              }           }

       Notes notes = slides[i].getNotesSheet();        if (notes != null) {            runs = notes.getTextRuns();            for (int j = 0; j < runs.length; j++) {                 text = runs[j].getText();                 flag = searchText(text);                 if (flag) {                       break OUTERMOST;                 }            }        }      }    } catch (IOException e) {    } finally {          try {            if (fis != null) {                  fis.close();              }           } catch (IOException e) {}    return flag;    } }`

Image 注意我任意选择了前面例子中使用的第三方库。还有许多其他的开源和商业库可以用来处理不同种类的文档。请随意使用任何方便您需求的东西。我们的搜索示例并不是最有效的搜索方式。在最坏的情况下,我们必须遍历整个数组(典型情况下是数组的一半)。也许使用 Apache Lucene ( [lucene.apache.org/java/docs/index.html](http://lucene.apache.org/java/docs/index.html))提供的索引搜索是更好的方法。这是一个你可以自己尝试的练习。

在文本文件中搜索

文本文件(.txt.html.xml等)。)不需要第三方库。可以使用纯 NIO.2 代码读取它们,如下所示:

boolean searchInText(Path file) { `boolean flag = false;    Charset charset = Charset.forName("UTF-8");    try (BufferedReader reader = Files.newBufferedReader(file, charset)) {         String line = null;

        OUTERMOST:         while ((line = reader.readLine()) != null) {               flag = searchText(line);               if (flag) {                    break OUTERMOST;               }         }

    } catch (IOException e) {     } finally {           return flag;     } }`

编写一个完整的搜索程序

没错。馅饼做好了!把它扔进烤箱!我们有搜索的文本、从一组常见文件格式中提取的文本,以及检查提取的文本是否包含搜索的文本的方法。将所有东西放入遍历过程,应用就准备好了:

import com.itextpdf.text.pdf.PdfReader; import com.itextpdf.text.pdf.parser.PdfTextExtractor; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; import java.nio.file.FileSystems; import java.nio.file.FileVisitOption; import java.nio.file.FileVisitResult; import java.nio.file.FileVisitor; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.EnumSet; import java.util.Iterator; import java.util.StringTokenizer; import org.apache.pdfbox.cos.COSDocument; import org.apache.pdfbox.pdfparser.PDFParser; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.util.PDFTextStripper; import org.apache.poi.hslf.HSLFSlideShow; import org.apache.poi.hslf.model.Notes; import org.apache.poi.hslf.model.Slide; `import org.apache.poi.hslf.model.TextRun; import org.apache.poi.hslf.record.TextHeaderAtom; import org.apache.poi.hslf.usermodel.SlideShow; import org.apache.poi.hssf.usermodel.HSSFCell; import org.apache.poi.hssf.usermodel.HSSFSheet; import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.hwpf.HWPFDocument; import org.apache.poi.hwpf.extractor.WordExtractor; import org.apache.poi.poifs.filesystem.POIFSFileSystem; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Row;

class Search implements FileVisitor {

    ArrayList wordsarray = new ArrayList<>();     ArrayList documents = new ArrayList<>();     boolean found = false;

    public Search(String words) {         wordsarray.clear();         documents.clear();

        StringTokenizer st = new StringTokenizer(words, ",");         while (st.hasMoreTokens()) {             wordsarray.add(st.nextToken().trim());         }     }

    void search(Path file) throws IOException {

        found = false;

        String name = file.getFileName().toString();         int mid = name.lastIndexOf(".");         String ext = name.substring(mid + 1, name.length());

        if (ext.equalsIgnoreCase("pdf")) {             found = searchInPDF_iText(file.toString());             if (!found) {                 found = searchInPDF_PDFBox(file.toString());             }         }

        if (ext.equalsIgnoreCase("doc") || ext.equalsIgnoreCase("docx")) {             found = searchInWord(file.toString());         }

        if (ext.equalsIgnoreCase("ppt")) {             searchInPPT(file.toString());         }

        if (ext.equalsIgnoreCase("xls")) { searchInExcel(file.toString());         }

        if ((ext.equalsIgnoreCase("txt")) || (ext.equalsIgnoreCase("xml")                                           || ext.equalsIgnoreCase("html"))                 || ext.equalsIgnoreCase("htm") || ext.equalsIgnoreCase("xhtml")                                                || ext.equalsIgnoreCase("rtf")) {             searchInText(file);         }

        if (found) {             documents.add(file.toString());         }     }

    //search in text files     boolean searchInText(Path file) {

        boolean flag = false;         Charset charset = Charset.forName("UTF-8");         try (BufferedReader reader = Files.newBufferedReader(file, charset)) {             String line = null;

            OUTERMOST:             while ((line = reader.readLine()) != null) {                 flag = searchText(line);                 if (flag) {                     break OUTERMOST;                 }             }

        } catch (IOException e) {         } finally {             return flag;         }     }

    //search in Excel files     boolean searchInExcel(String file) {

        Row row;         Cell cell;         String text;         boolean flag = false;         InputStream xls = null;

        try {             xls = new FileInputStream(file);             HSSFWorkbook wb = new HSSFWorkbook(xls);

            int sheets = wb.getNumberOfSheets(); OUTERMOST:             for (int i = 0; i < sheets; i++) {                 HSSFSheet sheet = wb.getSheetAt(i);

                Iterator row_iterator = sheet.rowIterator();                 while (row_iterator.hasNext()) {                     row = (Row) row_iterator.next();                     Iterator cell_iterator = row.cellIterator();                     while (cell_iterator.hasNext()) {                         cell = cell_iterator.next();                         int type = cell.getCellType();                         if (type == HSSFCell.CELL_TYPE_STRING) {                             text = cell.getStringCellValue();                             flag = searchText(text);                             if (flag) {                                 break OUTERMOST;                             }                         }                     }                 }             }

        } catch (IOException e) {         } finally {             try {                 if (xls != null) {                     xls.close();                 }             } catch (IOException e) {             }             return flag;         }     }

    //search in PowerPoint files     boolean searchInPPT(String file) {

        boolean flag = false;         InputStream fis = null;         String text;

        try {             fis = new FileInputStream(new File(file));             POIFSFileSystem fs = new POIFSFileSystem(fis);             HSLFSlideShow show = new HSLFSlideShow(fs);

            SlideShow ss = new SlideShow(show);             Slide[] slides = ss.getSlides();

            OUTERMOST:             for (int i = 0; i < slides.length; i++) { TextRun[] runs = slides[i].getTextRuns();                 for (int j = 0; j < runs.length; j++) {                     TextRun run = runs[j];                     if (run.getRunType() == TextHeaderAtom.TITLE_TYPE) {                         text = run.getText();                     } else {                         text = run.getRunType() + " " + run.getText();                     }

                    flag = searchText(text);                     if (flag) {                         break OUTERMOST;                     }

                }

                Notes notes = slides[i].getNotesSheet();                 if (notes != null) {                     runs = notes.getTextRuns();                     for (int j = 0; j < runs.length; j++) {                         text = runs[j].getText();                         flag = searchText(text);                         if (flag) {                             break OUTERMOST;                         }                     }                 }             }

        } catch (IOException e) {         } finally {             try {                 if (fis != null) {                     fis.close();                 }             } catch (IOException e) {             }             return flag;         }

    }

    //search in Word files     boolean searchInWord(String file) {

        POIFSFileSystem fs = null;         boolean flag = false;

        try {             fs = new POIFSFileSystem(new FileInputStream(file));

            HWPFDocument doc = new HWPFDocument(fs); WordExtractor we = new WordExtractor(doc);             String[] paragraphs = we.getParagraphText();

            OUTERMOST:             for (int i = 0; i < paragraphs.length; i++) {

                flag = searchText(paragraphs[i]);                 if (flag) {                     break OUTERMOST;                 }             } } catch (Exception e) {         } finally {             return flag;         }     }

    //search in PDF files using PDFBox library     boolean searchInPDF_PDFBox(String file) {

        PDFParser parser = null;         String parsedText = null;         PDFTextStripper pdfStripper = null;         PDDocument pdDoc = null;         COSDocument cosDoc = null;         boolean flag = false;         int page = 0;

        File pdf = new File(file);

        try {             parser = new PDFParser(new FileInputStream(pdf));             parser.parse();

            cosDoc = parser.getDocument();             pdfStripper = new PDFTextStripper();             pdDoc = new PDDocument(cosDoc);

            OUTERMOST:             while (page < pdDoc.getNumberOfPages()) {                 page++;                 pdfStripper.setStartPage(page);                 pdfStripper.setEndPage(page + 1);                 parsedText = pdfStripper.getText(pdDoc);

                flag = searchText(parsedText);                 if (flag) {                     break OUTERMOST;                 }             }

        } catch (Exception e) {         } finally {             try {                 if (cosDoc != null) {                     cosDoc.close();                 }                 if (pdDoc != null) {                     pdDoc.close();                 }             } catch (Exception e) {             }             return flag;         }     }

    //search in PDF files using iText library     boolean searchInPDF_iText(String file) {

        PdfReader reader = null;         boolean flag = false;

        try {             reader = new PdfReader(file);             int n = reader.getNumberOfPages();

            OUTERMOST:             for (int i = 1; i <= n; i++) {                 String str = PdfTextExtractor.getTextFromPage(reader, i);

                flag = searchText(str);                 if (flag) {                     break OUTERMOST;                 }             }

        } catch (Exception e) {         } finally {             if (reader != null) {                 reader.close();             }             return flag;         }

    }

    //search text     private boolean searchText(String text) {

        boolean flag = false;         for (int j = 0; j < wordsarray.size(); j++) {             if ((text.toLowerCase()).contains(wordsarray.get(j).toLowerCase())) {                 flag = true; break;             }         }

        return flag;     }

    @Override     public FileVisitResult postVisitDirectory(Object dir, IOException exc)                                                                       throws IOException {         System.out.println("Visited: " + (Path) dir);         return FileVisitResult.CONTINUE;     }

    @Override     public FileVisitResult preVisitDirectory(Object dir, BasicFileAttributes attrs)                                                                       throws IOException {         return FileVisitResult.CONTINUE;     }

    @Override     public FileVisitResult visitFile(Object file, BasicFileAttributes attrs)                                                                       throws IOException {         search((Path) file);         return FileVisitResult.CONTINUE;     }

    @Override     public FileVisitResult visitFileFailed(Object file, IOException exc)                                                                       throws IOException {         //report an error if necessary

        return FileVisitResult.CONTINUE;     } }

class Main {

public static void main(String[] args) throws IOException {

 String words = "Rafael Nadal, tennis, winner of Roland Garros, BNP Paribas tournament draws";  Search walk = new Search(words);  EnumSet opts = EnumSet.of(FileVisitOption.FOLLOW_LINKS);

 Iterable dirs = FileSystems.getDefault().getRootDirectories();  for (Path root : dirs) {       Files.walkFileTree(root, opts, Integer.MAX_VALUE, walk);  }

 System.out.println("");  for(String path_string: walk.documents){      System.out.println(path_string);  }  System.out.println("");

 } }`

请注意,有时这是一个相当慢的过程,可能需要几秒到几十分钟,运行时间会因文件树大小、检查的文件数量以及这些文件的大小而异。在前面的示例中,文件树包含默认文件系统中的所有文件存储,因此对于我们的搜索词集,将以任何受支持的格式打开、读取和浏览每个文件。根据匹配文件的大小和数量,在返回结果时,该过程可能会出现几秒钟的阻塞。您可以通过添加更多的文件格式、指示进程状态的进度条或标志以及加速进程的多线程来改进该应用。此外,显示找到的文件名可能比存储文件名和路径更好。

编写文件删除应用

删除单个文件是一个简单的操作,正如你在第四章中看到的“删除文件和目录”在您调用了delete()deleteIfExists()方法之后,该文件将从您的文件系统中删除。删除整个文件树是基于通过一个FileVisitor实现递归调用delete()deleteIfExists()方法的操作。在看到示例之前,您需要记住以下几点:

  • 删除目录之前,必须删除其中的所有文件。
  • visitFile()方法是执行每个文件删除的最佳位置。
  • 因为只有当目录为空时才能删除,所以建议使用postVisitDirectory()方法删除目录。
  • 如果一个文件不能被访问,visitFileFailed()方法应该返回FileVisitResult.CONTINUETERMINATE,这取决于您的决定。
  • 删除过程可以遵循符号链接,这可能是不可取的,因为符号链接可能会指向删除域之外的文件。但是如果你确定这种情况永远不会发生,或者一个补充条件阻止了不期望的删除,那么就跟随符号链接。

我们在这一节的目标是创建一个删除整个文件树的应用。以下代码删除了C:\rafaelnadal目录(为了进一步使用,请在运行以下代码之前备份该目录):

import java.io.IOException; import java.nio.file.FileVisitOption; import java.nio.file.FileVisitResult; import java.nio.file.FileVisitor; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.BasicFileAttributes; import java.util.EnumSet; `class DeleteDirectory implements FileVisitor {    

 boolean deleteFileByFile(Path file) throws IOException {     return Files.deleteIfExists(file);  }

 @Override  public FileVisitResult postVisitDirectory(Object dir, IOException exc)                                                                   throws IOException {

    if (exc == null) {         System.out.println("Visited: " + (Path) dir);         boolean success = deleteFileByFile((Path) dir);

        if (success) {              System.out.println("Deleted: " + (Path) dir);         } else {               System.out.println("Not deleted: " + (Path) dir);         }     } else {         throw exc;     }     return FileVisitResult.CONTINUE;  }

 @Override  public FileVisitResult preVisitDirectory(Object dir, BasicFileAttributes attrs)                                                                   throws IOException {    return FileVisitResult.CONTINUE;  }

 @Override  public FileVisitResult visitFile(Object file, BasicFileAttributes attrs)                                                                   throws IOException {    boolean success = deleteFileByFile((Path) file);

   if (success) {         System.out.println("Deleted: " + (Path) file);    } else {         System.out.println("Not deleted: " + (Path) file);    }

   return FileVisitResult.CONTINUE;  }

 @Override  public FileVisitResult visitFileFailed(Object file, IOException exc)                                                                   throws IOException {    //report an error if necessary

   return FileVisitResult.CONTINUE;  } }

class Main {

 public static void main(String[] args) throws IOException {

   Path directory = Paths.get("C:/rafaelnadal");    DeleteDirectory walk = new DeleteDirectory();    EnumSet opts = EnumSet.of(FileVisitOption.FOLLOW_LINKS);

   Files.walkFileTree(directory, opts, Integer.MAX_VALUE, walk);          } }`

Image 注意将删除的文件发送到回收站可以通过使用 JNI 调用 Windows API SHFileOperation()方法来完成。查看大卫·谢伊在 www.jroller.com/ethdsy/entr… 的帖子了解更多细节。

编写复制文件应用

复制文件树需要为每个遍历的文件和目录调用Files.copy()方法。(有关在 NIO.2 中复制文件或目录的详细信息,请参考第四章的“复制文件和目录”一节)在你看到一个例子之前,这里有一些要点要记住:

  • 在从目录中复制任何文件之前,您必须复制目录本身。复制源目录(空或不空)将导致一个空的目标目录。该任务必须在preVisitDirectory()方法中完成。
  • visitFile()方法是复制每个文件的最佳地方。
  • 当您复制文件或目录时,您需要决定是否要使用REPLACE_EXISTINGCOPY_ATTRIBUTES选项。
  • 如果您想保留源目录的属性,您需要在文件被复制之后,在postVisitDirectory()方法中这样做。
  • 如果你选择跟随链接(FOLLOW_LINKS)并且你的文件树有一个到父目录的循环链接,循环目录在visitFileFailed()方法中被报告,带有FileSystemLoopException异常。
  • 如果一个文件不能被访问,visitFileFailed()方法应该返回FileVisitResult.CONTINUETERMINATE,这取决于您的决定。
  • 如果您指定了FOLLOW_LINKS选项,复制过程可以遵循符号链接。

下面的代码片段结合了前面的概念,并将the C:\rafaelnadal子树复制到C:\rafaelnadal_copy文件树中:

import java.nio.file.FileSystemLoopException; `import java.nio.file.attribute.FileTime; import java.io.IOException; import java.nio.file.FileVisitOption; import java.nio.file.FileVisitResult; import java.nio.file.FileVisitor; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.BasicFileAttributes; import java.util.EnumSet; import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; import static java.nio.file.StandardCopyOption.COPY_ATTRIBUTES;

class CopyTree implements FileVisitor {

   private final Path copyFrom;    private final Path copyTo;

   public CopyTree(Path copyFrom, Path copyTo) {         this.copyFrom = copyFrom;         this.copyTo = copyTo;     }

   static void copySubTree(Path copyFrom, Path copyTo) throws IOException {         try {             Files.copy(copyFrom, copyTo, REPLACE_EXISTING, COPY_ATTRIBUTES);         } catch (IOException e) {             System.err.println("Unable to copy " + copyFrom + " [" + e + "]");         }

    }

   @Override    public FileVisitResult postVisitDirectory(Object dir, IOException exc)                                                                      throws IOException {         if (exc == null) {             Path newdir = copyTo.resolve(copyFrom.relativize((Path) dir));             try {                 FileTime time = Files.getLastModifiedTime((Path) dir);                 Files.setLastModifiedTime(newdir, time);             } catch (IOException e) {                 System.err.println("Unable to copy all attributes to: " + newdir+" ["+e+ "]");             }         } else {             throw exc;         }

        return FileVisitResult.CONTINUE;     }

    @Override     public FileVisitResult preVisitDirectory(Object dir, BasicFileAttributes attrs)                                                                       throws IOException {         System.out.println("Copy directory: " + (Path) dir);         Path newdir = copyTo.resolve(copyFrom.relativize((Path) dir));         try {             Files.copy((Path) dir, newdir, REPLACE_EXISTING, COPY_ATTRIBUTES);         } catch (IOException e) {             System.err.println("Unable to create " + newdir + " [" + e + "]");             return FileVisitResult.SKIP_SUBTREE;         }

        return FileVisitResult.CONTINUE;     }

    @Override     public FileVisitResult visitFile(Object file, BasicFileAttributes attrs)                                                                       throws IOException {         System.out.println("Copy file: " + (Path) file);         copySubTree((Path) file, copyTo.resolve(copyFrom.relativize((Path) file)));         return FileVisitResult.CONTINUE;     }

    @Override     public FileVisitResult visitFileFailed(Object file, IOException exc)                                                                      throws IOException {         if (exc instanceof FileSystemLoopException) {             System.err.println("Cycle was detected: " + (Path) file);         } else {             System.err.println("Error occurred, unable to copy:" +(Path) file+" ["+ exc + "]");         }

        return FileVisitResult.CONTINUE;     } }

class Main {

    public static void main(String[] args) throws IOException {

        Path copyFrom = Paths.get("C:/rafaelnadal");         Path copyTo = Paths.get("C:/rafaelnadal_copy");

        CopyTree walk = new CopyTree(copyFrom, copyTo);         EnumSet opts = EnumSet.of(FileVisitOption.FOLLOW_LINKS);

        Files.walkFileTree(copyFrom, opts, Integer.MAX_VALUE, walk);     } }`

运行前面的应用后,您会发现一个与C:\rafaelnadal源具有相同内容和属性的C:\rafaelnadal_copy目标。

编写一个移动文件应用

移动文件树是将复制和删除文件树的步骤组合到单个应用中的任务。(有关移动文件的更多详细信息,请参考第四章的“移动文件和目录”一节。)实际上,移动文件树通常有两种方法:组合使用Files.move()Files.copy()Files.delete(),或者只使用Files.copy()Files.delete()。根据您选择的方法,应该相应地执行FileVisitor来完成移动文件树的任务。在看到示例之前,您需要记住以下几点:

  • 在从目录中移动任何文件之前,您必须移动目录本身。由于不能移动非空目录(只能移动空目录),您需要使用Files.copy()方法,它将复制一个空目录。该任务必须在preVisitDirectory()方法中完成。
  • visitFile()方法是移动每个文件的最佳位置。为此,您可以使用Files.move()方法,或者将Files.copy()Files.delete()结合使用。
  • 当一个源目录中的所有文件都被移动到目标目录中后,你需要调用Files.delete()来删除源目录,此时源目录应该是空的。该任务必须在postVisitDirectory()方法中完成。
  • 当您复制文件或目录时,您需要决定是否要使用REPLACE_EXISTINGCOPY_ATTRIBUTES选项。此外,当你移动一个文件或目录时,你需要决定是否需要ATOMIC_MOVE
  • 如果您想保留源目录的属性,您需要在文件被移动之后,在postVisitDirectory()方法中这样做。一些属性,如lastModifiedTime,应该在preVisitDirectory()方法中提取并存储,直到它们在postVisitDirectory()中被设置。原因是从源目录移动文件后,目录内容发生了变化,初始的最后修改时间被新日期覆盖。
  • 如果一个文件不能被访问,visitFileFailed()方法应该返回FileVisitResult.CONTINUETERMINATE,这取决于您的决定。
  • 如果指定了FOLLOW_LINKS选项,移动过程可以遵循符号链接。请记住,移动符号链接会移动链接本身,而不是链接的目标。

下面的代码片段将C:\rafaelnadal目录的内容移动到C:\ATP\players\rafaelnafal目录中(在测试之前,您必须手动创建文件夹C:\ATP\players\)。在这种情况下,使用Files.copy()Files.delete()移动目录和子目录,使用Files.move()移动文件。

import java.io.IOException; import java.nio.file.FileVisitOption; import java.nio.file.FileVisitResult; import java.nio.file.FileVisitor; import java.nio.file.Files; `import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; import java.util.EnumSet; import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; import static java.nio.file.StandardCopyOption.COPY_ATTRIBUTES; import static java.nio.file.StandardCopyOption.ATOMIC_MOVE;

class MoveTree implements FileVisitor {

   private final Path moveFrom;    private final Path moveTo;    static FileTime time = null;

   public MoveTree(Path moveFrom, Path moveTo) {         this.moveFrom = moveFrom;         this.moveTo = moveTo;    }

   static void moveSubTree(Path moveFrom, Path moveTo) throws IOException {         try {             Files.move(moveFrom, moveTo, REPLACE_EXISTING, ATOMIC_MOVE);         } catch (IOException e) {             System.err.println("Unable to move " + moveFrom + " [" + e + "]");         }

   }

   @Override    public FileVisitResult postVisitDirectory(Object dir, IOException exc)                                                                      throws IOException {         Path newdir = moveTo.resolve(moveFrom.relativize((Path) dir));         try {             Files.setLastModifiedTime(newdir, time);             Files.delete((Path) dir);         } catch (IOException e) {             System.err.println("Unable to copy all attributes to: " + newdir+" [" + e + "]");         }

        return FileVisitResult.CONTINUE;    }

   @Override    public FileVisitResult preVisitDirectory(Object dir, BasicFileAttributes attrs)                                                                         throws IOException {         System.out.println("Move directory: " + (Path) dir);         Path newdir = moveTo.resolve(moveFrom.relativize((Path) dir));         try {             Files.copy((Path) dir, newdir, REPLACE_EXISTING, COPY_ATTRIBUTES);             time = Files.getLastModifiedTime((Path) dir);         } catch (IOException e) {             System.err.println("Unable to move " + newdir + " [" + e + "]");             return FileVisitResult.SKIP_SUBTREE;         }

        return FileVisitResult.CONTINUE;    }

   @Override    public FileVisitResult visitFile(Object file, BasicFileAttributes attrs)                                                                         throws IOException {         System.out.println("Move file: " + (Path) file);         moveSubTree((Path) file, moveTo.resolve(moveFrom.relativize((Path) file)));         return FileVisitResult.CONTINUE;    }

   @Override    public FileVisitResult visitFileFailed(Object file, IOException exc)                                                                         throws IOException {         return FileVisitResult.CONTINUE;    } }

class Main {

    public static void main(String[] args) throws IOException {

        Path moveFrom = Paths.get("C:/rafaelnadal");         Path moveTo = Paths.get("C:/ATP/players/rafaelnadal");

        MoveTree walk = new MoveTree(moveFrom, moveTo);         EnumSet opts = EnumSet.of(FileVisitOption.FOLLOW_LINKS);

        Files.walkFileTree(moveFrom, opts, Integer.MAX_VALUE, walk);     } }`

您可以不使用Files.move()来完成同样的任务,因为每次移动都只是一对复制和删除操作。例如,您可以重写moveSubTree()方法来使用Files.copy()Files.delete()来移动文件:

static void moveSubTree(Path moveFrom, Path moveTo) throws IOException {
        try {
            Files.copy(moveFrom, moveTo, REPLACE_EXISTING, COPY_ATTRIBUTES);
            Files.delete(moveFrom);
        } catch (IOException e) {
            System.err.println("Unable to move " + moveFrom + " [" + e + "]");
        }
    }

总结

本章着重于开发文件和目录的递归操作。在简单介绍了递归编程技术之后,您了解了FileVisitor接口和SimpleFileVisitor实现。然后,您看到了如何开发一组应用,您可以使用它们来执行涉及遍历文件树的任务,例如查找、复制、删除和移动文件。

六、监视服务 API

监视服务 API 是在 Java 7 (NIO.2)中作为线程安全服务引入的,它能够监视对象的变化和事件。最常见的用途是通过创建、删除和修改等操作来监视目录内容的变化。您可能已经多次看到这种服务的效果。例如,当你在编辑器中打开一个文本文件(如 GridinSoft 记事本、jEdit 等。)并且文件内容在编辑器外被修改,您将看到一条消息,询问您是否要重新加载该文件,因为它已被修改。这意味着编辑器已经通过监视服务检测到文件更改,并相应地报告它。这就是所谓的文件更改通知机制,从 NIO.2 开始,它可以通过 Watch Service API 获得。

Watch Service API 是一个低级 API,可以按原样使用,也可以进行定制。你甚至可以在它上面写一个高级 API。默认情况下,这个 API 使用底层文件系统功能来监视文件系统的变化。它允许您注册一个(或多个)目录,以便针对您在注册期间指定的不同类型的通知事件进行监控。当观察器服务检测到一个或多个已注册的通知事件时,观察器服务将通知事件传递给已注册的进程,以便通过单独的线程或线程池来处理它们。

Image 注意从 NIO.2 开始,您不再需要轮询文件系统的变化或者使用其他内部解决方案来监控文件系统的变化。在以前的 Java 版本中,您必须实现一个在单独线程中运行的代理,该代理跟踪被监视目录的所有内容,不断轮询文件系统以查看是否发生了任何重要的事情。现在,无论您运行的是 Mac OS X、Linux、Unix、Windows 还是其他操作系统,您都可以保证底层操作系统和文件系统提供所需的功能,允许 Java 注册以接收文件系统更改的通知。

在本章中,您将看到如何基于提供的 Watch Service API 开发应用。实现一个功能应用并不容易,所以我们将从最简单的情况开始,应用监视单个目录的变化。之后,您将看到如何递归地监视您已经注册要被监视的目录树。此外,我们将开发另外两个不太通用的应用,它们封装了现实生活中的案例。为了帮助您入门,本章提供了编写基于 Watch Service API 的应用所涉及的主要类的概述。

监视服务 API 类

java.nio.file.WatchService接口是这个 API 的起点。对于不同的文件系统和操作系统,它有多种实现。您可以使用这个接口和三个类来开发一个具有文件系统监视功能的系统。下面的项目符号概述了这些类:

  • 可观察对象:如果一个对象代表了一个实现了java.nio.file.Watchable接口的类的实例,那么这个对象就是“可观察的”。在我们的例子中,这是 NIO 2 最重要的类,也就是众所周知的Path类。
  • 事件类型:这是我们感兴趣监控的事件列表。只有在 register 调用中指定了事件时,事件才会触发通知。标准支持的事件由java.nio.file.StandardWatchEventKinds类表示,包括创建、删除和修改。这个类实现了WatchEvent.Kind<T>接口。
  • 事件修饰符:这限定了Watchable如何向WatchService注册。在撰写本文时,NIO.2 还没有定义任何标准修饰符。
  • 守望者:守望者观看监视!在我们的例子中,观察者是WatchService,它监视文件系统的变化(文件系统是一个FileSystem实例)。正如您将看到的,WatchService将通过FileSystem类创建。它会在后台静静地看着注册的Path

实现监视服务

实现监视服务是一项需要完成一系列步骤的任务。在本节中,您将看到开发监视服务的主要步骤,该服务监视给定目录的三个通知事件:删除、创建和修改。每一步都有一段代码支持,演示了如何实际完成该步骤。最后,我们将把这些块粘合在一起,形成一个完整的监视服务功能示例。

创建监视服务

我们从创建一个用于监控文件系统的WatchService开始我们的旅程。为此我们称之为FileSystem.newWatchService()方法:

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

我们现在有一个监视服务供我们使用。

向监视服务注册对象

应该被监视的每个对象都必须向监视服务显式注册。我们可以注册任何实现了Watchable接口的对象。对于我们的例子,我们将注册作为Path类实例的目录。除了被监视的对象之外,注册过程还需要服务应该监视和通知的事件的标识。受支持的事件类型在StandardWatchEventKinds类下映射为Kind<Path>类型的常量:

  • StandardWatchEventKinds.ENTRY_CREATE:创建一个目录条目。当文件被重命名或移动到这个目录中时,也会触发一个ENTRY_CREATE事件。
  • StandardWatchEventKinds.ENTRY_DELETE:删除一个目录条目。当文件被重命名或移出该目录时,也会触发一个ENTRY_DELETE事件。
  • StandardWatchEventKinds.ENTRY_MODIFY:修改一个目录条目。哪些事件构成修改在某种程度上是特定于平台的,但是实际上修改文件的内容总是会触发一个修改事件。在某些平台上,更改文件属性也会触发此事件。
  • StandardWatchEventKinds.OVERFLOW:表示事件可能已经丢失或被丢弃。您不必注册参加OVERFLOW活动就能收到它。

因为Path类实现了Watchable接口,所以它提供了Watchable.register()方法。有两种这样的方法专用于向 watch 服务注册对象。其中一个接收两个参数,表示该对象要注册到的监视服务和该对象应该注册到的事件。第二个 register 方法也接收这两个参数,第三个参数指定限定目录注册方式的修饰符。在撰写本文时,NIO.2 没有提供任何标准的修饰符。

下面的代码片段向观察服务注册了Path 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返回给您。下一节将提供更多关于监视钥匙的详细信息。

等待到来的事件

等待到来的事件需要一个无限循环。当一个事件发生时,watch 服务负责发送相应的 watch 键信号,并将其放入 watcher 的队列中,我们可以从队列中检索它——我们说 watch 键被排队。因此,我们的无限循环可能是以下类型:

while(true){
//retrieve and process the incoming events
…
}

或者它可以是以下类型:

for(;;){
//retrieve and process the incoming events
…
}
拿到监视钥匙

检索排队的键可以通过调用WatchService类的以下三个方法之一来完成。这三种方法都检索下一个键,并将其从队列中删除。如果没有可用的密钥,它们的响应会有所不同,如下所述:

  • poll():如果没有可用的键,则立即返回一个null值。
  • poll(long, TimeUnit):如果没有可用的按键,它会等待指定的时间,然后再次尝试。如果仍然没有可用的密钥,那么它返回null。时间段用一个long数字表示,而TimeUnit参数决定指定的时间是分钟、秒、毫秒还是其他时间单位。
  • take():如果没有可用的密钥,它将一直等待,直到一个密钥被排队或者无限循环由于任何不同的原因而停止。

以下三段代码向您展示了在无限循环中调用的每一种方法:

`//poll method, without arguments 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 method, with arguments 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 method 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 … }`

请记住,键总是有一个状态,可以是就绪、有信号或无效:

  • 就绪:刚创建的时候,一个按键处于就绪状态,这意味着它已经准备好接受事件了。
  • 有信号:当一个键处于有信号状态时,意味着至少有一个事件已经发生,并且这个键已经被排队,所以它可以被poll()take()方法检索。(这类似于钓鱼:关键是浮子,事件是鱼。当你有鱼上钩时,浮标(钥匙)会向你发出信号,让你把鱼线拉出水面。)一旦发出信号,键就保持在这个状态,直到调用它的reset()方法将键返回到就绪状态。如果其他事件在该键被发出信号时发生,它们会被排队,而不会对该键本身重新排队(钓鱼时不会发生这种情况)。
  • 无效:当一个按键处于无效状态时,意味着它不再有效。一个键保持有效,直到通过显式调用cancel()方法取消它,目录变得不可访问,或者 watch 服务被关闭。您可以通过调用WatchKey.isValid()方法来测试一个键是否有效,该方法将返回一个相应的布尔值。

Image 注意监视键对于多个并发线程来说是安全的。

检索关键字的未决事件

当键被发出信号时,我们有一个或多个未决事件等待我们采取行动。我们可以通过调用WatchKey.pollEvents()方法来检索和移除特定监视键的所有未决事件。它不获取任何参数,并返回一个包含检索到的未决事件的List。我们可以迭代这个List来单独提取和处理每个未决事件。List类型为WatchEvent<T>,代表用WatchService注册的对象的事件(或重复事件):

public List<WatchEvent<?>> pollEvents()

Image 注意如果没有未决事件,pollEvents()方法不会等待,这有时可能会导致空的List

下面的代码片段迭代我们的键的未决事件:

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()) {
…
      }
      …
}
…

Image 注意观察事件是不可变的和线程安全的。

检索事件类型和计数

WatchEvent<T>接口映射事件属性,如类型计数。事件的类型可以通过调用WatchEvent.kind()方法获得,该方法将事件类型作为Kind<T>对象返回。

Image 注意如果忽略注册的事件类型,有可能会收到一个OVERFLOW事件。这种事件可以被忽略,也可以被处理,选择哪个取决于你。

下面的代码片段将列出由pollEvents()方法提供的每个事件的类型:

//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);
}
…

除了事件类型,我们还可以得到事件被观察到的次数(重复事件)。如果我们调用返回一个intWatchEvent.count()方法,这是可能的:

System.out.println(watchEvent.count());
检索与事件相关的文件名

当文件发生删除、创建或修改事件时,我们可以通过获取事件上下文来找出它的名称(文件名作为事件的上下文存储)。这个任务可以通过调用WatchEvent.context()方法来完成:

final WatchEvent<Path> watchEventPath = (WatchEvent<Path>) watchEvent;
final Path filename = watchEventPath.context();

System.out.println(filename);
…
将钥匙放回就绪状态

一旦发出信号,键就保持在这个状态,直到调用它的reset()方法将键返回到就绪状态。然后,它继续等待事件。如果监视键有效并已被重置,则reset()方法返回true,如果监视键因不再有效而无法重置,则返回false。在某些情况下,如果密钥不再有效,无限循环应该被打破;例如,如果我们有一个单键,就没有理由停留在无限循环中。

下面是在密钥不再有效时用于中断循环的代码:

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;
  }
}
…

Image 注意如果您忘记或未能调用reset()方法,该键将不会收到任何进一步的事件!

关闭监视服务

当线程退出或服务关闭时,监视服务也将退出。应该通过显式调用WatchService.close()方法或通过将创建代码放入 try-with-resources 块来关闭它,如下所示:

try (WatchService watchService = FileSystems.getDefault().newWatchService()) {
…
}

当 watch 服务关闭时,任何当前操作都将被取消并失效。在一个观察服务被关闭后,任何对它调用操作的进一步尝试都将抛出ClosedWatchServiceException。如果这个观察服务已经关闭,那么调用这个方法没有任何作用。

把它们粘在一起

在这一节中,我们将前面的所有代码块(包括导入和意大利面条式代码)粘合到一个应用中,该应用监视路径C:\rafaelnadal的创建、删除和修改事件,并报告事件的类型和发生事件的文件。出于测试目的,请尝试手动添加、删除或修改该路径下的文件或目录。请记住,只有一个级别被监控(只有C:\rafaelnadal目录),没有目录下的整个目录树。

应用代码如下:

`package watch_01;

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 watchEventPath = (WatchEvent) 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);         }

    } }`

由于该应用包含一个无限循环,请小心手动停止该应用,或者实现一个停止机制。该应用是作为 NetBeans 项目提供的,因此您可以从“输出”窗口轻松地停止它,不需要任何补充代码。

使用监视服务的其他例子

在本节中,我们将“玩”前面的应用,编写一些场景来探索监视服务的可能性。我们将在此基础上构建新的应用,以完成涉及监视服务的更复杂的任务。与上一节一样,在每个步骤的描述之后,提供了支持该步骤的代码块。在完整描述了这些步骤之后,我们将把所有的东西整合到一个完整的应用中。

Image 注意为了保持代码尽可能的干净,我们将跳过变量的声明(它们的名称与前面的应用中的相同)和应该重复的代码。

看目录树

首先,我们将开发一个应用,扩展前面的例子来观察整个C:\rafaelnadal目录树。此外,如果一个CREATE事件在这棵树的某个地方创建了一个新目录,它将立即被注册,就好像它从一开始就在那里一样。

首先,创建一个观察服务:

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

接下来,我们需要为创建、删除和修改事件注册目录树。这比在原始应用中更棘手,因为我们需要注册C:\rafaelnadal的每个子目录,而不仅仅是这个目录。因此,我们需要一次遍历(参见第五章)来遍历每个子目录,并在监视服务中单独注册。这种情况非常适合通过扩展SimpleFileVisitor类来实现遍历,因为我们只需要在目录被预先访问时参与进来(此外,您可能希望覆盖visitFileFailed()方法来显式处理意外的遍历错误)。为此,我们将创建一个名为registerTree()的方法,如下所示:

private void registerTree(Path start) throws IOException {

  Files.walkFileTree(start, new SimpleFileVisitor<Path>() {

     @Override
     public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
                                                                          throws IOException {
        System.out.println("Registering:" + dir);
        registerPath(dir);
        return FileVisitResult.CONTINUE;
        }
   });
}

如您所见,这里没有注册。对于每个遍历的目录,该代码调用另一个名为registerPath()的方法,该方法将接收到的路径注册到 watch 服务,如下所示:

private void registerPath(Path path) throws IOException {

  //register the received path
  WatchKey key = path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
                  StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE);
    }

此时,初始的C:\rafaelnadal目录和所有子目录被注册用于创建、删除和修改事件。

接下来,我们将关注将“捕获”这些事件的无限循环。当一个事件发生时,我们特别感兴趣的是它是否是一个CREATE事件,因为它可能表示已经创建了一个新的子目录,在这种情况下,我们有责任通过调用具有相应路径的registerTree()方法将这个子目录添加到观察服务进程中。这里我们需要解决的问题是,我们不知道哪个键已经排队,所以我们不知道应该通过哪个路径进行注册。解决方案可能是将键和相应的路径保存在一个HashMap中,该路径在每次注册时在registerPath()方法中更新,如下所示,在此之后,当事件发生时,我们可以从哈希映射中提取相关的键:

private final Map<WatchKey, Path> directories = new HashMap<>();
…
private void registerPath(Path path) throws IOException {
  //register the received path
  WatchKey key = path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,  
                  StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE);

  //store the key and path
  directories.put(key, path);
}

现在,在无限循环中,我们可以注册任何新的子目录,如下所示:

while (true) {
  …
  if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
      final Path directory_path = directories.get(key);
      final Path child = directory_path.resolve(filename);

      if (Files.isDirectory(child, LinkOption.NOFOLLOW_LINKS)) {
        registerTree(child);
      }
  }
…
}
…

HashMap也可用于在没有有效密钥时停止无限循环。为了实现这一点,当一个键无效时,它被从HashMap中移除,当HashMap为空时,循环被中断:

while (true) {
  …
  //reset the key
  boolean valid = key.reset();

  //remove the key if it is not valid
  if (!valid) {
      directories.remove(key);

      if (directories.isEmpty()) {
         break;
      }
  }
}
…

就这样!现在,让我们把所有的东西放在一个镜头里:

`import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; 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; import java.nio.file.attribute.BasicFileAttributes; import java.util.HashMap; import java.util.Map;

class WatchRecursiveRafaelNadal {

    private WatchService watchService;     private final Map<WatchKey, Path> directories = new HashMap<>();

    private void registerPath(Path path) throws IOException {         //register the received path         WatchKey key = path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,                   StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE);

        //store the key and path         directories.put(key, path);     } private void registerTree(Path start) throws IOException {

        Files.walkFileTree(start, new SimpleFileVisitor() {

            @Override             public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)                     throws IOException {                 System.out.println("Registering:" + dir);                 registerPath(dir);                 return FileVisitResult.CONTINUE;             }         });

    }

    public void watchRNDir(Path start) throws IOException, InterruptedException {

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

        registerTree(start);

        //start an infinite loop         while (true) {

            //retrieve and remove the next watch key             final WatchKey key = watchService.take();

            //get list of events for the watch key             for (WatchEvent<?> watchEvent : key.pollEvents()) {

                //get the kind of event (create, modify, delete)                 final Kind<?> kind = watchEvent.kind();

                //get the filename for the event                 final WatchEvent watchEventPath = (WatchEvent) watchEvent;                 final Path filename = watchEventPath.context();

                //handle OVERFLOW event                 if (kind == StandardWatchEventKinds.OVERFLOW) {                     continue;                 }

                //handle CREATE event                 if (kind == StandardWatchEventKinds.ENTRY_CREATE) {                     final Path directory_path = directories.get(key);                     final Path child = directory_path.resolve(filename);

                    if (Files.isDirectory(child, LinkOption.NOFOLLOW_LINKS)) {                         registerTree(child);                     }                 } //print it out                 System.out.println(kind + " -> " + filename);             }

            //reset the key             boolean valid = key.reset();

            //remove the key if it is not valid             if (!valid) {                 directories.remove(key);

                //there are no more keys registered                 if (directories.isEmpty()) {                     break;                 }             }         }         watchService.close();     } }

public class Main {

    public static void main(String[] args) {

        final Path path = Paths.get("C:/rafaelnadal");         WatchRecursiveRafaelNadal watch = new WatchRecursiveRafaelNadal();

        try {             watch.watchRNDir(path);         } catch (IOException | InterruptedException ex) {             System.err.println(ex);         }

    } }`

出于测试目的,尝试创建新的子目录和文件,修改它们,然后删除它们。同时,注意控制台输出,看看事件是如何报告的。以下是将名为rafa_champ.jpg的新图片添加到C:\rafaelnadal\photos目录并在几秒钟后将其删除的示例输出:


Registering:C:\rafaelnadal

Registering:C:\rafaelnadal\equipment

Registering:C:\rafaelnadal\grandslam

Registering:C:\rafaelnadal\grandslam\AustralianOpen


Registering:C:\rafaelnadal\grandslam\RolandGarros

Registering:C:\rafaelnadal\grandslam\USOpen

…

Registering:C:\rafaelnadal\wiki

ENTRY_CREATE -> rafa_champ.jpg

ENTRY_MODIFY -> rafa_champ.jpg

ENTRY_MODIFY -> photos

ENTRY_MODIFY -> rafa_champ.jpg

ENTRY_DELETE -> rafa_champ.jpg

ENTRY_MODIFY -> photos

看摄像机

对于这个场景,假设我们有一个监控摄像机,它每 10 秒钟至少捕捉一个图像,并以 JPG 格式将其发送到计算机目录。在幕后,控制器负责检查相机是否按时以正确的 JPG 格式发送图像捕捉。如果摄像机工作不正常,它会显示一条警告消息。

由于 Watch Service API,这个场景可以很容易地在代码行中重现。我们对编写监视摄像机的控制器特别感兴趣。由于摄像机将捕获的图像发送到一个目录,我们的控制器可以观察这个目录中的CREATE事件。本例中的目录是C:\security(您应该手动创建它),它被path变量映射为一个Path:

final Path path = Paths.get("C:/security");
…
WatchService watchService = FileSystems.getDefault().newWatchService();
path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE);
…

接下来,我们知道摄像机每 10 秒发送一次图像,这意味着poll(long, TimeUnit)方法应该非常适合监控这一点(请记住,如果在指定的时间段内发生了事件,此方法将退出,返回相关的WatchKey)。我们将其设置为正好等待 11 秒,如果在这段时间内没有创建新的捕获,我们将通过消息报告这一情况并停止系统:

while (true) {      final WatchKey key = watchService.poll(11, TimeUnit.SECONDS);    if (key == null) {     System.out.println("The video camera is jammed - security watch system is canceled!");     break;    } else {    …    } } …

最后,如果我们有一个新的捕获可用,那么我们需要做的就是检查它是否是 JPG 图像格式。为此,我们可以使用来自Files类的助手方法,名为probeContentType(),它探测文件的内容类型。我们传递文件,它以 MIME 的形式返回null或内容类型。对于 JPG 图像,该方法应该返回image/jpeg

…
OUTERMOST:
while (true) {
  …
  if (kind == StandardWatchEventKinds.ENTRY_CREATE) {

    //get the filename for the event
    final WatchEvent<Path> watchEventPath = (WatchEvent<Path>) watchEvent;
    final Path filename = watchEventPath.context();
    final Path child = path.resolve(filename);

    if (Files.probeContentType(child).equals("image/jpeg")) {

       //print out the video capture time
       SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MMM-dd HH:mm:ss");
       System.out.println("Video capture successfully at: " + dateFormat.format(new Date()));
    } else {
       System.out.println("The video camera capture format failed! This could be a virus!");
       break OUTERMOST;
    }
  }
}
…

我们已经完成了编写控制器的主要任务,所以现在我们需要做的就是填充缺失的代码(导入、声明、主函数等)。)向我们提供完整的申请,如下所示:

import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.Files; 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; `import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.TimeUnit;

class SecurityWatch {

    WatchService watchService;

    private void register(Path path, Kind kind) throws IOException {         //register the directory with the watchService for Kind event             path.register(watchService, kind);     }

    public void watchVideoCamera(Path path) throws IOException, InterruptedException {

        watchService = FileSystems.getDefault().newWatchService();         register(path, StandardWatchEventKinds.ENTRY_CREATE);

        //start an infinite loop         OUTERMOST:         while (true) {

            //retrieve and remove the next watch key             final WatchKey key = watchService.poll(11, TimeUnit.SECONDS);

            if (key == null) {                 System.out.println("The video camera is jammed - security watch system is                                                                                   canceled!");                 break;             } else {

                //get list of 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;                     }

                    if (kind == StandardWatchEventKinds.ENTRY_CREATE) {

                        //get the filename for the event                         final WatchEvent watchEventPath = (WatchEvent) watchEvent;                         final Path filename = watchEventPath.context();                         final Path child = path.resolve(filename);

                        if (Files.probeContentType(child).equals("image/jpeg")) {

                            //print out the video capture time                             SimpleDateFormat dateFormat = new                                              SimpleDateFormat("yyyy-MMM-dd HH:mm:ss");                             System.out.println("Video capture successfully at: " +                                              dateFormat.format(new Date()));                         } else {                             System.out.println("The video camera capture format failed!                                                                      This could be a virus!");                             break OUTERMOST;                         }                     }                 }

                //reset the key                 boolean valid = key.reset();

                //exit loop if the key is not valid                 if (!valid) {                     break;                 }             }         }

        watchService.close();     } }

public class Main {

    public static void main(String[] args) {

        final Path path = Paths.get("C:/security");         SecurityWatch watch = new SecurityWatch();

        try {             watch.watchVideoCamera(path);         } catch (IOException | InterruptedException ex) {             System.err.println(ex);         }

    } }`

出于测试的目的,您可能需要编写一个测试器类,或者更简单地扮演摄像机的角色。只需启动应用,在关键时刻到来之前复制并粘贴C:\security中的 JPG 图像。尝试不同的情况,例如使用错误的文件格式,在复制另一个图像之前等待超过 11 秒,等等。

观察打印机托盘系统

在本节中,我们将开发一个应用来监控大规模的打印机托盘。假设我们有一个多线程基类,它接收要打印的文档,并根据一种旨在优化打印机使用的算法将它们分派给一套网络打印机——打印线程在相应的文档打印完毕后终止。该类的实现方式如下:

import java.nio.file.Path;
import java.util.Random;

class Print implements Runnable {

    private Path doc;

    Print(Path doc) {
        this.doc = doc;
    }

    @Override
    public void run() {
        try {
            //sleep a random number of seconds for simulating dispatching and printing            
            Thread.sleep(20000 + new Random().nextInt(30000));
            System.out.println("Printing: " + doc);
        } catch (InterruptedException ex) {
            System.err.println(ex);
        }
    }
}

Image 注意 Java 7 推荐使用新的ThreadLocalRandom类在多线程情况下生成随机数。但是我更喜欢老的Random类,因为新类好像有 bug 它在多个线程上生成相同的数字。如果在你读这本书的时候这个错误已经被解决了,那么你可能想用下面这句话来代替:ThreadLocalRandom.current().nextInt(20000, 50000);

现在,打印机从一个由目录(C:\printertray,您需要手动创建)表示的公共托盘中“进给”。我们的工作是实现一个监视服务来管理这个托盘。当一个新文档到达托盘时,我们必须将它传递给Print类,在一个文档被打印后,我们必须将它从托盘中删除。

我们首先通过传统方法获得一个监视服务,并为CREATEDELETE事件注册C:\printertray目录:

final Path path = Paths.get("C:/printertray");
…
WatchService watchService = FileSystems.getDefault().newWatchService();
path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
                                                        StandardWatchEventKinds.ENTRY_DELETE);
…

接下来,当一个新文档到达托盘时,我们必须创建一个新的Print线程,并存储该线程和文档路径,以便进一步跟踪线程状态。这将有助于我们了解文档何时已经打印,因此应该从托盘中删除并移除以进行存储(我们使用HashMap来完成此任务)。下面的代码片段包含当一个新文档到达托盘时执行的代码块(一个CREATE事件被排队):

private final Map<Thread, Path> threads = new HashMap<>();
…
if (kind == StandardWatchEventKinds.ENTRY_CREATE) {

     System.out.println("Sending the document to print -> " + filename);

     Runnable task = new Print(path.resolve(filename));
     Thread worker = new Thread(task);

     //we can set the name of the thread
     worker.setName(path.resolve(filename).toString());

     //store the thread and the path
     threads.put(worker, path.resolve(filename));

     //start the thread, never call method run() direct
     worker.start();
}
…

从托盘中删除一个文档后(一个DELETE事件被排队),我们只打印一条消息:

if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
    System.out.println("Document " + filename + " was successfully printed!");
}
…

但是文档是什么时候删除的呢?为了解决这个任务,我们使用了一个小技巧。我们不使用take()方法来等待键排队,而是使用poll(long, TimeUnit)方法,这将在指定的时间间隔内给我们无限循环中的控制权——当我们拥有控制权时(不管是否有键排队),我们可以循环线程的HashMap,以查看是否有任何打印作业已经终止(相关的线程状态是TERMINATED)。每一个TERMINATED状态之后都将删除相关联的路径并移除HashMap条目。当路径被删除时,一个DELETE事件将被排队。以下代码向您展示了如何实现这一点:

if (!threads.isEmpty()) {
    for (Iterator<Map.Entry<Thread, Path>> it = threads.entrySet().iterator(); it.hasNext();)
         Map.Entry<Thread, Path> entry = it.next();                        
         if (entry.getKey().getState() == Thread.State.TERMINATED) {
             Files.deleteIfExists(entry.getValue());
             it.remove();
          }
    }
}
…

现在,将所有东西放在一起,获得完整的应用:

`import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.Files; 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; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Random; import java.util.concurrent.TimeUnit;

class Print implements Runnable {

    private Path doc;

    Print(Path doc) {         this.doc = doc;     }

    @Override     public void run() {         try {             //sleep a random number of seconds for simulating dispatching and printing                         Thread.sleep(20000 + new Random().nextInt(30000));             System.out.println("Printing: " + doc);         } catch (InterruptedException ex) {             System.err.println(ex);         }     } }

class WatchPrinterTray {

    private final Map<Thread, Path> threads = new HashMap<>();

    public void watchTray(Path path) throws IOException, InterruptedException {         try (WatchService watchService = FileSystems.getDefault().newWatchService()) {             path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,                                                         StandardWatchEventKinds.ENTRY_DELETE);

            //start an infinite loop             while (true) {

                //retrieve and remove the next watch key                 final WatchKey key = watchService.poll(10, TimeUnit.SECONDS); //get list of events for the watch key                 if (key != null) {                     for (WatchEvent<?> watchEvent : key.pollEvents()) {

                        //get the filename for the event                         final WatchEvent watchEventPath = (WatchEvent) watchEvent;                         final Path filename = watchEventPath.context();

                        //get the kind of event (create, modify, delete)                         final Kind<?> kind = watchEvent.kind();

                        //handle OVERFLOW event                         if (kind == StandardWatchEventKinds.OVERFLOW) {                             continue;                         }

                        if (kind == StandardWatchEventKinds.ENTRY_CREATE) {                             System.out.println("Sending the document to print ->" + filename);

                            Runnable task = new Print(path.resolve(filename));                             Thread worker = new Thread(task);

                            //we can set the name of the thread                             worker.setName(path.resolve(filename).toString());

                            //store the thread and the path                             threads.put(worker, path.resolve(filename));

                            //start the thread, never call method run() direct                             worker.start();                         }

                        if (kind == StandardWatchEventKinds.ENTRY_DELETE) {                             System.out.println(filename + " was successfully printed!");                         }                     }

                    //reset the key                     boolean valid = key.reset();

                    //exit loop if the key is not valid                     if (!valid) {                         threads.clear();                         break;                     }                 }

                if (!threads.isEmpty()) {                     for (Iterator<Map.Entry<Thread, Path>> it = threads.entrySet().iterator();                                                                                 it.hasNext();) {                         Map.Entry<Thread, Path> entry = it.next();                         if (entry.getKey().getState() == Thread.State.TERMINATED) {                             Files.deleteIfExists(entry.getValue());                             it.remove();                         }                     }                 }             }         }     } }

public class Main {

    public static void main(String[] args) {

        final Path path = Paths.get("C:/printertray");         WatchPrinterTray watch = new WatchPrinterTray();

        try {             watch.watchTray(path);         } catch (IOException | InterruptedException ex) {             System.err.println(ex);         }

    } }`

出于测试目的,启动应用并将一组文件复制到C:\printertray目录中。例如,以下是使用一组文件进行测试的输出:


Sending the document to print -> rafa_1.jpg

Sending the document to print -> AEGON.txt

Sending the document to print -> BNP.txt

Printing: C:\printertray\rafa_1.jpg

Printing: C:\printertray\AEGON.txt

rafa_1.jpg was successfully printed!

AEGON.txt was successfully printed!

Printing: C:\printertray\BNP.txt

Sending the document to print -> rafa_winner.jpg

BNP.txt was successfully printed!


Printing: C:\printertray\rafa_winner.jpg

rafa_winner.jpg was successfully printed

总结

在这一章中,你已经探索了 NIO.2 的一个很好的工具——监视服务 API。您学习了如何监视目录或目录树中的事件,如创建、删除和修改。在概述了这个 API 和一个介绍性的应用之后,您看到了如何将这个 API 与 NIO.2 walks 结合起来,如何模拟摄像机监控,以及如何观察大规模的打印机托盘。这些例子只是为了激发您的好奇心,进一步探索这个 API 的精彩世界。由于它非常通用,因此可以应用于许多其他场景。例如,您可以使用它来更新 GUI 显示中的文件列表,或者检测可以重新加载的配置文件的修改。