Java7技术系列:NIO.2异步IO

    xiaoxiao2023-10-04  159

    Java7技术系列:try-with-resource Java7技术系列:int与二进制的转换优化 Java7技术系列:MultiCatchException Java7技术系列:NIO.2异步IO Java7技术系列:DI依赖注入 Java7技术系列:Queue Java7技术系列:Java并发

    文章目录

    1 NIO.2相关API介绍1.1 NIO.2关键基础类1.2 文件处理的基础类 2 Path2.1 获取Path2.2 从Path中获取信息2.3 去除冗余项2.4 转换Path2.5 java7的Path与java7之前的File之间转换2.6 文件属性 3 文件操作3.1 创建文件3.2 删除文件3.3 复制文件3.4 移动文件 4 文件读写数据4.1 读文件4.2 写文件4.3 读写文件的简化4.4 拥有文件寻址的FileChannel读写文件4.5 文件修改监听通知WatchService 5 处理目录与目录树5.1 目录树查找文件过滤5.2 遍历目录树 6 异步IO7 Socket和Channel整合

    1 NIO.2相关API介绍

    NIO.2把位置(由Path表示)的概念和物理文件系统的处理(比如复制一个文件)分得很清楚,物理文件系统通常是由Files辅助类实现的【即Path不一定代表真实的文件或目录,你可以随心所欲地操作Path,用Files中的功能来检查文件是否存在,并对它进行处理】

    【个人理解:Path是虚拟的,不对实际的文件进行操作处理,只读取Path指定目录或文件的信息; Files是会对实际的文件进行操作处理,比如复制、读写等】

    1.1 NIO.2关键基础类

    API描述NIO2Path类中的方法可以用来获取路径信息,访问该路径中的各元素,将路径转换为其他形式,或提取路径中的一部分。有的方法还可以匹配路径字串以及移除路径中的冗余项Paths工具类,提供返回一个路径的辅助方法,比如get(String first, String… more)和get(URI uri)FileSystem与文件系统交互的类,无论是默认的文件系统,还是通过其统一资源标识(URI)获取的可选文件系统FileSystems工具类,提供各种方法,比如其中用于返回默认文件系统的FileSystems.getDefault()

    相对路径与绝对路径(以访问 /java7developer/src/main 目录为例):

    比如程序在路径 /java7developer/src/test 目录下运行,代码要读取位于/java7developer/src/main目录的文件名。

    为了进入 /java7developer/src/main 目录,可以用相对路径 ../main。

    如果程序在路径 /java7developer/src/test/java/com/java7developer 运行,用相对路径 ../main 则无法进入目标目录,而是会访问到不存在的目录 /java7developer/src/test/java/com/main,只能用绝对路径 /java7developer/src/main。

    【总结:使用相对路径,相当于将当前运行程序的目录回退一个目录,然后再进入对应的目标目录,也就是说,要访问的目录与程序运行目录,它们的上一级目录是相同的,此时才能使用相对路径正确访问】

    1.2 文件处理的基础类

    API描述Files实现复制、移动、删除或处理文件的工具类WatchService用来监视文件或目录的核心类,不管它们有没有变化

    2 Path

    2.1 获取Path

    private static void operate1() { // NIO2.get(String first, String... more),第二个参数一般用不到,它仅仅用来把额外的字符串合并起来形成Path字符串 // 注意:NIO.2的API中,Path或Paths中的各种方法抛出的受检异常只有IOException。这有时会存在掩藏潜在的问题, // 如果想处理IOException的显式子类,则需要额外编写异常处理代码。 java.nio.file.Path path = Paths.get("/usr/bin/zip"); java.nio.file.Path samePath = FileSystems.getDefault().getPath("/usr/bin/zip"); // 与上面代码等价 java.nio.file.Path absolutePath = path.toAbsolutePath(); // 转换为绝对路径 }

    2.2 从Path中获取信息

    private static void operate2() { java.nio.file.Path path = Paths.get("/usr/bin/zip"); java.nio.file.Path filename = path.getFileName(); // 获取文件名,zip int nameCount = path.getNameCount(); // 获取文件名字符数,3 java.nio.file.Path parent = path.getParent(); // 获取父目录,/usr/bin java.nio.file.Path root = path.getRoot(); // 获取根目录,/ java.nio.file.Path subpath = parent.subpath(0, 2); // 根据索引获取子路径,usr/bin }

    2.3 去除冗余项

    private static void operate3() { // ①在处理Path时可能会有一个或两个点: // .表示当前目录 ..表示父目录 // 假设程序在/java7developer/src/main/java/com/java7developer/chapter2/目录运行, // 所在目录和Listing_2_1.java一样,如果传给你的Path是./Listing_2_1.java,其中./部分即为正在运行的当前目录 // normalize()去除掉冗余项./,结果为Listing_2_1.java java.nio.file.Path normalizePath = Paths.get("./Listing_2_1.java").normalize(); // ②符号链接冗余处理,在*nix系统中/usr/logs目录下,你想寻找日志文件log1.txt, // 但其实.usr/logs只是一个指向/application/logs的符号链接,那里才是存放日志文件的真正位置, // 要得到这个位置,就需要去掉冗余符号信息。这些冗余项都会导致Path指向的不是你认为它应该指向的位置。 // toRealPath()也可以去除冗余项,它是融合了toAbsolutePath()和normalize()两个方法的功能 try { java.nio.file.Path realPath = Paths.get("/usr/logs/log1.txt").toRealPath(LinkOption.NOFOLLOW_LINKS); } catch (IOException e) { e.printStackTrace(); } }

    2.4 转换Path

    private static void operate4() { // 合并Path,调用resolve()方法,合并表示/uat/conf/application.properties的完整Path java.nio.file.Path prefix = Paths.get("/uat/"); java.nio.file.Path completePath = prefix.resolve("conf/application.properties"); // 计算两个目录之间的路径,通过relativize()方法 String java7developerPath = "/java7developer/src/test/java/com/java7developer"; String mainPath = "/java7developer/src/main"; java.nio.file.Path java7developerDir = Paths.get(java7developerPath); java.nio.file.Path mainDir = Paths.get(mainPath); java.nio.file.Path java7developerRelativizePath = java7developerDir.relativize(mainDir); java.nio.file.Path mainRelativizePath = mainDir.relativize(java7developerDir); System.out.println(java7developerRelativizePath); // 结果:..\..\..\..\main System.out.println(mainRelativizePath); // 结果:..\test\java\com\java7developer }

    2.5 java7的Path与java7之前的File之间转换

    private static void operate5() { File file = new File("../Listing_2_1.java"); java.nio.file.Path path = file.toPath(); // 通过toPath()转换为Path path.toAbsolutePath(); file = path.toFile(); // 通过toFile()转换为File }

    2.6 文件属性

    private static void operate3() { // ①在处理Path时可能会有一个或两个点: // .表示当前目录 ..表示父目录 // 假设程序在/java7developer/src/main/java/com/java7developer/chapter2/目录运行, // 所在目录和Listing_2_1.java一样,如果传给你的Path是./Listing_2_1.java,其中./部分即为正在运行的当前目录 // normalize()去除掉冗余项./,结果为Listing_2_1.java java.nio.file.Path normalizePath = Paths.get("./Listing_2_1.java").normalize(); // ②符号链接冗余处理,在*nix系统中/usr/logs目录下,你想寻找日志文件log1.txt, // 但其实.usr/logs只是一个指向/application/logs的符号链接,那里才是存放日志文件的真正位置, // 要得到这个位置,就需要去掉冗余符号信息。这些冗余项都会导致Path指向的不是你认为它应该指向的位置。 // toRealPath()也可以去除冗余项,它是融合了toAbsolutePath()和normalize()两个方法的功能 try { java.nio.file.Path realPath = Paths.get("/usr/logs/log1.txt").toRealPath(LinkOption.NOFOLLOW_LINKS); } catch (IOException e) { e.printStackTrace(); } }

    3 文件操作

    3.1 创建文件

    // 创建文件,通常处于安全考虑,要定义所创建的文件是用于读、写、执行,或者三者权限的某种组合时,要指明该文件的某些FileAttributes,这取决于文件系统,所以需要使用与文件系统相关的文件权限类 // 创建一个在POSIX系统上为属主、属组内用户和所有用户设置读写权限的文件 // 注意:如果在创建文件时要指定访问许可,不要忽略其父目录强加给该文件的umask限制或受限许可。 // 比如说,你会发现即使为新文件指定了rw-rw-rw许可,但由于目录的掩码,实际上文件最终访问许可为rw-r--r-- java.nio.file.Path path = Paths.get("D:\\Backup\\MyStuff.txt"); Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rw-rw-rw-"); FileAttribute<Set<PosixFilePermission>> attr = PosixFilePermissions.asFileAttribute(perms); Files.createFile(path, attr);

    3.2 删除文件

    // 删除文件,要删除文件需要有删除文件的权限 java.nio.file.Path target = Paths.get("D:\\Backup\\MyStuff.txt"); Files.delete(target);

    3.3 复制文件

    // 复制文件 // StandardCopyOption有三种选项: // [1] REPLACE_EXISTING:覆盖即替换已有文件 // [2] COPY_ATTRIBUTES:复制文件 // [3] ATOMIC_MOVE:确保在两边都操作成功,否则回滚 java.nio.file.Path sourcePath = Paths.get("C:\\MyDocuments\\Stuff.txt"); java.nio.file.Path targetPath = Paths.get("D:\\Backup\\MyStuff.txt"); Files.copy(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING);

    3.4 移动文件

    // 移动文件 // 在移动源文件时保留其属性,并且覆盖目标文件(如果存在的话) java.nio.file.Path sources = Paths.get("C:\\MyDocuments\\Stuff.txt"); java.nio.file.Path targets = Paths.get("D:\\Backup\\MyStuff.txt"); Files.move(sources, targets, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES);

    4 文件读写数据

    4.1 读文件

    // 注意:在处理String时,要处理字符编码问题StandardCharsets.UTF_8,防止不可预料的字符编码问题 java.nio.file.Path path = Paths.get("/tmp/app.log"); try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) { String line; while ((line = reader.readLine()) != null) { System.out.println(line); } } catch (IOException e) { e.printStackTrace(); }

    4.2 写文件

    // 写文件,对应的StandardOpenOptions可以确保文件的读写有正确的访问许可 // Files.newInputStream(path,OpenOptions)新建一个输入流 // Files.newOutputStream(path, OpenOptions)新建一个输出流 // StandardOpenOptions.WRITE // StandardOpenOptions.READ // StandardOpenOptions.APPEND java.nio.file.Path logPath = Paths.get("/tmp/app.log"); try (BufferedWriter writer = Files.newBufferedWriter(logPath, StandardOpenOption.WRITE)) { writer.write("Hello World!"); } catch (IOException e) { e.printStackTrace(); }

    4.3 读写文件的简化

    // 读写简化 java.nio.file.Path logFile = Paths.get("/tmp/app.log"); try { List<String> lines = Files.readAllLines(logFile); // 读取文件所有行 byte[] bytes = Files.readAllBytes(path); // 读取文件所有字节 } catch (IOException e) { e.printStackTrace(); }

    4.4 拥有文件寻址的FileChannel读写文件

    // SeekableByteChannel接口,是为了让开发人员能够改变字节通道的位置和大小。 // 它的实现类FileChannel,这个类可以在文件读取或写入时保持当前位置 // 读取日志文件中的最后1000个字符,FileChannel具有寻址能力 java.nio.file.Path file = Paths.get("/temp/app.log"); ByteBuffer buffer = ByteBuffer.allocate(1024); FileChannel channel; try { channel = FileChannel.open(file, StandardOpenOption.READ); channel.read(buffer, channel.size() - 1000); } catch (IOException e) { e.printStackTrace(); }

    4.5 文件修改监听通知WatchService

    // 文件修改通知 // WatchService类检测文件或目录变化,该类用客户线程监视注册文件或目录的变化 // 并且检测到变化时返回一个事件。是轮询机制的一个理想替代品 // 检测目录变化,打印消息 // StandardWatchEventKinds.ENTRY_MODIFY:监听事件修改 // StandardWatchEventKinds.ENTRY_CREATE:监听事件创建 // StandardWatchEventKinds.ENTRY_DELETE:监听事件删除 // StandardWatchEventKinds.OVERFLOW:监听事件已经丢失或丢弃 try { boolean isShutdown = false; WatchService watcher = FileSystems.getDefault().newWatchService(); java.nio.file.Path dir = Paths.get("/usr/vincent"); WatchKey key = dir.register(watcher, StandardWatchEventKinds.ENTRY_MODIFY);// 检测变化 while (!isShutdown) { key = watcher.take(); // 得到下一个key及其事件 for (WatchEvent<?> event : key.pollEvents()) { if (event.kind() == StandardWatchEventKinds.ENTRY_MODIFY) { // 检查是否为变化事件 System.out.println("dir changed!"); isShutdown = true; } } key.reset(); // 重置监听key,等待下一个事件 } } catch (IOException | InterruptedException e) { e.printStackTrace(); }

    5 处理目录与目录树

    5.1 目录树查找文件过滤

    // 目录树查找文件,过滤指定目录中所有.properties文件 java.nio.file.Path dir = Paths.get("C:\\workspace\\java7developer"); try { // Files.newDirectoryStream()返回指定目录中过滤后的DirectoryStream DirectoryStream<java.nio.file.Path> stream = Files.newDirectoryStream(dir, "*.properties"); for (java.nio.file.Path path : stream) { System.out.println(path.getFileName()); } } catch (IOException e) { e.printStackTrace(); }

    5.2 遍历目录树

    // 遍历目录树,walkFileTree可以实现递归移动、复制和删除操作 // API // 关键方法:Files.walkFileTree(NIO2 startingDir, FileVisitor<? super NIO2> isitor); // FileVisitor接口有五个方法(T一般就是Path): // [1]FileVisitResult preVisitDirectory(T dir) // [2]FileVisitResult preVisitDirectoryFailed(T dir, IOException e) // [3]FileVisitResult visitFile(T file, BasicFileAttributes attrs) // [4]FileVisitResult visitFileFailed(T file, IOException e) // [5]FileVisitResult postVisitDirectory(T dir, IOException e) // java7已经提供了一个默认实现类,SimpleFileVisitor<T> // 注意:为了确保递归等操作的安全性,walkFileTree()不会自动跟随符号链接。如果需要跟随符号链接,需要使用 normalize()或toRealPath()检查。 // 遍历C:\workspace\java7developer\src目录下及其子目录内的所有.java文件 java.nio.file.Path startingDir = Paths.get("C:\\workspace\\java7developer\\src"); try { Files.walkFileTree(startingDir, new FindJavaVisitor()); } catch (IOException e) { e.printStackTrace(); } private static final class FindJavaVisitor extends SimpleFileVisitor<java.nio.file.Path> { @Override public FileVisitResult visitFile(java.nio.file.Path file, BasicFileAttributes attrs) throws IOException { if (file.toString().endsWith(".java")) { System.out.println(file.getFileName()); } return FileVisitResult.CONTINUE; } }

    6 异步IO

    privatestatic void operate10() { // java7中有三个新的异步通道: // AsynchronousFileChannel:用于文件I/O // AsynchronousSocketChannel:用于套接字I/O,支持超时 // AsynchronousServerSocketChannel:用于套接字接受异步连接 // 新的异步I/O主要有两种形式:将来式和回调式 // 将来式:表明使用java.util.concurrent.Future接口。 // Future接口提供了几个方法: // ①get():用来获取结果。如果结果还没有准备好,get()会被阻塞直到它能取得结果。 // ②get(int second, TimeUnit unit):获取结果,可以设置超时时间。 // ③cancel():在运算结束前取消。 // ④isDone():调用者用它来判断运算是否结束。 // 当希望由主线程发起I/O操作并轮询等待结果时,一般都会用将来式异步处理 // 流程:将来式用现有的java.util.concurrent技术声明一个Future,用来保存异步操作的处理结果。 // 这很关键,因为这意味着当前线程不会因为比较慢的I/O操作而停滞。相反,有一个单独的 // 线程发起I/O操作,并在操作完成时返回结果。与此同时,主线程可以继续执行其他需要完成的任务。 // 在其他任务结束后,如果I/O操作还没有完成,主线程会一直等待。 // 实现方式:AsynchronousFileChannel会关联线程池,它的任务是接收I/O处理事件,并分发给负责处理通道中I/O操作结果 // 的结果处理器。跟通道中发起的I/O操作关联的结果处理器确保是由线程池中的某个线程产生的。 // 如果在创建AsynchronousFileChannel时没有为其指明线程池,那就会为其分配一个系统默认的线程池(可能会和其他通道共享)。 // 默认线程池是由AsynchronousChannelGroup类定义的系统属性进行配置的。 try { java.nio.file.Path file = Paths.get("/usr/vincent/foobar.txt"); AsynchronousFileChannel channel = AsynchronousFileChannel.open(file); ByteBuffer byteBuffer = ByteBuffer.allocate(100_0000); // 在该文件中读取字节大小 Future<Integer> result = channel.read(byteBuffer, 0); // 这里手动调用了isDone()判断是否读取完成 // 通常情况下,result或结束(主线程会继续执行),或等待后台I/O完成 while (!result.isDone()) { // 在Future并发异步执行文件读取操作时,主线程可以继续做其他操作 System.out.println("do something in main thread"); } Integer bytesRead = result.get(); // 获取读取结果 System.out.println("Bytes read[" + bytesRead + "]"); } catch (IOException | InterruptedException | ExecutionException e) { e.printStackTrace(); } // 回调式:其基本思想是主线程会派一个侦察员CompletionHandler到独立的线程中执行I/O操作。 // 这个侦察员将带着I/O操作的结果返回到主线程中,这个结果会触发它自己的completed或failed方法。 // CompletionHandler<V, A>:V是结果类型,A是提供的附着对象 try { java.nio.file.Path files = Paths.get("/usr/vincent/foobar.txt"); AsynchronousFileChannel channel = AsynchronousFileChannel.open(files); ByteBuffer byteBuffer = ByteBuffer.allocate(100_0000); channel.read(byteBuffer, 0, byteBuffer, new CompletionHandler<Integer, ByteBuffer>() { @Override public void completed(Integer result, ByteBuffer attachment) { System.out.println("Bytes read [" + result + "]"); } @Override public void failed(Throwable exc, ByteBuffer attachment) { System.out.println(exc.getMessage()); } }); } catch (IOException e) { e.printStackTrace(); } }

    7 Socket和Channel整合

    private static void operate11() { // java7提供了NetworkChannel,把Socket和Channel结合到一起 // java.nio.channel包:定义通道,表示连接到执行I/O操作的实体,比如文件和套接字。定义用于多路传输、非阻塞I/O操作的选择器。 // java.nio.Socket包:该类实现了客户端套接字。套接字是两个机器间通信的端点。 // 在旧版java中,为了执行I/O操作,比如向TCP端口中写入数据,你需要将通道绑定到Socket的实现类上,但存在问题: // ①在旧版java中,为了配置套接字选项和绑定在套接字上,必须把通道和套接字的API整合在一起。 // ②在旧版java中,不能利用平台特定的套接字行为。 // NetworkChannel:代表一个连接到网络套接字通道的映射。它定义了一组实用的方法,比如查看及设置通道上可用的套接字选项等。 // 输出互联网套接字地址在端口3080上所支持的选项,设置IP服务条款选项以及确认套接字通道上的SO_KEEPALIVE选项 SelectorProvider provider = SelectorProvider.provider(); try { SocketChannel channel = provider.openSocketChannel(); SocketAddress address = new InetSocketAddress(3080); channel = channel.bind(address); // 将端口绑定到3080 Set<SocketOption<?>> socketOptions = channel.supportedOptions(); // 获取该端口所支持选项 System.out.println(socketOptions.toString()); channel.setOption(StandardSocketOptions.IP_TOS, 3); // 设置IP服务条款选项TOS Boolean keepAlive = channel.getOption(StandardSocketOptions.SO_KEEPALIVE);// 获取SO_KEEPALIVE选项 } catch (IOException e) { e.printStackTrace(); } // NetworkChannel让多播操作称为可能 // 多播(组播):表示一对多的网络通讯,通常用来指代IP多播。其基本前提是将一个包发送到一个组播地址, // 然后网络对该包进行复制,分发给所有接收端(注册到组播地址中) // 为了让NetworkChannel加入多播组,java7提供了一个新接口java.nio.channels.MulticastChannel及默认实现类DatagramChannel // 加入IP地址为180.90.4.12的多播组,并对其发送和接收系统状态信息 try { NetworkInterface networkInterface = NetworkInterface.getByName("net1"); // 选择网络接口 DatagramChannel dc = DatagramChannel.open(StandardProtocolFamily.INET); // 打开DatagramChannel // 将通道设置为多播 dc.setOption(StandardSocketOptions.SO_REUSEADDR, true); dc.bind(new InetSocketAddress(8080)); dc.setOption(StandardSocketOptions.IP_MULTICAST_IF, networkInterface); // 加入多播组 InetAddress group = InetAddress.getByName("180.90.4.12"); MembershipKey key = dc.join(group, networkInterface); } catch (IOException e) { e.printStackTrace(); } }
    最新回复(0)