JavaJava 进阶JVM · Java虚拟机原理 · JVM上语言·框架· 生态系统

netty(三)nio之文件编程

2021-11-03  本文已影响0人  我犟不过你

本篇文章主要讨论一下NIO中的文件变成,主要是FileChannel的用法。

一、FileChannel常用操作

1.1 获取FileChannel

有一个文件text.txt,其内容如下:

abcdef

不能直接打开 FileChannel,必须通过 FileInputStream、FileOutputStream 或者 RandomAccessFile 来获取 FileChannel,它们都有 getChannel 方法

1.1.1 通过 FileInputStream 获取

    public static void main(String[] args) throws Exception {
        //使用FileInputStream获取channel
        FileInputStream fileInputStream = new FileInputStream(new File("C:\\Users\\P50\\Desktop\\text.txt"));
        FileChannel channel1 = fileInputStream.getChannel();
        ByteBuffer buffer= ByteBuffer.allocate(10);
        //channel1.write(buffer);
        channel1.read(buffer);
        buffer.flip();
        System.out.println((print(buffer)));
    }

    static String print(ByteBuffer b) {
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < b.limit(); i++) {
            stringBuilder.append((char) b.get(i));
        }
        return stringBuilder.toString();
    }

结果:

abcdef

通过 FileInputStream 获取的 channel 只能读,如果使用写入write方法,会抛出异常:

Exception in thread "main" java.nio.channels.NonWritableChannelException
    at sun.nio.ch.FileChannelImpl.write(FileChannelImpl.java:201)
    at com.cloud.bssp.nio.FileChannel.GetFileChannel.main(GetFileChannel.java:21)

1.1.2 通过 FileOutputStream 获取

    public static void main(String[] args) throws Exception {
        //使用FileOutputStream获取channel
        FileOutputStream fileOutputStream = new FileOutputStream(new File("C:\\Users\\P50\\Desktop\\text.txt"),true);
        FileChannel channel2 = fileOutputStream.getChannel();
        ByteBuffer buffer= ByteBuffer.allocate(10);
        buffer.put(StandardCharsets.UTF_8.encode("helloworld"));
        buffer.flip();
        channel2.write(buffer);
    }

    static String print(ByteBuffer b) {
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < b.limit(); i++) {
            stringBuilder.append((char) b.get(i));
        }
        return stringBuilder.toString();
    }

文件被写入,这里注意FileOutputStream 的属性append,如果是true,表示追加,否则覆盖。本文使用的追加。

abcdefhelloworld

通过 FileOutputStream 获取的 channel 只能写,如果使用read方法,会抛出异常:

Exception in thread "main" java.nio.channels.NonReadableChannelException
    at sun.nio.ch.FileChannelImpl.read(FileChannelImpl.java:149)
    at com.cloud.bssp.nio.FileChannel.GetFileChannel.main(GetFileChannel.java:28)

1.1.3 通过 RandomAccessFile获取

    public static void main(String[] args) throws Exception {
        //使用RandomAccessFile获取channel
        RandomAccessFile file = new RandomAccessFile("C:\\Users\\P50\\Desktop\\text.txt", "rw");
        FileChannel channel3 = file.getChannel();
        ByteBuffer buffer = ByteBuffer.allocate(15);
        //读取文件内容到buffer
        channel3.read(buffer);
        buffer.flip();
        System.out.println(print(buffer));

        // 切换为写模式,并且清空buffer
        buffer.clear();
        //写入helloworld到文件
        buffer.put(StandardCharsets.UTF_8.encode("helloworld"));
        buffer.flip();
        channel3.write(buffer);
    }

        // 切换为写模式,并且清空buffer
        buffer.clear();
        //写入helloworld到文件
        buffer.put(StandardCharsets.UTF_8.encode("helloworld"));
        buffer.flip();
        channel3.write(buffer);

结果:这里读取的少了一个字节,因为我指定的buffer只有15,文档中是16,只读取了一次,。

abcdefhelloworl

文档内容被修改为如下,将channel读取到的内容以及新加入的内容拼接在了一起

abcdefhelloworlhelloworld

通过 RandomAccessFile 是否能读写根据构造 RandomAccessFile 时的读写模式决定,指定rw(读写模式)。

1.2 读取和写入

1.2.1 读取

在前面的获取例子中已经给出了关于读取的方式,如下所示,会返回int类型,从 channel 读取数据填充ByteBuffer,返回值表示读到了多少字节,返回值为-1 表示到达了文件的末尾。

int readBytes = channel.read(buffer);

仍然使用上面的第一个例子,如果文档是空的话,则会返回-1

 int read = channel1.read(buffer);
 System.out.println(read);
-1

1.2.2 写入

如上一章节的例子,已经演示了如何写入数据,利用write方法,将buffer的数据写入channel,但是正确的写入方式应该如下所示:

while(buffer.hasRemaining()) {
    channel.write(buffer);
}

hasRemaining()是buffer的一个方法,判断position是否小于limit,是则返回true,表示buffer仍然有未读取的数据。

在 while 中调用 channel.write 是因为 write 方法并不能保证一次将 buffer 中的内容全部写入 channel。

1.2.3 强制写入

操作系统出于性能的考虑,会将数据缓存,不是立刻写入磁盘。可以调用 channel.force(true) 方法将文件内容和元数据(文件的权限等信息)立刻写入磁盘。

public abstract void force(boolean metaData) throws IOException;

1.3 关闭

像我们上面写的代码实际上都没有去关闭流和channel的,这如果在生产环境都是会产生严重的问题。

channel是必须要关闭的,不过调用了 FileInputStream、FileOutputStream 或者 RandomAccessFile 的 close() 方法会间接地调用 channel 的 close 方法。

看下FileInputStream的close方法:

    public void close() throws IOException {
        synchronized (closeLock) {
            if (closed) {
                return;
            }
            closed = true;
        }
        if (channel != null) {
           channel.close();
        }

        fd.closeAll(new Closeable() {
            public void close() throws IOException {
               close0();
           }
        });
    }

1.4 FileChannel的位置

获取当前位置

long pos = channel.position();

设置当前位置

long newPos = ...;
channel.position(newPos);

如下获取文件channel:

        // 文件内容为10个字节的helloworld
        RandomAccessFile file = new RandomAccessFile("C:\\Users\\P50\\Desktop\\text.txt", "rw");
        FileChannel channel = file.getChannel();

打印不同设置时的位置:

        // 打印位置,没有读取时是0
        System.out.println(channel.position());

        // 读取后是文件的长度
        ByteBuffer buffer = ByteBuffer.allocate(10);
        channel.read(buffer);
        System.out.println(channel.position());

        // 设置位置后的长度
        FileChannel position = channel.position(5);
        System.out.println(position.position());

结果:

0
10
5

1.5 获取文件大小

channel.size();

二、channel的相互传输

channel提供两个用来channel相互传输数据的方法:

/**
  * 将一个channel的数据传输到target这个channel中,其中position,count,都是调用此方法的channel的
  * in.transferTo(0, in.size(), out);
  */
transferTo(long position, long count, WritableByteChannel target)
/**
  * 一个channel从src这个channel获取数据,其中position,count,都是src这个channel的
  * out.transferFrom(in,0,in.size());
  */
transferFrom(ReadableByteChannel src, long position, long count)

使用例子如下:

public class TestCopyFileByNIO {

    public static void fileChannelCopy(String sfPath, String tfPath) {
        FileInputStream fi = null;
        FileOutputStream fo = null;
        FileChannel in = null;
        FileChannel out = null;
        try {
            fi = new FileInputStream(new File(sfPath));
            fo = new FileOutputStream(new File(tfPath));
            in = fi.getChannel();//得到对应的文件通道
            out = fo.getChannel();//得到对应的文件通道
            in.transferTo(0, in.size(), out);//连接两个通道,并且从in通道读取,然后写入out通道
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                fi.close();
                fo.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        String sPath = "E:\\workspace\\comprehend-service.rar";
        String tPath = "E:\\workspace\\comprehend-service-" + System.currentTimeMillis() + "-bak.rar";
        fileChannelCopy(sPath, tPath);
        long end = System.currentTimeMillis();
        System.out.println("用时为:" + (end - start) + "ms");
    }
}

结果:

用时为:194ms

2.1 channel的最大传输值

channel的传输是有大小限制的,最大为2个g,超过会导致数据丢失。所以需要使用循环去多次传输数据。

public class TestCopyFileByNIO {

    public static void fileChannelCopy(String sfPath, String tfPath) {
        FileInputStream fi = null;
        FileOutputStream fo = null;
        FileChannel in;
        FileChannel out;
        try {
            fi = new FileInputStream(new File(sfPath));
            fo = new FileOutputStream(new File(tfPath));
            in = fi.getChannel();
            out = fo.getChannel();
            // 总文件大小
            long size = in.size();
            // left 剩余文件的数量
            for (long left = size; left > 0;){
                System.out.println("position = " + (size - left) + ",left = " + left);
                // transferTo返回传输的数量,剩余的减去传输的,就是当前剩余的数量
                left -= in.transferTo((size -left), left, out);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                fi.close();
                fo.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        String sPath = "E:\\workspace\\workspace.zip";
        String tPath = "E:\\workspace\\workspace-" + System.currentTimeMillis() + "-bak.zip";
        fileChannelCopy(sPath, tPath);
        long end = System.currentTimeMillis();
        System.out.println("用时为:" + (end - start) + "ms");
    }

结果:

position = 0,left = 2925330022
position = 2147483647,left = 777846375
用时为:13664ms

三、Path 和 Paths 类

jdk7 引入了 Path 和 Paths 类

// 相对路径 使用 user.dir 环境变量来定位 1.txt
Path source = Paths.get("1.txt"); 

// 绝对路径 代表了  d:\1.txt
Path source = Paths.get("d:\\1.txt"); 

// 绝对路径 同样代表了  d:\1.txt
Path source = Paths.get("d:/1.txt"); 

 // 代表了  d:\data\projects
Path projects = Paths.get("d:\\data", "projects");

例如目录结构如下

d:
    |- data
        |- projects
            |- a
            |- b

代码

Path path = Paths.get("d:\\data\\projects\\a\\..\\b");
System.out.println(path);
// 正常化路径
System.out.println(path.normalize()); 

会输出

d:\data\projects\a\..\b
d:\data\projects\b

四、Files类

检查文件是否存在

Path path = Paths.get("helloword/data.txt");
System.out.println(Files.exists(path));

创建一级目录

Path path = Paths.get("helloword/d1");
Files.createDirectory(path);

创建多级目录用

Path path = Paths.get("helloword/d1/d2");
Files.createDirectories(path);

拷贝文件

Path source = Paths.get("helloword/data.txt");
Path target = Paths.get("helloword/target.txt");

Files.copy(source, target);

如果希望用 source 覆盖掉 target,需要用 StandardCopyOption 来控制

Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);

移动文件

Path source = Paths.get("helloword/data.txt");
Path target = Paths.get("helloword/data.txt");

Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);

删除文件

Path target = Paths.get("helloword/target.txt");

Files.delete(target);

删除目录

Path target = Paths.get("helloword/d1");

Files.delete(target);

遍历目录文件

public static void main(String[] args) throws IOException {
    Path path = Paths.get("C:\\Program Files\\Java\\jdk1.8.0_91");
    AtomicInteger dirCount = new AtomicInteger();
    AtomicInteger fileCount = new AtomicInteger();
    Files.walkFileTree(path, new SimpleFileVisitor<Path>(){
        @Override
        public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) 
            throws IOException {
            System.out.println(dir);
            dirCount.incrementAndGet();
            return super.preVisitDirectory(dir, attrs);
        }

        @Override
        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) 
            throws IOException {
            System.out.println(file);
            fileCount.incrementAndGet();
            return super.visitFile(file, attrs);
        }
    });
    System.out.println(dirCount); // 133
    System.out.println(fileCount); // 1479
}

统计 jar 的数目

Path path = Paths.get("C:\\Program Files\\Java\\jdk1.8.0_91");
AtomicInteger fileCount = new AtomicInteger();
Files.walkFileTree(path, new SimpleFileVisitor<Path>(){
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) 
        throws IOException {
        if (file.toFile().getName().endsWith(".jar")) {
            fileCount.incrementAndGet();
        }
        return super.visitFile(file, attrs);
    }
});
System.out.println(fileCount); // 724

删除多级目录

Path path = Paths.get("d:\\a");
Files.walkFileTree(path, new SimpleFileVisitor<Path>(){
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) 
        throws IOException {
        Files.delete(file);
        return super.visitFile(file, attrs);
    }

    @Override
    public FileVisitResult postVisitDirectory(Path dir, IOException exc) 
        throws IOException {
        Files.delete(dir);
        return super.postVisitDirectory(dir, exc);
    }
});

删除是危险操作,确保要递归删除的文件夹没有重要内容

拷贝多级目录

long start = System.currentTimeMillis();
String source = "D:\\Snipaste-1.16.2-x64";
String target = "D:\\Snipaste-1.16.2-x64aaa";

Files.walk(Paths.get(source)).forEach(path -> {
    try {
        String targetName = path.toString().replace(source, target);
        // 是目录
        if (Files.isDirectory(path)) {
            Files.createDirectory(Paths.get(targetName));
        }
        // 是普通文件
        else if (Files.isRegularFile(path)) {
            Files.copy(path, Paths.get(targetName));
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
});
long end = System.currentTimeMillis();
System.out.println(end - start);

关于NIO文件编程此处就写到这了,有帮助的话朋友个点个赞

上一篇下一篇

猜你喜欢

热点阅读