首页 » Java程序员修炼之道 » Java程序员修炼之道全文在线阅读

《Java程序员修炼之道》2.4 NIO.2的文件系统I/O

关灯直达底部

对于文件系统的操作任务,比如移动文件、修改文件属性,以及处理文件内容等,在NIO.2中都有所改善。对这些操作的支持主要是由Files类提供的。

表2-2中有关于Files类的详细介绍,此外本节还会介绍另外一个也很重要的类:WatchService

表2-2 文件处理的基础类

类 说明Files 让你轻松复制、移动、删除或处理文件的工具类,有你需要的所有方法WatchService用来监视文件或目录的核心类,不管它们有没有变化

在本节中,你将学会如何在文件和文件系统上执行下面这些任务:

  • 创建和删除文件;
  • 移动、复制、重命名和删除文件;
  • 文件属性的读写;
  • 文件内容的读取和写入;
  • 处理符号链接;
  • WatchService发出文件修改通知;
  • 使用SeekableByteChannel——一个可以指定位置及大小的增强型字节通道。

这看起来可能挺恐怖的,但由于设计巧妙,API提供了很多辅助方法,把抽象层隐藏了起来,让你可以轻松快捷地处理文件系统。

警告 NIO.2 API对原子操作的支持有很大改进,但涉及文件系统处理时,仍然主要依靠代码来提供保护。即使是执行了一半的操作,也很可能会因为突然断网、咖啡泼到服务器上,或某个冒失鬼在错误的UNIX机器上执行了shutdown now命令(本书作者之一亲身经历的著名事件)等诸多原因而出错。尽管API的某些方法还是会偶尔抛出个RuntimeException,但某些异常状况可以由Files.exists(Path)这样的辅助方法来缓解。

学习新API最好的办法就是读写代码。接下来我们来看一些实际案例,先从基本的文件创建和删除开始。

2.4.1 创建和删除文件

只需要调用Files类里的辅助方法,就可以很容易地创建和删除文件。当然,你接到的任务不可能总像默认情况那么简单,所以我们额外加了一些选项,比如在新创建的文件上设定可读/可写/可执行的安全访问权限。

提示 如果你要在自己的机器上运行本节中的代码,请用实际路径替换掉代码中的路径。

下面的代码展示了基本的文件创建操作,用到了Files.createFile(Path target)方法。如果你的操作系统里有个D:/Backup目录,运行代码之后就会在那里创建一个MyStuff.txt文件。

Path target = Paths.get("D://Backup//MyStuff.txt");Path file = Files.createFile(target);  

通常出于安全考虑,要定义所创建的文件是用于读、写、执行,或三者权限的某种组合时,你要指明该文件的某些FileAttributes。因为这取决于文件系统,所以需要使用与文件系统相关的文件权限类。

下面是一个在POSIX文件系统1上为属主、属主组内用户和所有用户设置读/写许可的例子。这种方法允许所有用户对即将创建的文件D:/Backup/MyStuff.txt进行读写操作。

1 可移植操作系统接口(UNIX),是一种许多操作系统都支持的基本标准。

Path target = Paths.get("D://Backup//MyStuff.txt");Set<PosixFilePermission> perms =    PosixFilePermissions.fromString("rw-rw-rw-");FileAttribute<Set<PosixFilePermission>> attr =    PosixFilePermissions.asFileAttribute(perms);Files.createFile(target, attr);  

java.nio.file.attribute包里有一大串已经写好的*FilePermission类。对文件属性的支持在2.4.3节中还有更详细的论述。

警告 如果在创建文件时要指定访问许可,不要忽略其父目录强加给该文件的umask限制或受限许可。比如说,你会发现即便你为新文件指定了rw-rw-rw许可,但由于目录的掩码,实际上文件最终的访问许可却是rw-r--r--

删除文件要简单一些,可以用Files.delete(Path)方法。下面的代码删除了刚刚创建的D:/Backup/MyStuff.txt文件。当然,运行这个Java程序的用户需要有删除文件的权限。

Path target = Paths.get("D://Backup//MyStuff.txt");Files.delete(target);  

接下来你将学到如何在文件系统中复制和移动文件。

2.4.2 文件的复制和移动

使用Files类中简单的辅助方法可以很轻松地完成文件的复制和移动。

下面的代码演示了如何用Files.copy(Path source, Path target)方法完成基本的复制操作。

Path source = Paths.get("C://My Documents//Stuff.txt");Path target = Paths.get("D://Backup//MyStuff.txt");Files.copy(source, target);  

复制文件时通常需要设置某些选项。下面这个例子用到了覆盖即替换已有文件的选项。

import static java.nio.file.StandardCopyOption.*;Path source = Paths.get("C://My Documents//Stuff.txt");Path target = Paths.get("D://Backup//MyStuff.txt");Files.copy(source, target, REPLACE_EXISTING);  

其他的复制选项包括COPY_ATTRIBUTES(复制文件属性)和ATOMIC_MOVE(确保在两边的操作都成功,否则回滚)。

移动和复制很像,都是用原子Files.move(Path source, Path target)方法完成的。通常在移动文件时,你想要用复制选项,此时便可以用Files.move(Path source, Path target, CopyOptions...)方法,但要注意变参的使用。

在下面这个例子中,我们要在移动源文件时保留其属性,并且覆盖目标文件(如果存在的话)。

import static java.nio.file.StandardCopyOption.*;Path source = Paths.get("C://My Documents//Stuff.txt");Path target = Paths.get("D://Backup//MyStuff.txt");Files.move(source, target, REPLACE_EXISTING, COPY_ATTRIBUTES);  

现在你已经能创建、删除、复制和移动文件了,下面该认真研究一下Java 7对文件属性的支持了。

2.4.3 文件的属性

文件属性控制着谁能对文件做什么。一般情况下,做什么许可包括能否读取、写入或执行文件,而由谁许可包括属主、群组或所有人。

本节从讨论文件的基本属性组开始,比如文件最后访问时间以及它是目录还是符号链接等。本节的第二部分讨论对特定文件系统的文件属性的支持,因为不同的文件系统都有它们自己的属性集和属性含义的解释,所以这部分比较难。

让我们先从了解Java 7 对基本文件属性的支持开始吧。

1. 基本文件属性支持

真正通用的文件属性并不多,但确实有一组大多数文件系统都支持的属性。接口BasicFileAttributes定义了这个通用集,但实际上工具类Files就可以回答与文件相关的各种问题,比如下面这些:

  • 最后修改时间是什么时候?
  • 它有多大?
  • 它是符号连接吗?
  • 它是目录吗?

代码清单2-4说明了Files类中用于收集这些基本文件属性的方法。代码输出了/usr/bin/zip的相关信息,你看到的输出应该和下面的类似:

/usr/bin/zip2011-07-20T16:50:18Z351872falsefalse{lastModifiedTime=2011-07-20T16:50:18Z,fileKey=(dev=e000002,ino=30871217), isDirectory=false,lastAccessTime=2011-06-13T23:31:11Z, isOther=false,isSymbolicLink=false, isRegularFile=true,creationTime=2011-07-20T16:50:18Z, size=351872}  

注意,所有这些属性都是调用Files.readAttributes(Path path, Stringattributes, LinkOption... options)得到的。代码清单2-4如下所示:

代码清单2-4 通用的文件属性

try{  Path zip = Paths.get("/usr/bin/zip");//获取Path  /**输出属性*/  System.out.println(Files.getLastModifiedTime(zip));  System.out.println(Files.size(zip));  System.out.println(Files.isSymbolicLink(zip));  System.out.println(Files.isDirectory(zip));  System.out.println(Files.readAttributes(zip, "*"));//执行批量读取}catch (IOException ex){  System.out.println("Exception [" + ex.getMessage + "]");}  

还有一些可以从Files类的方法中采集到的通用文件属性信息。这样的信息包括文件属主,是否为符号链接等。请参照Files类的Javadoc查看完整的辅助方法列表。

Java 7也支持跨文件系统的文件属性查看和处理功能。

2. 特定文件属性支持

在2.4.1节创建文件时你已经见过FileAttribute接口和PosixFilePermissions类了。为了支持文件系统特定的文件属性,Java 7允许文件系统提供者实现FileAttributeViewBasicFileAttributes接口。

警告 我们之前已经说过了,但有必要再重复一次。在编写特定文件系统的代码时一定要小心。一定要确保你的逻辑和异常处理考虑到了代码在不同文件系统上运行的情况。

来看一个例子,其中你想用Java 7保证正确的访问许可被设置在特定文件中。图2-2显示了Admin用户的home目录的列表。注意那个特殊的.profile隐藏文件,它只允许Admin用户写,其他任何人都没有写权限,但所有人都可以读取该文件。

图2-2 Admin用户的home目录列表,显示.profile的访问许可

在下面的代码中,你要确保.profile文件的访问许可设置正确,与图2-2对应。Admin用户希望其他所有用户都可以读取该文件,但只有他自己来写。你可以用特定的POSIX PosixFilePermissionPosixFileAttributes类来保证访问许可(rw-r--r--)是正确的。

代码清单2-5 Java 7对文件属性的支持

import static java.nio.file.attribute.PosixFilePermission.*;try{  Path profile = Paths.get("/user/Admin/.profile");  PosixFileAttributes attrs =      Files.readAttributes(profile,                           PosixFileAttributes.class);//①获取属性视图  Set<PosixFilePermission> posixPermissions =                               attrs.permissions;//②读取访问许可  posixPermissions.clear; //③清除访问许可  /**日志信息*/  String owner = attrs.owner.getName;  String perms =      PosixFilePermissions.toString(posixPermissions);  System.out.format("%s %s%n", owner, perms);   /**④设置新的访问许可*/  posixPermissions.add(OWNER_READ);  posixPermissions.add(GROUP_READ);  posixPermissions.add(OTHER_READ);  posixPermissions.add(OWNER_WRITE);  Files.setPosixFilePermissions(profile, posixPermissions);}catch(IOException e){  System.out.println(e.getMessage);}  

代码从导入PosixFilePermission常量还有其他未显示的导入开始,然后得到.profile文件的PathFiles类中有个辅助方法让你可以读取特定文件系统的属性,在这个例子中是PosixFileAttributes①。然后你就可以访问PosixFilePermission②。在清除了已有的许可之后③,你可以为文件添加新的访问许可,当然还是用Files中的方法④。

你可能已经注意到了,PosixFilePermission是一个enum,因此没有实现FileAttributeView接口。为什么这里没用PosixFileAttributeView呢?实际上是Files辅助类把它隐藏了起来,这样你就可以用readAttributes方法直接读取文件属性了,也可以用setPosixFilePermissions方法直接设置访问许可。

除了基本属性,Java 7还有一个用来支持特别操作系统特性的扩展系统。可惜,我们不可能囊括所有特殊情况,但我们会给你看一个扩展系统的例子:Java 7对符号链接的支持。

3. 符号链接

你可以把符号链接看做指向另一个文件或目录的入口,并且在大多数情况下它们都是透明的。比如切换到符号链接的目录下会把你带到符号链接所指向的目录下。但在写软件时,比如备份工具或部署脚本,你需要慎重考虑是否应该跟随符号链接,NIO.2允许你做出选择。

我们再用一下2.2.3节的例子。你要在*nix系统上查询/usr/logs目录下的日志文件log1.txt的信息。但/usr/logs目录实际上是一个指向/application/logs目录的符号链接(指针),/application/logs目录才是日志文件的真正位置。

符号链接在宿主操作系统中使用,包括(但不限于)UNIX、Linux、Windows 7和Mac OS X。Java 7对符号链接的支持遵循UNIX操作系统中实现的语义。

下面的代码在读取基本文件属性之前先检查指向安装Java的 /opt/platform目录的Path,看它是否为符号链接,我们想读取文件真正位置的属性。代码清单2-6如下所示:

代码清单2-6 探索符号链接

Path file = Paths.get("/opt/platform/java");try{  if(Files.isSymbolicLink(file)) //① 检查符号链接  {    file = Files.readSymbolicLink(file); //②读取符号链接  }  Files.readAttributes(file, BasicFileAttributes.class);//③ 读取文件属性}catch (IOException e){  System.out.println(e.getMessage);}  

Files类提供了一个isSymbolicLink(Path)方法来检查符号链接①。它还有一个辅助方法,可以用于返回符号链接目标的真实Path②,所以你能读到正确的文件属性③。

NIO.2 API默认会跟随符号链接。如果不想跟随,需要用LinkOption.NOFOLLOW_LINKS选项。这一选项可以用在几个方法调用上。如果你要读取符号链接本身的基本文件属性,应该调用:

Files.readAttributes(target,                     BasicFileAttributes.class,                     LinkOption.NOFOLLOW_LINKS);  

符号链接是Java 7对特定文件系统支持最常用的例子,API设计者也考虑到了未来对特定文件系统支持特性的扩展,比如量子加密文件系统。

你已经做过文件处理了,现在可以开始研究对文件内容的处理了。

2.4.4 快速读写数据

Java 7可以尽可能多地提供用来读取和写入文件内容的辅助方法。当然,这些新方法使用Path,但它们也可以与那些java.io包里基于流的类进行互操作。因此,你用一个方法就可以读取文件中的所有行或全部字节。

本节会向你介绍打开文件(带选项)的过程,以及一小组常用的文件读/写例子。让我们先从打开文件的不同方式开始。

1. 打开文件

Java 7可以直接用带缓冲区的读取器和写入器或输入输出流(为了和以前的Java I/O代码兼容)打开文件。下面的代码演示了Java 7如何用Files.newBufferedReader方法打开文件并按行读取其中的内容。

Path logFile = Paths.get("/tmp/app.log");try (BufferedReader reader = Files.newBufferedReader(logFile, StandardCharsets.UTF_8)) {  String line;  while ((line = reader.readLine) != null) {     ...  }}  

打开一个用于写入的文件也很简单。

Path logFile = Paths.get("/tmp/app.log");try (BufferedWriter writer =     Files.newBufferedWrite(logFile, StandardCharsets.UTF_8, StandardOpenOption.WRITE)) {   writer.write("Hello World!");    ..}  

注意StandardOpenOption.WRITE选项的使用,这是可以添加的几个OpenOption变参之一。它可以确保写入的文件有正确的访问许可。其他常用的文件打开选项还有READAPPEND

InputStreamOutputStream的交互是通过Files.newInputStream(Path,OpenOption...)Files.newOutputStream(Path,OpenOption...)实现的。它们为过去基于java.io包的I/O和新的基于java.nio包的文件I/O之间架起了一座桥梁。

提示 在处理String时,不要忘了查看它的字符编码。忘记设置字符编码(通过StandardCharsets类,比如new String(byte,StandardCharsets.UTF_8)) 可能导致不可预料的字符编码问题。

前面的代码片段还是用Java 6及之前版本编写的读取和写入文件代码,仍然属于比较繁琐的底层代码。而Java 7具备更高层的抽象能力,可以帮你避免很多不必要的繁琐编码工作。

2.简化读取和写入

辅助类Files有两个辅助方法,用于读取文件中的全部行和全部字节。也就是说你没必要再用while循环把数据从字节数组读到缓冲区里去。下面的代码演示了如何调用辅助方法。

Path logFile = Paths.get("/tmp/app.log");List<String> lines = Files.readAllLines(logFile, StandardCharsets.UTF_8);byte bytes = Files.readAllBytes(logFile);  

对于某些软件来说,什么时候读、写是个问题,特别是在处理属性文件或日志时。这时就该文件修改通知系统大显身手了。

2.4.5 文件修改通知

在Java 7中可以用java.nio.file.WatchService类监测文件或目录的变化。该类用客户线程监视注册文件或目录的变化,并且在检测到变化时返回一个事件。这种事件通知对于安全监测、属性文件中的数据刷新等很多用例都很有用。是现在某些应用程序中常用的轮询机制(相对而言性能较差)的理想替代品。

下面的代码用WatchService监测用户karianna主目录的变化,每当发现变化时就会在控制台中输出一个事件通知。和很多持续轮询的设计一样,它也需要一个轻量的退出机制。代码清单2-7如下所示:

代码清单2-7 使用WatchService

import static java.nio.file.StandardWatchEventKinds.*;try{   WatchService watcher =       FileSystems.getDefault.newWatchService;   Path dir =       FileSystems.getDefault.getPath("/usr/karianna");   WatchKey key = dir.register(watcher, ENTRY_MODIFY); //①监测变化   while(!shutdown) //②检查shutdown标志   {     /**③得到下一个 key及其事件*/     key = watcher.take;      for (WatchEvent<?> event: key.pollEvents)      {         if (event.kind == ENTRY_MODIFY) //④检查是否为变化事件         {            System.out.println("Home dir changed!");          }     }     key.reset; //⑤重置监测key   }}catch (IOException | InterruptedException e){  System.out.println(e.getMessage);}  

在得到默认的WatchService后,将karianna的主目录登记到变化监测名单中①。然后在一个无限循环(直到shutdown标志改变)②中执行WatcherService take方法,直到WatchKey的到来。一旦得到WatchKey,代码就遍历其WatchEvent进行检测③。如果发现了类型为ENTRY_MODIFYWatchEvent④,就诏告天下karianna的主目录发生了变化!最后重置key⑤准备迎接下一个事件,继续等待。

还有其他可以监测的事件,比如ENTRY_CREATEENTRY_DELETEOVERFLOW(可以表明事件已经丢失或被丢弃了)。

接下来,我们要进入一个非常重要的、抽象的新API——用于数据的读写,使异步I/O成为现实的SeekableByteChannel

2.4.6 SeekableByteChannel

Java 7引入SeekableByteChannel接口,是为了让开发人员能够改变字节通道的位置和大小。比如,应用服务器为了分析日志中的某个错误码,可以让多个线程去访问连接在一个大型日志文件上的字节通道。

JDK中有一个java.nio.channels.SeekableByteChannel接口的实现类——java.nio.channels.FileChannel。这个类可以在文件读取或写入时保持当前位置。比如说,你可能想要写一段代码读取日志文件中的最后1000个字符,或者向一个文本文件中的特定位置写入一些价格数据。

下面的代码展示了如何运用FileChannel的寻址能力读取日志文件中的最后1000个字符。

Path logFile = Paths.get("c://temp.log");ByteBuffer buffer = ByteBuffer.allocate(1024);FileChannel channel = FileChannel.open(logFile, StandardOpenOption.READ);channel.read(buffer, channel.size - 1000);  

FileChannel类的寻址能力意味着开发人员可以更加灵活地处理文件内容。我们期待能由此产生一些有趣的开源项目,比如针对大型文件的并行访问。随着对该接口的不断扩展,可能还会有网络数据流的续传。

NIO.2 API中下一个主要修改是异步I/O,它可以使用多个后台线程读写文件、套接字和通道中的数据。