登录后台

页面导航

本文编写于 2298 天前,最后修改于 1258 天前,其中某些信息可能已经过时。

异步通道 API

概览

异步通道 提供支持连接、读取、以及写入之类非锁定操作的连接,并提供对已启动操作的控制机制。Java 7 中用于 Java Platform(NIO.2)的 More New I/O APIs,通过在 java.nio.channels 包中增加四个异步通道,从而增强了 Java 1.4 中的 New I/O APIs(NIO):

  • AsynchronousSocketChannel
  • AsynchronousServerSocketChannel
  • AsynchronousFileChannel
  • AsynchronousDatagramChannel

这些类在风格上与 NIO 通道 API 很相似。他们共享相同的方法与参数结构体,并且大多数对于 NIO 通道类可用的参数,对于新的异步版本仍然可用。主要区别在于新通道可使一些操作异步执行。

异步通道 API 提供两种对已启动异步操作的监测与控制机制。第一种是通过返回一个 java.util.concurrent.Future 对象来实现,它将会建模一个挂起操作,并可用于查询其状态以及获取结果。第二种是通过传递给操作一个新类的对象,java.nio.channels.CompletionHandler,来完成,它会定义在操作完毕后所执行的处理程序方法。每个异步通道类为每个操作定义 API 副本,这样可采用任一机制。

在本文中,关于 NIO.2 的 两部分系列文章 中的第一部分,介绍了每个通道,并提供一些简单的例子来演示它们的使用方法。这些例子都处于可运行状态(见 下载),您可在 Oracle 以及 IBM®(在本文写作期间,都还处于开发阶段;见 参见资料) 所提供的 Java 7 版中运行这些例子。在 第二部分 中,您将有机会了解 NIO.2 文件系统 API。

异步套接字通道及特性

首先,我们将了解 AsynchronousServerSocketChannelAsynchronousSocketChannel 类。我们将看到的第一个例子将演示如何利用这些新的类来实施简单的客户端/服务器。第一步,我们要设置服务器。

设置服务器

打开 AsychronousServerSocketChannel 并将其绑定到类似于 ServerSocketChannel 的地址:

AsynchronousServerSocketChannel server =
    AsynchronousServerSocketChannel.open().bind(null);

方法 bind() 将一个套接字地址作为其参数。找到空闲端口的便利方法是传递一个 null 地址,它会自动将套接字绑定到本地主机地址,并使用空闲的 临时 端口。

接下来,可以告诉通道接受一个连接:

Future<AsynchronousSocketChannel> acceptFuture = server.accept();

这是与 NIO 的第一个不同之处。接受调用总会立刻返回,并且,—— 不同于 ServerSocketChannel.accept(),它会返回一个 SocketChannel —— 它返回一个 Future<AsynchronousSocketChannel> 对象,该对象可在以后用于检索 AsynchronousSocketChannel。 Future 对象的通用类型是实际操作的结果。比如,读取或写入操作会因为操作返回读或写的字节数,而返回一个 Future<Integer>

利用 Future 对象,当前线程可阻塞来等待结果:

AsynchronousSocketChannel worker = future.get();

此处,其阻塞超时时间为 10 秒:

AsynchronousSocketChannel worker = future.get(10, TimeUnit.SECONDS);

或者轮询操作的当前状态,还可取消操作:

if (!future.isDone()) {
    future.cancel(true);
}

cancel() 方法可利用一个布尔标志来指出执行接受的线程是否可被中断。这是个很有用的增强;在以前的 Java 版本中,只能通过关闭套接字来中止此类阻塞 I/O 操作。

客户端设置

接下来,要通过打开并连接与服务器之间的 AsynchronousSocketChannel,来设置客户端:

AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
client.connect(server.getLocalAddress()).get();

一旦客户端与服务器建立连接,可通过使用字节缓存的通道来执行读写操作,如清单 1 所示:

清单 1. 使用读写字节缓存

// send a message to the server
ByteBuffer message = ByteBuffer.wrap("ping".getBytes());
client.write(message).get();

// read a message from the client
worker.read(readBuffer).get(10, TimeUnit.SECONDS);
System.out.println("Message: " + new String(readBuffer.array()));

还支持异步地分散读操作与写操作,该操作需要大量字节缓存。

新异步通道的 API 完全从底层套接字中抽取掉:无法直接获取套接字,而以前可以调用 socket() ,例如,SocketChannel。引入了两个新的方法 —— getOptionsetOption —— 来在异步网络通道中查询并设置套接字选项。例如,可通过 channel.getOption(StandardSocketOption.SO_RCVBUF) 而不是 channel.socket().getReceiveBufferSize(); 来检索接收缓存大小。

完成处理程序

使用 Future 对象的替代机制,是向异步操作注册一个 callback 。接口 CompletionHandler 有两个方法:

  • void completed(V result, A attachment) 在任务完成结果中具有类型 V 时执行。
  • void failed(Throwable e, A attachment) 在任务由于 Throwable e 而失败时执行。

两个方法的附件参数都是一个传递到异步操作的对象。如果相同的对象用于多个操作,其可用于追踪哪个操作已完成。

Open 命令

我们来看一个使用 AsynchronousFileChannel 类的例子。可通过将 java.nio.file.Path 对象传递到静态 open() 方法中,来创建一个新的通道:

AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(Paths.get("myfile"));

Path 是 Java 7 中的新类,可在 第 2 部分 中找到更多细节。可利用 Paths.get(String) 实用方法,从代表文件名的 String 中创建 Path。

默认情况下,该文件已打开以供读取。open() 方法可利用附加选项来指定如何打开该文件。例如,此调用打开文件以供读取或写入,如果必要将创建该文件,并在通道关闭或者 JVM 终止时尝试删除文件:

fileChannel = AsynchronousFileChannel.open(Paths.get("afile"),
    StandardOpenOption.READ, StandardOpenOption.WRITE,
    StandardOpenOption.CREATE, StandardOpenOption.DELETE_ON_CLOSE);

替代方法,open() 提供了对通道的更好的控制,允许设置文件属性。

用于 的新 命令
用于异步通道的 open 命令格式已被移植到 FileChannel 类。在 NIO 中,通过在 FileInputStream、FileOutputStream、或者 RandomAccessFile 上调用 getChannel() 来获取 FileChannel。借助 NIO.2,可利用 open() 方法来直接创建 FileChannel,此处展示了相关的例子。

实现一个完成处理程序

接下来,可将这些写入文件,写入完成后,就可执行一些操作。 首先要构造一个封装了 “ something ” 的 CompletionHandler ,如清单 2 所示:

清单 2. 创建完成处理程序

CompletionHandler<Integer, Object> handler =
    new CompletionHandler<Integer, Object>() {
    @Override
    public void completed(Integer result, Object attachment) {
        System.out.println(attachment + " completed with " + result + " bytes written");
    }
    @Override
    public void failed(Throwable e, Object attachment) {
        System.err.println(attachment + " failed with:");
        e.printStackTrace();
    }
};

现在可以进行写入:

fileChannel.write(ByteBuffer.wrap(bytes), 0, "Write operation 1", handler);

write() 方法采取:

  • 包含要写入内容的 ByteBuffer
  • 文件中的绝对位置
  • 要传递给完成处理程序方法的附件对象
  • 完成处理程序

操作必须给出进行读或写的文件中的绝对位置。文件具有内部位置标记,来指出读/写发生的位置,这样做没有意义,因为在上一个操作完成之前,就可以启动新操作,它们的发生顺序无法得到保证。由于相同的原因,在 AsynchronousFileChannel API 中没有用于设置或查询位置的方法,在 FileChannel 中同样也没有。

除了读写方法之外,还支持异步锁定方法,因此,如果当前有其他线程保持锁定时,可对文件进行执行访问锁定,而不必在当前线程中锁定(或者利用 tryLock 轮询)。

异步通道组

每个异步通道都属于一个通道组,它们共享一个 Java 线程池,该线程池用于完成启动的异步 I/O 操作。这看上去有点像欺骗,因为您可在自己的 Java 线程中执行大多数异步功能,来获得相同的表现,并且,您可能希望能够仅仅利用操作系统的异步 I/O 能力,来执行 NIO.2 ,从而获得更优的性能。然而,在有些情况下,有必要使用 Java 线程:比如,保证 completion-handler 方法在来自线程池的线程上执行。

默认情况下,具有 open() 方法的通道属于一个全局通道组,可利用如下系统变量对其进行配置:

  • java.nio.channels.DefaultThreadPoolthreadFactory,其不采用默认设置,而是定义一个 java.util.concurrent.ThreadFactory
  • java.nio.channels.DefaultThreadPool.initialSize,指定线程池的初始规模

java.nio.channels.AsynchronousChannelGroup 中的三个实用方法提供了创建新通道组的方法:

  • withCachedThreadPool()
  • withFixedThreadPool()
  • withThreadPool()

这些方法或者对线程池进行定义,如 java.util.concurrent.ExecutorService,或者是 java.util.concurrent.ThreadFactory。例如,以下调用创建了具有线程池的新的通道组,该线程池包含 10 个线程,其中每个都构造为来自 Executors 类的线程工厂:

AsynchronousChannelGroup tenThreadGroup =
    AsynchronousChannelGroup.withFixedThreadPool(10, Executors.defaultThreadFactory());

三个异步网络通道都具有 open() 方法的替代版本,它们采用给出的通道组而不是默认通道组。例如,当有异步操作请求时,此调用告诉 channel 使用 tenThreadGroup 而不是默认通道组来获取线程:

AsynchronousServerSocketChannel channel =
    AsynchronousServerSocketChannel.open(tenThreadGroup);

定义自己的通道组可更好地控制服务于操作的线程,并能提供关闭线程或者等待终止的机制。清单 3 展示了相关的例子:

清单 3. 利用通道组来控制线程关闭

// first initiate a call that won't be satisfied
channel.accept(null, completionHandler);
// once the operation has been set off, the channel group can
// be used to control the shutdown
if (!tenThreadGroup.isShutdown()) {
    // once the group is shut down no more channels can be created with it
    tenThreadGroup.shutdown();
}
if (!tenThreadGroup.isTerminated()) {
    // forcibly shutdown, the channel will be closed and the accept will abort
    tenThreadGroup.shutdownNow();
}
// the group should be able to terminate now, wait for a maximum of 10 seconds
tenThreadGroup.awaitTermination(10, TimeUnit.SECONDS);

AsynchronousFileChannel 在此处与其他通道不同,为了使用定制的线程池,open() 方法采用 ExecutorService 而不是 AsynchronousChannelGroup。

异步数据报通道与多播

最后的新通道是 AsynchronousDatagramChannel。它与 AsynchronousSocketChannel 很类似,但由于 NIO.2 API 在该通道级别增加了对多播的支持,而在 NIO 中只在 MulticastDatagramSocket 级别才提供这一支持,因此有必要将其单独提出。Java 7 中的 java.nio.channels.DatagramChannel 也能提供这一功能。

作为服务器来使用的 AsynchronousDatagramChannel 可构建如下:

AsynchronousDatagramChannel server = AsynchronousDatagramChannel.open().bind(null);

接下来,可设置客户端来接收发往一个多播地址的数据报广播。首先,必须在多播地址范围内选择一个地址(从 224.0.0.0 到 239.255.255.255),还要选择一个所有客户端都可绑定的端口:

// specify an arbitrary port and address in the range
int port = 5239;
InetAddress group = InetAddress.getByName("226.18.84.25");

我们也需要一个到所使用网络接口的引用:

// find a NetworkInterface that supports multicasting
NetworkInterface networkInterface = NetworkInterface.getByName("eth0");

现在,打开数据报通道并设置多播选项,如清单 4 所示:

清单 4. 打开数据报通道并设置多播选项

// the channel should be opened with the appropriate protocol family,
// use the defined channel group or pass in null to use the default channel group
AsynchronousDatagramChannel client =
    AsynchronousDatagramChannel.open(StandardProtocolFamily.INET,  tenThreadGroup);
// enable binding multiple sockets to the same address
client.setOption(StandardSocketOption.SO_REUSEADDR, true);
// bind to the port
client.bind(new InetSocketAddress(port));
// set the interface for sending datagrams
client.setOption(StandardSocketOption.IP_MULTICAST_IF, networkInterface);

客户端可通过如下方式加入多播组:

MembershipKey key = client.join(group, networkInterface);

java.util.channels.MembershipKey 是提供对组成员控制的新类。利用该键,您可丢弃组成员、阻塞或者取消阻塞来自特定地址的数据报、以及返回有关组和通道的消息。

服务器可以向特定地址和端口发送数据报,供客户端接收,如清单 5 所示:

清单 5. 发送以及接收数据报

// send message
ByteBuffer message = ByteBuffer.wrap("Hello to all listeners".getBytes());
server.send(message, new InetSocketAddress(group, port));

// receive message
final ByteBuffer buffer = ByteBuffer.allocate(100);
client.receive(buffer, null, new CompletionHandler<SocketAddress, Object>() {
    @Override
    public void completed(SocketAddress address, Object attachment) {
        System.out.println("Message from " + address + ": " +
            new String(buffer.array()));
    }

    @Override
    public void failed(Throwable e, Object attachment) {
        System.err.println("Error receiving datagram");
        e.printStackTrace();
    }
});

可在同一端口上创建多个客户端,它们可加入多播组来接收来自服务器的数据报。

异步通道 结束语

NIO.2 的异步通道 APIs 提供方便的、平台独立的执行异步操作的标准方法。这使得应用程序开发人员能够以更清晰的方式来编写程序,而不必定义自己的 Java 线程,此外,还可通过使用底层 OS 所支持的异步功能来提高性能。如同其他 Java API 一样,API 可利用的 OS 自有异步功能的数量取决于其对该平台的支持程度。

文件系统 API

本文是介绍 Java 7 中的 More New I/O APIs for Java (NIO.2) 的两部分文章的第二部分。 正如在 第一部分 中探索的异通道 API 一样, NIO.2 的文件系统用以前 Java 版本处理 I/O 的相关方法,填补了一些重大的空白。 依照 NIO.2 Java 规范要求(JSR 203):

Java 平台早就需要一个文件系统接口而不是 java.io.File 类。 该类不会在平台中以一贯的方式来处理文件名,它不支持高效文件属性访问,不允许复杂应用程序利用可用的文件系统特定特性(比如,符号链接), 而且,其大多数方法在出错时仅返回失败,而不会提供异常信息。
补救措施是 Java 7 试用版中的三个新的文件系统包:

  • java.nio.file
  • java.nio.file.attribute
  • java.nio.file.spi

本文重点关注这些包中最有用的类:

  • java.nio.file.Filesjava.nio.file.FileVisitor 使得您可以在文件系统中漫步,在特定目录深度查询文件或者目录,并可对每个查询结果执行用户实现的回调方法。
  • java.nio.file.Pathjava.nio.file.WatchService 允许 “ 注册 ” 来监视特定目录。如果在目录中发生了文件创建、修改或者删除操作,监视目录的应用程序将收到通知。
  • java.nio.attribute.*AttributeView 允许查看此前对于 Java 用户隐藏的文件和目录属性。这些属性包括文件所有者及组权限,访问控制列表(ACL),以及扩展文件属性。

文件访问类

我们的第一个例子演示了新的 FileVisitor API。

设想一个场景,您想要递归地访问一个目录树,在该树中的每个文件及目录上停下来,并为每个查找到的条目调用您自己的回调方法。在以前的 Java 版本中,这可能是个很痛苦的过程,包括递归列出目录、检查其条目、并自己调用回调。在 Java 7 中,这些都在 FileVisitor 中有提供,使用起来非常简单。

第一步是实现您自己的 FileVisitor 类。这个类包含 file-visitor 引擎穿越文件系统时所调用的回调方法。FileVisitor 接口由 5 个方法组成,在此处以遍历其间被调用的典型顺序来列出(T 在此处代表 java.nio.file.Path 或者超类):

  • 在访问目录中的条目之前调用 FileVisitResult preVisitDirectory(T dir)。它返回一个 FileVisitResult 枚举值,来告诉文件访问程序 API 下一步做什么。
  • 当目录由于某些原因无法访问时,调用 FileVisitResult preVisitDirectoryFailed(T dir, IOException exception)。在第二个参数中指出了导致访问失败的异常。
  • 在当前目录中有文件被访问时,调用 FileVisitResult visitFile(T file, BasicFileAttributes attribs)。该文件的属性传递给第二个参数。(可在本文 文件属性 部分更深入了解文件属性。)
  • 当访问文件失败时,调用 FileVisitResult visitFileFailed(T file, IOException exception)。第二个参数指明导致访问失败的异常。
  • 完成对目录及其子目录的访问后,调用 FileVisitResult postVisitDirectory(T dir, IOException exception)。当目录访问成功时,异常参数为空,或者包含导致目录访问过早结束的异常。

为了节约开发人员的时间, NIO.2 提供了 FileVisitor 的实现接口:java.nio.file.SimpleFileVisitor。该类以基础方式获取:对于 *Failed() 方法,它只是重新引发该异常,并且对于其他方法,它会继续下去而根本不做任何事!它的作用在于,您可以使用匿名类来替代您所希望替代的方法;剩下的方法会按默认方式实现。

清单 1 展示如何创建一个 FileVisitor 实例的例子:

清单 1. FileVisitor 实现

FileVisitor<Path> myFileVisitor = new SimpleFileVisitor<Path>() {

    @Override
    public FileVisitResult preVisitDirectory(Path dir) {
       System.out.println("I'm about to visit the "+dir+" directory");
       return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attribs) {

       System.out.println("I'm visiting file "+file+" which has size " +attribs.size());
       return FileVisitResult.CONTINUE;
    }

};

清单 1 中 FileVisitor 的实现,应当为其访问的每个目录和文件打印消息,并给出从其 BasicFileAttributes 中获取的文件大小。

接下来,我们想创建开始我们文件访问的 Path。利用类 java.nio.Paths 完成这一操作:

Path headDir = Paths.get("headDir");

我们可以利用两个方法中的任何一个在 类上启动树遍历:

  • public static void walkFileTree(Path head, FileVisitor<? super Path> fileVisitor) 浏览头目录下的文件树,在这一过程中调用在 fileVisitor 中实现的回调方法。
  • public static void walkFileTree(Path head, Set<FileVisitOption> options, int depth, FileVisitor<? super Path> fileVisitor) 与前面的方法相似,但是它给出两个附加的参数来指定访问选项,以及遍历将访问文件树中的多少个目录。

我们将使用 walkFileTree() 方法的简单版本,来开始浏览文件数的流程:

Files.walkFileTree(headDir, myFileVisitor);

假设目录结构是这样的:

headDir
 |--- myFile1
 |--- mySubDirectory1
 |     \myFile2
 \--- mySubDirectory2
       |--- myFile3
       \--- mySubdirectory3
             \---myFile4

清单 2 展示了这一例子的输出:

清单 2. FileVisitor 输出

I'm about to visit the headDir directory
I'm about to visit the headDir\mySubDirectory2 directory
I'm about to visit the headDir\mySubDirectory2\mySubDirectory3 directory
I'm visiting file headDir\mySubDirectory2\mySubDirectory3\myFile4 which has size 2
I'm visiting file headDir\mySubDirectory2\myFile3 which has size 2
I'm about to visit the headDir\mySubDirectory1 directory
I'm visiting file headDir\mySubDirectory1\myFile2 which has size 2
I'm visiting file headDir\myFile1 which has size 2

正如您所见到的,该文件是深度优先遍历,但不一定在任何目录内按字母顺序来进行。回调方法如期望那样被调用,我们可以看到,树中的所有文件都已列出,并且所有目录均已被访问。

仅利用 15 行内容,我们就创建了文件访问程序,来浏览任何您所给出的文件树,并检查包含在其中的文件。这个例子很基础,但是回调可按需地进行更加复杂的实现。

目录监视

第二个例子展示了新 WatchService API 及其相关类的精彩世界。

这一例子的场景很简单:您想要追踪特定目录(或多个目录)中是否有文件或者目录正被创建、修改、或者删除。您可能要利用这一信息来更新 GUI 显示中列示的文件,或者想检查对将要重新加载的配置文件的修改。在以前的 Java 版本中,必须实现一个代理,该代理运行在单独的线程中,来保持对目录所有内容的追踪,不断轮询文件系统来查看是否有相关的情况发生。在 Java 7 中,WatchService API 提供了查看目录的能力。这就免除了自己编写文件系统轮询程序的所有麻烦,并且,如果可能的话,它可基于本地系统 API 来获取更优的性能。

第一步是通过 java.nio.file.FileSystems 类创建 WatchService 实例。本文不涉及文件系统的细节,因此在大多数情况下,您 会希望得到默认的文件系统,然后调用其 newWatchService() 方法:

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

现在我们有了自己的监视服务实例,我们想要注册到一个路径来进行监视。因为我们想要以与文件访问程序示例完全不同的方式,来为监视的目录创建一个 Path 对象,所以,我们可以在此使用其 File 实例:

File watchDirFile = new File("watchDir");
Path watchDirPath = watchDirFile.toPath();

Path 类实现 java.nio.file.Watchable 接口,并且该接口定义我们将在这里例子中使用的 register() 方法。WatchKey register(WatchService watchService, WatchEvent.Kind<?>... events) 通过为所给特定事件所指定的 watchService 来注册这一方法所要调用的 Path。仅当在注册调用中指定了事件时,事件才会触发一个通知。

对于默认的 WatchService 实现,java.nio.file.StandardWatchEventKind 类定义三个 java.nio.file.StandardWatchEventKind 的静态实现,这些可用于 register() 调用:

  • StandardWatchEventKind.ENTRY_CREATE 指出在所注册的 Path 中创建了文件或者目录。当文件重命名或者移入这一目录时,还触发了 ENTRY_CREATE 事件。
  • StandardWatchEventKind.ENTRY_MODIFY 指出在所注册的 Path 中文件或者目录被修改。究竟是哪些事件组成了修改,在一定程度上是平台特定的,但是在这里只想说,其实对文件内容的修改总会触发一个修改事件。在一些平台中,变更文件的属性也会触发这一事件。
  • StandardWatchEventKind.ENTRY_DELETE 指出在所注册的 Path 中删除了文件或者目录。当对文件重命名或者将文件移出目录时,也会触发 ENTRY_DELETE 事件。

对于我们的例子,让我们监视 ENTRY_CREATE 和 ENTRY_MODIFY 事件,而不是 ENTRY_DELETE:

WatchKey watchKey = watchDirPath.register(watchService,
       StandardWatchEventKind.ENTRY_CREATE, StandardWatchEventKind.ENTRY_MODIFY);

Path 现在注册为被监视,并且 WatchService 将总会一直在后台安静地工作,专心地监视目录。利用前面所展示的相同 Path 创建和 register() 调用,同一个 WatchService 实例能够监视多个目录。

您可能已经发现 register() 方法调用返回了我们以前从未遇到过的类:WatchKey。该类代表注册到 WatchService。是否挂接到这个引用由您决定,因为触发事件后,WatchService 会返回相关的 WatchKey。然而,要注意不存在方法调用用来找出 WatchKey 注册到哪个 目录,因此如果正在监视多个目录,您可能想要追踪哪个 WatchKey 与哪个 Path 相关。当您 在处理特定 WatchKey 及其所注册的事件时,可以很简单地通过调用 cancel() 方法,来取消其在 WatchService 的注册。

现在已注册了 Path,我们可以用很方便的方式来检查 WatchService,看是否发生了任何我们感兴趣的事件。WatchService 提供三个方法来检查是否有任何令人激动的事发生。

  • 如果有相关事件出现,WatchKey poll() 会返回下一个 WatchKey,或者没有注册的事件发生,会返回 null。
  • WatchKey poll(long timeout, TimeUnit unit) 需要超时和时间单元(java.util.concurrent.TimeUnit)。如果在特定时间范围内,有任何事件发生,这一方法存在,会返回相应的 WatchKey。如果在超时时间结束时,没有 WatchKeys 返回,这一方法将会返回 null
  • WatchKey take() 与前面的方法相似,不同之处是,它将无限期等待,直到可以返回 WatchKey

一旦这三个方法之一返回了 WatchKey,它将不会再被 poll() 或者 take() 调用返回,直到其 reset() 方法被调用。一旦 WatchService 返回了 WatchKey,就可以检查由于调用了 WatchKeypollEvents() 方法而触发的事件,其将返回一列 WatchEvent

为了便于说明,清单 3 中的简单示例仍然是来自我们前面注册的 WatchKey :

清单 3. 使用 pollEvents()

// Create a file inside our watched directory
File tempFile = new File(watchDirFile, "tempFile");
tempFile.createNewFile();

// Now call take() and see if the event has been registered
WatchKey watchKey = watchService.take();
for (WatchEvent<?> event : watchKey.pollEvents()) {
   System.out.println(
   "An event was found after file creation of kind " + event.kind()
   + ". The event occurred on file " + event.context() + ".");
}

当执行完成后,清单 3 中的代码打印为:

An event was found after file creation of kind ENTRY_CREATE. The event occurred
 on file tempFile.
An event was found after file creation of kind ENTRY_MODIFY. The event occurred
 on file tempFile.

正如您所见到的,我们得到了期望的、新创建的 tempFile 的 ENTRY_CREATE 事件,但我们还得到了其他事件。在一些操作系统中,文件的创建或删除也会产生修改事件,那么不管是什么 OS ,我们只会得到 ENTRY_CREATE 事件。

文件属性

第三个并且是最后一个例子涉及了利用 java.nio.file.attribute 包中的类获取并设置文件属性的新的 API。

新的 API 能够提供对各种文件属性的访问。在以前的 Java 版本中,仅能得到基本的文件属性集(大小、修改时间、文件是否隐藏、以及它是文件还是目录)。为了获取或者修改更多的文件属性,必须利用运行所在平台特定的本地代码来实现,这很困难。很幸运的是,Java 7 能够允许您通过很简单的方式,利用 java.nio.file.attribute 类来读取,如果可能,修改扩展的属性集,完全去掉了这些操作的平台特定属性。

在新的 API 中有七个属性视图,其中一些特定于操作系统。这些 “ 视图 ” 类允许您获取并设置任何关联的属性,并且其中每个都具有对应的包含真实属性信息的属性类。让我们依次来看一下。

AclFileAttributeView 与 AclEntry

AclFileAttributeView 允许您为特定文件设置 ACL 及文件所有者属性。其 getAcl() 方法返回一个 List of AclEntry 对象,每个对应文件的一个权限集。其 setAcl(List<AclEntry>) 方法允许您修改该访问列表。这些属性视图仅可用于 Microsoft® Windows® 系统。

BasicFileAttributeView 与 BasicFileAttributes

这一视图类允许您获取一系列 —— 平常的 —— 基本文件属性,构建于以前的 Java 版本之上。其 readAttributes() 方法返回一个 BasicFileAttributes 实例,该实例包含最后修改时间、最后访问时间、创建时间、大小、以及文件属性等细节(常规文件、目录、符号链接、或者其他)。这一属性视图在所有平台上均可用。

我们来看一下这一视图的相关例子。为获取特定文件的文件属性视图,我们通常从为我们所感兴趣的文件创建 Path 对象开始:

File attribFile = new File("attribFile");
Path attribPath = attribFile.toPath();

为获取想要的文件属性视图,我们在 Path 上使用 getFileAttributeView(Class viewClass) 方法。为获取 BasicFileAttributeView for attribPath,我们简单地调用:

BasicFileAttributeView basicView
    = attribPath.getFileAttributeView(BasicFileAttributeView.class);

正如前面所描述的,为从 BasicFileAttributeView 获取 BasicFileAttributes,我们只要调用其 readAttributes() 方法:

BasicFileAttributes basicAttribs = basicView.readAttributes();

那么这样就可以了,现在已经得到了您所想要的任何基本文件属性。对于 BasicFileAttributes,只有创建、最后修改、以及最后访问时间可被修改(因为改变文件大小或者类型没有意义)。为改变这些,我们可以使用 java.nio.file.attribute.FileTime 类来创建新的时间,然后在 BasicFileAttributeView 上调用 setTimes() 方法。例如,我们可以不断地更新文件的最后修改时间。

FileTime newModTime
    = FileTime.fromMillis(basicAttribs.lastModifiedTime().toMillis() + 60000);
 basicView.setTimes(newModTime, null, null);

这两个 null 指出,我们不想改变这一文件的最后访问时间或者创建时间。如果以前面相同的方式再次检查基本属性,您会发现最后修改时间已被修改,但是创建时间和最后访问时间还保持原样。

DosFileAttributeView 与 DosFileAttributes

这一视图类允许您获取指定给 DOS 的属性。(您可能会猜想,这一视图仅用于 Windows 系统。)其 readAttributes() 方法返回一个 DosFileAttributes 实例,该实例包含有问题的文件是否为只读、隐藏、系统文件、以及存档文件等细节信息。这一视图还包含针对每个属性的 set*(boolean) 方法。

FileOwnerAttributeView 与 UserPrincipal

这一视图类允许您获取并设置特定文件的所有者。其 getOwner()方法返回一个 UserPrincipal(还处于 java.nio.file.attribute 包中),其又具有 getName() 方法,来返回包含所有者名字的 String。该视图还提供 setOwner(UserPrincipal) 方法用于变更文件所有者。该视图在所有平台上都可用。

FileStoreSpaceAttributeView 与 FileStoreSpaceAttributes

这一用很吸引人的方式命名的类,允许您获取有关特定文件存储的信息。其 readAttributes() 方法返回一个包含文件存储的整个空间、未分配空间、以及已使用空间细节的 FileStoreSpaceAttributes 实例。这一视图在所有平台上都可用。

PosixFileAttributeView 与 PosixFileAttributes

这一视图类,仅在 UNIX® 系统上可用,允许您获取并设置指定给 POSIX(Portable Operating System Interface)的属性。其 readAttributes() 方法返回一个包含有关这一文件的所有者、组所有者、以及这一文件许可(这些细节通常用 UNIX chmod 命令设置)的 PosixFileAttributes 实例。这一视图还提供 setOwner(UserPrincipal)setGroup(GroupPrincipal)、以及 setPermissions(Set<PosixFilePermission>) 来修改这些属性。

UserDefinedFileAttributeView 与 String

这一视图类,仅可用于 Windows,允许您获取并设置文件的扩展属性。 这些属性跟其他的不同,它们只是名称值对,并可按需对其进行设置。 如果想向文件增加一些隐藏的元数据,而不必修改文件内容,这就很有用了。 这一属性提供 list() 方法,来为相关的文件返回 List of String 扩展属性的名字。

有了其名字后,就要获取特定属性的内容,这一视图具有一个 size(String name) 方法来返回属性值的大小,以及一个 read(String name, ByteBuffer dest) 方法来将属性值读取到 ByteBuffer 中。这一视图还提供 write(String name, ByteBuffer source) 方法来创建或者修改属性,以及一个 delete(String name) 方法来完全移除现有的属性。

这可能是最有趣的新属性视图,因为它允许您利用任意 String 名字和 ByteBuffer 值向文件增加属性。这很对 —— 其值是个 ByteBuffer,因此您可以在这里存储任何二进制数据。

首先,我们将会获取属性视图:

UserDefinedFileAttributeView userView
    = attribPath.getFileAttributeView(UserDefinedFileAttributeView.class);

为获取用户为这一文件定义的属性名,我们在视图上调用 list() 方法:

List<String> attribList = userView.list();

一旦我们拥有了想得到相关值的特定属性名,就为该值分配一个大小合适的 ByteBuffer,然后调用视图的 read(String, ByteBuffer) 方法:

ByteBuffer attribValue = ByteBuffer.allocate(userView.size(attribName));
userView.read(attribName, attribValue);

attribValue 现在包含了为那一特定属性所存储的任何数据。 想设置自己的属性,只需创建 ByteBuffer 并按需填入数据,然后在视图上调用 write(String, ByteBuffer) 方法:

userView.write(attribName, attribValue);

写入属性,或者创建该属性,或者利用相同的名字覆盖已有的属性。

这样,我们结束第三个而且是最后一个例子。

文件系统 结束语

除了本文所提到的以外,还有很多其他的 NIO.2 文件 API。Java 7 具有创建、检查、并修改符号链接的新功能。还有新的类,来允许访问文件系统的低级信息,并支持提供者(称为 FileSystem 和 FileStore)访问任何想要访问的文件系统。

总之,NIO.2 为 Java 开发人员提供了一系列简单、兼容、并且功能强大的 API,来与文件系统交互。其目的是抽取处理文件和目录时所涉及的复杂的、平台特定的细节,并能较好地为程序员提供更强大的功能和更多的灵活性。

本文转载自:

  1. 异步通道 API
  2. 文件系统 API