User-Profile-Image
hankin
  • 5
  • Java
  • Kotlin
  • Spring
  • Web
  • SQL
  • MegaData
  • More
  • Experience
  • Enamiĝu al vi
  • 分类
    • Zuul
    • Zookeeper
    • XML
    • WebSocket
    • Web Notes
    • Web
    • Vue
    • Thymeleaf
    • SQL Server
    • SQL Notes
    • SQL
    • SpringSecurity
    • SpringMVC
    • SpringJPA
    • SpringCloud
    • SpringBoot
    • Spring Notes
    • Spring
    • Servlet
    • Ribbon
    • Redis
    • RabbitMQ
    • Python
    • PostgreSQL
    • OAuth2
    • NOSQL
    • Netty
    • MySQL
    • MyBatis
    • More
    • MinIO
    • MegaData
    • Maven
    • LoadBalancer
    • Kotlin Notes
    • Kotlin
    • Kafka
    • jQuery
    • JavaScript
    • Java Notes
    • Java
    • Hystrix
    • Git
    • Gateway
    • Freemarker
    • Feign
    • Eureka
    • ElasticSearch
    • Docker
    • Consul
    • Ajax
    • ActiveMQ
  • 页面
    • 归档
    • 摘要
    • 杂图
    • 问题随笔
  • 友链
    • Spring Cloud Alibaba
    • Spring Cloud Alibaba - 指南
    • Spring Cloud
    • Nacos
    • Docker
    • ElasticSearch
    • Kotlin中文版
    • Kotlin易百
    • KotlinWeb3
    • KotlinNhooo
    • 前端开源搜索
    • Ktorm ORM
    • Ktorm-KSP
    • Ebean ORM
    • Maven
    • 江南一点雨
    • 江南国际站
    • 设计模式
    • 熊猫大佬
    • java学习
    • kotlin函数查询
    • Istio 服务网格
    • istio
    • Ktor 异步 Web 框架
    • PostGis
    • kuangstudy
    • 源码地图
    • it教程吧
    • Arthas-JVM调优
    • Electron
    • bugstack虫洞栈
    • github大佬宝典
    • Sa-Token
    • 前端技术胖
    • bennyhuo-Kt大佬
    • Rickiyang博客
    • 李大辉大佬博客
    • KOIN
    • SQLDelight
    • Exposed-Kt-ORM
    • Javalin—Web 框架
    • http4k—HTTP包
    • 爱威尔大佬
    • 小土豆
    • 小胖哥安全框架
    • 负雪明烛刷题
    • Kotlin-FP-Arrow
    • Lua参考手册
    • 美团文章
    • Java 全栈知识体系
    • 尼恩架构师学习
    • 现代 JavaScript 教程
    • GO相关文档
    • Go学习导航
    • GoCN社区
    • GO极客兔兔-案例
    • 讯飞星火GPT
    • Hollis博客
    • PostgreSQL德哥
    • 优质博客推荐
    • 半兽人大佬
    • 系列教程
    • PostgreSQL文章
    • 云原生资料库
    • 并发博客大佬
Help?

Please contact us on our email for need any support

Support
    首页   ›   Java   ›   Java Notes   ›   正文
Java Notes

Java—IO读写文件的方式

2020-03-14 20:09:35
929  0 1
参考目录 隐藏
1) InputStream、OutputStream(字节流)
2) BufferedInputStream、BufferedOutputStream(缓存字节流)使用方式和字节流差不多,但是效率更高(推荐使用)
3) BufferedOutputStream和ByteArrayOutputStream区别
4) BufferedOutputStream
5) ByteArrayOutputStream
6) InputStreamReader、OutputStreamWriter(字节流,这种方式不建议使用,不能直接字节长度读写)。使用范围用做字符转换
7) BufferedReader、BufferedWriter(缓存流,提供readLine方法读取一行文本)
8) Reader、PrintWriter(PrintWriter这个很好用,在写数据的同事可以格式化)
9) Scanner
10) Files.lines (Java 8)
11) Files.readAllLines
12) Files.readString(JDK 11)
13) Files.readAllBytes()
14) 经典管道流的方式
15) Java7新增文件IO类
16) Paths
17) Path
18) Files
19) 遍历文件列表方法
20) Path和Files使用
21) FileTime对象
22) RandomAccessFile(大文件)
23) RandomAccessFile 使用
24) 大文件上传

阅读完需:约 45 分钟

Java语言的输入输出功能是十分强大而灵活的。

在Java类库中,IO部分的内容是很庞大的,因为它涉及的领域很广泛:标准输入输出,文件的操作,网络上的数据流,字符串流,对象流,zip文件流。

InputStream、OutputStream(字节流)

  //读取文件(字节流)
        InputStream in = new FileInputStream("d:\\1.txt");
        //写入相应的文件
        OutputStream out = new FileOutputStream("d:\\2.txt");
        //读取数据
        //一次性取多少字节
        byte[] bytes = new byte[2048];
        //接受读取的内容(n就代表的相关数据,只不过是数字的形式)
        int n = -1;
        //循环取出数据
        while ((n = in.read(bytes,0,bytes.length)) != -1) {
            //转换成字符串
            String str = new String(bytes,0,n,"GBK"); #这里可以实现字节到字符串的转换,比较实用
            System.out.println(str);
            //写入相关文件
            out.write(bytes, 0, n);
        }
        //关闭流
        in.close();
        out.close();

BufferedInputStream、BufferedOutputStream(缓存字节流)使用方式和字节流差不多,但是效率更高(推荐使用)

//读取文件(缓存字节流)
        BufferedInputStream in = new BufferedInputStream(new FileInputStream("d:\\1.txt"));
        //写入相应的文件
        BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream("d:\\2.txt"));
        //读取数据
        //一次性取多少字节
        byte[] bytes = new byte[2048];
        //接受读取的内容(n就代表的相关数据,只不过是数字的形式)
        int n = -1;
        //循环取出数据
        while ((n = in.read(bytes,0,bytes.length)) != -1) {
            //转换成字符串
            String str = new String(bytes,0,n,"GBK");
            System.out.println(str);
            //写入相关文件
            out.write(bytes, 0, n);
        }
        //清楚缓存
        out.flush();
        //关闭流
        in.close();
        out.close();

BufferedOutputStream和ByteArrayOutputStream区别

BufferedOutputStream是一个缓冲数据输出流接口, ByteArrayOutputStream则是字节数组输出流接口. 这2个输出流都是我们经常用到的, 它们都是OutputStream的子类

BufferedOutputStream

BufferedOutputStream会首先创建一个默认的容器量, capacity = 8192 = 8KB, 每次在写的时候都会去比对capacity是否还够用, 如果不够用的时候, 就flushBuffer(), 把buf中的数据写入对应的outputStream中, 然后将buf清空, 一直这样等到把内容写完. 在这过程中主要起到了一个数据缓冲的功能. 

public synchronized void write(byte b[], int off, int len) throws IOException {  
      // 在这判断需要写的数据长度是否已经超出容器的长度了,如果超出则直接写到相应的outputStream中,并清空缓冲区  
      if (len >= buf.length) {  
          flushBuffer();  
          out.write(b, off, len);  
          return;  
      }  
      // 判断缓冲区剩余的容量是否还够写入当前len的内容,如果不够则清空缓冲区  
      if (len > buf.length - count) {  
          flushBuffer();  
      }  
      // 将要写的数据先放入内存中,等待数据达到了缓冲区的长度后,再写到相应的outputStream中  
      System.arraycopy(b, off, buf, count, len);  
      count += len;  
    } 

flushBuffer () 

private void flushBuffer() throws IOException {  
       if (count > 0) {  
          // 把写入内存中的数据写到构造方法里传入的OutputStream句柄里, 并把容量大小清楚  
    out.write(buf, 0, count);  
    count = 0;  
       }  
   }  

这个类最重要的就是这2个方法, 这样节省了大量的内存空间, 合理的分配内存来完成数据输出

ByteArrayOutputStream

普通的OutputStream, 例如ByteArrayOutputStream也会首先创建一个默认的容器量, capacity = 32 = 32b, 每次在写的时候都会去比对capacity是否还够用, 如果不够用的时候, 就重新创建buf的容量, 一直等到内容写完, 这些数据都会一直处于内存中.  

public synchronized void write(byte b[], int off, int len) {  
      if ((off < 0) || (off > b.length) || (len < 0) ||  
            ((off + len) > b.length) || ((off + len) < 0)) {  
          throw new IndexOutOfBoundsException();  
      } else if (len == 0) {  
          return;  
      }  
        // 不断对自己的容量进行相加  
        int newcount = count + len;  
        // 如果新的容量大小已经超过了现有的大小时,则重新开辟新的内存区域来保存当前的数据  
        if (newcount > buf.length) {  
            buf = Arrays.copyOf(buf, Math.max(buf.length << 1, newcount));  
        }  
        System.arraycopy(b, off, buf, count, len);  
        count = newcount;  
    }  

总结 :

当你资源不足够用时,选择BufferedOutputStream是最佳的选择, 当你选择快速完成一个作业时,可以选择ByteArrayOutputStream之类的输出流 

ByteArrayOutputStream是将数据全部缓存到自身,然后一次性输出;而BufferedOutputStream是缓存一部分后,一次一次的输出。

另外,使用ByteArrayOutputStream、ByteArrayInputStream另一个好处是:当使用完他们缓存的字节流以后或者关闭它们之后,其中的字节流依然存在,可在此利用。


InputStreamReader、OutputStreamWriter(字节流,这种方式不建议使用,不能直接字节长度读写)。使用范围用做字符转换

  //读取文件(字节流)
        InputStreamReader in = new InputStreamReader(new FileInputStream("d:\\1.txt"),"GBK");
        //写入相应的文件
        OutputStreamWriter out = new OutputStreamWriter(new FileOutputStream("d:\\2.txt"));
        //读取数据
        //循环取出数据
        byte[] bytes = new byte[1024];
        int len = -1;
        while ((len = in.read()) != -1) {
            System.out.println(len);
            //写入相关文件
            out.write(len);
        }
        //清楚缓存
        out.flush();
        //关闭流
        in.close();
        out.close();

BufferedReader、BufferedWriter(缓存流,提供readLine方法读取一行文本)

 //读取文件(字符流)
        BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream("d:\\1.txt"),"GBK"));#这里主要是涉及中文
        //BufferedReader in = new BufferedReader(new FileReader("d:\\1.txt")));
        //写入相应的文件
        BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("d:\\2.txt"),"GBK"));
        //BufferedWriter out = new BufferedWriter(new FileWriter("d:\\2.txt"));
        //读取数据
        //循环取出数据
        String str = null;
        while ((str = in.readLine()) != null) {
            System.out.println(str);
            //写入相关文件
            out.write(str);
            out.newLine();
        }
        
        //清楚缓存
        out.flush();
        //关闭流
        in.close();
        out.close();

Reader、PrintWriter(PrintWriter这个很好用,在写数据的同事可以格式化)

   //读取文件(字节流)
        Reader in = new InputStreamReader(new FileInputStream("d:\\1.txt"),"GBK");
        //写入相应的文件
        PrintWriter out = new PrintWriter(new FileWriter("d:\\2.txt"));
        //读取数据
        //循环取出数据
        byte[] bytes = new byte[1024];
        int len = -1;
        while ((len = in.read()) != -1) {
            System.out.println(len);
            //写入相关文件
            out.write(len);
        }
        //清楚缓存
        out.flush();
        //关闭流
        in.close();
        out.close();

基本的几种用法就这么多,当然每一个读写的使用都是可以分开的。为了更好的来使用io。流里面的读写,建议使用BufferedInputStream、BufferedOutputStream


Scanner

Scanner,从JDK1.5开始提供的API,特点是可以按行读取、按分割符去读取文件数据,既可以读取String类型,也可以读取Int类型、Long类型等基础数据类型的数据。

@Test
void testReadFile1() throws IOException {
   //文件内容:Hello World|Hello Zimug
   String fileName = "D:\\data\\test\\newFile4.txt";

   try (Scanner sc = new Scanner(new FileReader(fileName))) {
      while (sc.hasNextLine()) {  //按行读取字符串
         String line = sc.nextLine();
         System.out.println(line);
      }
   }

   try (Scanner sc = new Scanner(new FileReader(fileName))) {
      sc.useDelimiter("\\|");  //分隔符
      while (sc.hasNext()) {   //按分隔符读取字符串
         String str = sc.next();
         System.out.println(str);
      }
   }

   //sc.hasNextInt() 、hasNextFloat() 、基础数据类型等等等等。
   //文件内容:1|2
   fileName = "D:\\data\\test\\newFile5.txt";
   try (Scanner sc = new Scanner(new FileReader(fileName))) {
      sc.useDelimiter("\\|");  //分隔符
      while (sc.hasNextInt()) {   //按分隔符读取Int
          int intValue = sc.nextInt();
         System.out.println(intValue);
      }
   }
}

Files.lines (Java 8)

如果你是需要按行去处理数据文件的内容,这种方式是我推荐大家去使用的一种方式,代码简洁,使用java 8的Stream流将文件读取与文件处理有机融合。

@Test
void testReadFile2() throws IOException {
   String fileName = "D:\\data\\test\\newFile.txt";

   // 读取文件内容到Stream流中,按行读取
   Stream<String> lines = Files.lines(Paths.get(fileName));

   // 随机行顺序进行数据处理
   lines.forEach(ele -> {
      System.out.println(ele);
   });
}

forEach获取Stream流中的行数据不能保证顺序,但速度快。如果你想按顺序去处理文件中的行数据,可以使用forEachOrdered,但处理效率会下降。

// 按文件行顺序进行处理
lines.forEachOrdered(System.out::println);

或者利用CPU多和的能力,进行数据的并行处理parallel(),适合比较大的文件。

// 按文件行顺序进行处理
lines.parallel().forEachOrdered(System.out::println);

也可以把Stream<String>转换成List<String>,但是要注意这意味着你要将所有的数据一次性加载到内存,要注意java.lang.OutOfMemoryError

// 转换成List<String>, 要注意java.lang.OutOfMemoryError: Java heap space
List<String> collect = lines.collect(Collectors.toList());

Files.readAllLines

这种方法仍然是java8 为我们提供的,如果我们不需要Stream<String>,我们想直接按行读取文件获取到一个List<String>,就采用下面的方法。同样的问题:这意味着你要将所有的数据一次性加载到内存,要注意java.lang.OutOfMemoryError

@Test
void testReadFile3() throws IOException {
   String fileName = "D:\\data\\test\\newFile3.txt";

   // 转换成List<String>, 要注意java.lang.OutOfMemoryError: Java heap space
   List<String> lines = Files.readAllLines(Paths.get(fileName),
               StandardCharsets.UTF_8);
   lines.forEach(System.out::println);

}

Files.readString(JDK 11)

从 java11开始,为我们提供了一次性读取一个文件的方法。文件不能超过2G,同时要注意你的服务器及JVM内存。这种方法适合快速读取小文本文件。

@Test
void testReadFile4() throws IOException {
   String fileName = "D:\\data\\test\\newFile3.txt";

   // java 11 开始提供的方法,读取文件不能超过2G,与你的内存息息相关
   //String s = Files.readString(Paths.get(fileName));
}

Files.readAllBytes()

如果你没有JDK11(readAllBytes()始于JDK7),仍然想一次性的快速读取一个文件的内容转为String,该怎么办?先将数据读取为二进制数组,然后转换成String内容。这种方法适合在没有JDK11的请开给你下,快速读取小文本文件。

@Test
void testReadFile5() throws IOException {
   String fileName = "D:\\data\\test\\newFile3.txt";

   //如果是JDK11用上面的方法,如果不是用这个方法也很容易
   byte[] bytes = Files.readAllBytes(Paths.get(fileName));

   String content = new String(bytes, StandardCharsets.UTF_8);
   System.out.println(content);
}

经典管道流的方式

@Test
void testReadFile6() throws IOException {
   String fileName = "D:\\data\\test\\newFile3.txt";

   // 带缓冲的流读取,默认缓冲区8k
   try (BufferedReader br = new BufferedReader(new FileReader(fileName))){
      String line;
      while ((line = br.readLine()) != null) {
         System.out.println(line);
      }
   }

   //java 8中这样写也可以
   try (BufferedReader br = Files.newBufferedReader(Paths.get(fileName))){
      String line;
      while ((line = br.readLine()) != null) {
         System.out.println(line);
      }
   }

}

这种方式可以通过管道流嵌套的方式,组合使用,比较灵活。比如我们
想从文件中读取java Object就可以使用下面的代码,前提是文件中的数据是ObjectOutputStream写入的数据,才可以用ObjectInputStream来读取。

try (FileInputStream fis = new FileInputStream(fileName);
     ObjectInputStream ois = new ObjectInputStream(fis)){
   ois.readObject();
} 

Java7新增文件IO类

在java中文件或是目录习惯用java.io.File对象来表示,但是File类有很多缺陷

它的很多方法不能抛出异常

它的delete方法经常莫名其妙的失败等,旧的File类经常是程序失败的根源。

因此在Java7中有了更好的替代:java.nio.file.Path及java.nio.file.Files。

Path接口的名字非常恰当,就是表示路径的,API中讲Path对象可以是一个文件,一个目录,或是一个符号链接,也可以是一个根目录。 用法很简单。创建Path并不会创建物理文件或是目录,path实例经常引用并不存在的物理对象,要真正创建文件或是目录,需要用到Files类。

Files类是一个非常强大的类,它提供了处理文件和目录以及读取文件和写入文件的静态方法。 可以用它创建和删除路径。复制文件。检查路径是否存在等。 此外。Files还拥有创建流对象的方法。

Paths

Paths类仅由静态方法组成,通过转换路径字符串返回Path或URI。

  • 因为就两个方法用来生成Path对象,以供Path和Files使用;而Path也经常由Paths来生成,又或者File类有一个toPath();方法可以使用

创建Paths

static Path get(String first, String... more) 
//将路径字符串或连接到路径字符串的字符串序列转换为 Path,可以get("c:/abc");或者get("c:","abc"),注意这里可以有多个参数String... more代表n个参数,这个比较常用
static Path get(URI uri) 
//将给定的URI转换为Path对象

Path

Path就是取代File的,用于来表示文件路径和文件。可以有多种方法来构造一个Path对象来表示一个文件路径,或者一个文件:
– 该接口的实现是不可变且安全的,可供多个并行线程使用。

创建Path

Path toPath() 
//File类对象方法--返回一个java.nio.file.Path对象
abstract Path getPath(String first, String... more) 
//FileSystem对象方法--将路径字符串或从路径字符串连接起来的一系列字符串转换为 Path 。  

创建Path的三种方式

Path path=FileSystems.getDefault().getPath("d:/users/日记5.txt");    
//并没有实际创建路径,而是一个指向d:/users/日记5.txt路径的引用

Path path=Paths.get("d:/users/日记5.txt");                                              //Paths类提供了这个快捷方法,直接通过它的静态get方法创建path

Path path= = new File("d:/users/日记5.txt").toPath();

Path常用方法

Path接口没什么判断方法,其实更多的判断和操作都在Files工具类里面

boolean isAbsolute() 
//告诉这条路是否是绝对的
boolean endsWith(Path other) 
//测试此路径是否以给定的路径结束
boolean endsWith(String other) 
//测试此路径是否以给定字符串结束,如"c:/a/banana/cat"可以以"/banana/cat"结尾,但不能以"t"结尾
boolean startsWith(Path other) 
//测试此路径是否以给定的路径开始。  
boolean startsWith(String other) 
//测试此路径是否以给定字符串开始,跟上面一样规律

Path getFileName() 
//将此路径表示的文件或目录的名称返回为 Path对象,文件名或文件夹名,不含路径
Path getName(int index) 
//返回此路径的名称元素作为 Path对象。目录中最靠近root的为0,最远的为(count-1),count由下面的方法获得
int getNameCount() 
//返回路径中的名称元素的数量。0则只有root
Path getParent() 
//返回 父路径,如果此路径没有父返回null,如/a/b/c返回/a/b,配合下面的方法消除"."或".."
Path normalize()
//返回一个路径,该路径是冗余名称元素的消除。如消除掉"."、".."
Path getRoot() 
//返回此路径的根组分作为 Path对象,或 null如果该路径不具有根组件。如返回"c:/"
Path relativize(Path other) 
//构造此路径和给定路径之间的相对路径。有点难理解,p1-"Topic.txt",p2-"Demo.txt",p3-"/Java/JavaFX/Topic.txt",p4-"/Java/2011";;那么p1和p2的结果是"../Demo.txt";;p2和p1的结果是"../Topic.txt";;p3和p4的结果是"../../2011";;p4和p3的结果是"../JavaFX/Topic.txt"
Path resolve(String other)
//将给定的路径字符串转换为 Path。如"c:/a/b"和字符串"c.txt"的结果是"c:/a/b/c.txt";更像是拼接
Path resolveSibling(String other) 
//将给定的路径字符串转换为 Path。如"c:/a/b.txt"和字符串"c.txt"的结果是"c:/a/c.txt";更像是替换
Path subpath(int beginIndex, int endIndex) 
//返回一个相对的 Path ,它是该路径的名称元素的子序列,如"d:/a/b/c.txt"参数为(1,3)返回一个"b/c.txt"
Path toAbsolutePath() 
//返回表示此路径的绝对路径的 Path对象。包括盘符和文件名或文件夹名

Iterator<Path> iterator() 
//返回此路径的名称元素的迭代器。"c:/a/b/c.txt"的迭代器可以next出以下"a""b""c.txt"
File toFile() 
//返回表示此路径的File对象

Files

Files类只包含对文件,目录或其他类型文件进行操作的静态方法。主要和Path接口的对象进行配合使用

判断方法:

static boolean exists(Path path, LinkOption... options) 
//测试文件是否存在。  
static boolean notExists(Path path, LinkOption... options) 
//测试此路径所在的文件是否不存在。  
static boolean isDirectory(Path path, LinkOption... options) 
//测试文件是否是目录。  
static boolean isExecutable(Path path) 
//测试文件是否可执行。  
static boolean isHidden(Path path) 
//告知文件是否被 隐藏 。  
static boolean isReadable(Path path) 
//测试文件是否可读。  
static boolean isRegularFile(Path path, LinkOption... options) 
//测试文件是否是具有不透明内容的常规文件。说实话,我也不太懂常规文件指的是啥
static boolean isSameFile(Path path, Path path2) 
//测试两个路径是否找到相同的文件。
static boolean isSymbolicLink(Path path) 
//测试文件是否是符号链接。//
static boolean isWritable(Path path) 
//测试文件是否可写。  

删除方法

static boolean deleteIfExists(Path path) 
//删除文件(如果存在)。  
static void delete(Path path) 
//删除文件。  

复制方法

static long copy(InputStream in, Path target, CopyOption... options) 
//将输入流中的所有字节复制到文件。
//关于CopyOption则是一个被继承的接口主要有枚举类StandardCopyOption和LinkOption
//   1.StandardCopyOption
//			REPLACE_EXISTING(也就是替换覆盖)
//          COPY_ATTRIBUTES(将源文件的文件属性信息复制到目标文件中)
//			ATOMIC_MOVE(原子性的复制)都是字面意思
//   2.LinkOption
//			NOFOLLOW_LINKS
static long copy(Path source, OutputStream out) 
//将文件中的所有字节复制到输出流。  
static Path copy(Path source, Path target, CopyOption... options) 
//将文件复制到目标文件。  

移动和重命名方法

static Path move(Path source, Path target, CopyOption... options) 
//将文件移动或重命名为目标文件。 

创建文件和文件夹方法

static Path createDirectories(Path dir, FileAttribute<?>... attrs) 
//首先创建所有不存在的父目录来创建目录。
static Path createDirectory(Path dir, FileAttribute<?>... attrs) 
//创建一个新的目录。  
static Path createFile(Path path, FileAttribute<?>... attrs) 
//创建一个新的和空的文件,如果该文件已存在失败。

文件属性方法

static <V extends FileAttributeView> V getFileAttributeView(Path path, 类<V> type, LinkOption... options) 
//返回给定类型的文件属性视图。指定六个视图其中一种,上面一开始有点到。拿到的xxxAttributeView会有一个跟下面一样名字的readAttributes方法来得到一个xxxAttributes真正的获取操作就全是在这个xxxAttributes类的对象里get啦
static <A extends BasicFileAttributes> A readAttributes(Path path, 类<A> type, LinkOption... options) 
//读取文件的属性作为批量操作。指定一个xxxAttributes,得到一个实例,通过里面的方法得到时间等基本属性

static Object getAttribute(Path path, String attribute, LinkOption... options) 
//读取文件属性的值。这个 String attributes 参数的语法固定是以 view-name:comma-separated-attributes 的形式;view-name指定视图名如basic,posix,acl等,不写默认为basic;有写默认要加":";可以用"basic:*"或"*"读取所有,又或者用"basic:size,lastModifiedTime"读取大小和修改时间。具体还有那些属性可以看具体指定的类,比如basic视图就看BasicFileAttributes这个接口都有哪些方法,可以读取哪些文件属性。同理,下面的 String attributes 一样是这个理
static Map<String,Object> readAttributes(Path path, String attributes, LinkOption... options) 
//读取一组文件属性作为批量操作。
static Path setAttribute(Path path, String attribute, Object value, LinkOption... options) 
//设置文件属性的值。  

/* 下面这些也是获取属性的方法,不过还没研究到是怎么用的 */
static FileTime getLastModifiedTime(Path path, LinkOption... options) 
//返回文件的上次修改时间。  
static UserPrincipal getOwner(Path path, LinkOption... options) 
//返回文件的所有者。  
static Set<PosixFilePermission> getPosixFilePermissions(Path path, LinkOption... options) 
//返回文件的POSIX文件权限。  
static Path setLastModifiedTime(Path path, FileTime time) 
//更新文件上次修改的时间属性。  
static Path setOwner(Path path, UserPrincipal owner) 
//更新文件所有者。  
static Path setPosixFilePermissions(Path path, Set<PosixFilePermission> perms) 
//设置文件的POSIX权限。  
static long size(Path path) 
//返回文件的大小(以字节为单位)。  

读取、编辑文件内容方法

static BufferedReader newBufferedReader(Path path) 
//打开一个文件进行阅读,返回一个 BufferedReader以高效的方式从文件读取文本。  
static BufferedReader newBufferedReader(Path path, Charset cs) 
//打开一个文件进行阅读,返回一个 BufferedReader ,可以用来以有效的方式从文件读取文本。  
static BufferedWriter newBufferedWriter(Path path, Charset cs, OpenOption... options) 
//打开或创建一个写入文件,返回一个 BufferedWriter ,可以用来以有效的方式将文本写入文件。  
static BufferedWriter newBufferedWriter(Path path, OpenOption... options) 
//打开或创建一个写入文件,返回一个 BufferedWriter以高效的方式写入文件。  
static SeekableByteChannel newByteChannel(Path path, OpenOption... options) 
//打开或创建文件,返回可访问的字节通道以访问该文件。  
static SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) 
//打开或创建文件,返回可访问的字节通道以访问该文件。  
static InputStream newInputStream(Path path, OpenOption... options) 
//打开一个文件,返回输入流以从文件中读取。  
static OutputStream newOutputStream(Path path, OpenOption... options) 
//打开或创建文件,返回可用于向文件写入字节的输出流。  
static byte[] readAllBytes(Path path) 
//读取文件中的所有字节。  
static List<String> readAllLines(Path path) 
//从文件中读取所有行。  
static List<String> readAllLines(Path path, Charset cs) 
//从文件中读取所有行。
static Path write(Path path, byte[] bytes, OpenOption... options) 
//将字节写入文件。  
static Path write(Path path, Iterable<? extends CharSequence> lines, Charset cs, OpenOption... options) 
//将文本行写入文件。  
static Path write(Path path, Iterable<? extends CharSequence> lines, OpenOption... options) 
//将文本行写入文件。  

以上方法适用于处理中等长度的文本文件,如果要处理的文件长度比较大,或者是二进制文件,那么还是应该使用流

遍历文件列表方法

  • newDirectoryStream只是遍历当前Path的子目录列表,或者写一个方法里面递归调用实现遍历到底;
  • walk则是可以通过maxDepth参数来决定遍历的深度,后面的FileVisitOption参数可有可无;
  • list类似于newDirectoryStream,区别是walk和newDirectoryStream是递归的,list是非递归的
static DirectoryStream<Path> newDirectoryStream(Path dir) 
//打开一个目录,返回一个DirectoryStream以遍历目录中的所有条目。最好用 try-with-resources 构造,可以自动关闭资源。返回的 DirectoryStream<Path> 其实可以直接使用 Iterator或者for循环 遍历每一个 dir 下面的文件或目录
static DirectoryStream<Path> newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter) 
//上面方法的重载,通过实现参数二(有一个 boolean accept(Path p) 方法来判断文件是否符合需要)来达到过滤的目的。如accept方法中写"return (Files.size(p) > 8192L);"来匹配大于8k的文件
static DirectoryStream<Path> newDirectoryStream(Path dir, String glob) 
//上面方法的重载,可以通过参数二作为过滤匹配出对应的文件。如 newDirectoryStream(dir, "*.java") 用于遍历目录里所有java后缀的文件

static Stream<Path> walk(Path start, FileVisitOption... options) 
//深度优先遍历。返回一个 Stream ,它通过 Path根据给定的起始文件的文件树懒惰地填充 Path 。  
static Stream<Path> walk(Path start, int maxDepth, FileVisitOption... options) 
//深度优先遍历。返回一个 Stream ,它是通过走根据给定的起始文件的文件树懒惰地填充 Path 。

static Stream<Path> list(Path dir) 
//返回一个懒惰的填充 Stream ,其元素是 Stream中的条目。返回的 Stream 里封装了一个 DirectoryStream 用于遍历。

Path和Files使用

import org.junit.Test;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFilePermission;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;

public class PathAndFilesTest {
    /**
     * 创建Path
     */
    @Test
    public void createPath() throws URISyntaxException, MalformedURLException {
        //1.Paths
        Path path = Paths.get("F:/测试数据.csv");
        System.out.println(path.getFileName());
        Path path1 = Paths.get(new URI("file:///f:/测试数据.csv"));

        //2.FileSystems
        Path path2 = FileSystems.getDefault().getPath("F:/测试数据.csv");

        //3.File
        Path path3 = new File("F:/测试数据.csv").toPath();
    }


    /**
     * 创建一个空文件/文件夹
     */
    @Test
    public void create() throws IOException {
        //文件夹
        Path path = Paths.get("F:/hello");
        if (!Files.exists(path)) {//如果不存在
            Files.createDirectory(path);
            //创建多个目录
            //Files.createDirectories(path);
        }

        //文件
        Path path1 = Paths.get("F:/helloFile.txt");
        if (!Files.exists(path1)) {//如果不存在
            Files.createFile(path1);
        }
    }

    /**
     * 文件属性
     */
    @Test
    public void getFileProperties() throws IOException {
        Path path = Paths.get("F:/测试数据.csv");
        System.out.println(Files.getLastModifiedTime(path));//最后修改时间:2019-05-22T02:52:45.625094Z
        System.out.println(Files.getOwner(path));//拥有者:DESKTOP-GE36VVD\87772 (User)
        //System.out.println(Files.getPosixFilePermissions(path));//权限,非admin可能会报错
        System.out.println(Files.size(path));//文件大小: 34207517
    }

    /**
     * 读取一个文本文件
     */
    @Test
    public void readText() throws IOException {
        Path path = Paths.get("F:/test.txt");
        //通过bufferedReader读取
        BufferedReader bufferedReader = Files.newBufferedReader(path, StandardCharsets.UTF_8);///该文件编码是什么newBufferedReader就必须指定什么字符集,否则报错

        StringBuilder sb = new StringBuilder();
        String tempString = null;
        while ((tempString = bufferedReader.readLine()) != null) {
            sb = sb.append(tempString + "\n");
        }
        System.out.println(sb);


        //通过Files方法readAllLines
        List<String> strings = Files.readAllLines(path);
        strings.forEach(System.out::println);
    }

    /**
     * 拿到文件输入流
     *
     * @throws IOException
     */
    @Test
    public void getInputStream() throws IOException {
        Path path = Paths.get("F:/test.txt");
        InputStream inputStream = Files.newInputStream(path);

        //转换字符流后在包装成缓冲流
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));

        StringBuilder sb = new StringBuilder();
        String tempString = null;
        while ((tempString = bufferedReader.readLine()) != null) {
            sb = sb.append(tempString + "\n");
        }
        System.out.println(sb);
    }

    /**
     * 文件写操作
     */
    @Test
    public void writeFile() throws IOException {
        Path path = Paths.get("F:/writeFile.txt");

        //获取写入流
        BufferedWriter bufferedWriter = Files.newBufferedWriter(path);

        //执行写入操作
        String str = "write file test";
        bufferedWriter.write(str);

        //关闭资源
        bufferedWriter.flush();
        bufferedWriter.close();
    }

    /**
     * 遍历一个文件夹
     */
    @Test
    public void traverseDirectory() throws IOException {
        Path path = Paths.get("F:/test");
        Stream<Path> list = Files.list(path);
        list.forEach(p -> {
            System.out.println(p.getFileName());
        });
    }

    /**
     * 遍历文件树
     */
    @Test
    public void traverseTree() throws IOException {
        Path path = Paths.get("F:/test/");
        Stream<Path> walk = Files.walk(path);
        walk.forEach(path1 -> {
//      System.out.println(path1.getRoot());//根目录
            System.out.println(path1.getFileName());//文件名
//      System.out.println(path1.getParent());//上级目录
//      System.out.println(path1.getFileSystem());//文件系统
        });
        //还有种方式Files.walkFileTree()
    }

    /**
     * 文件复制
     */
    @Test
    public void copyFile() throws IOException {
        Path src = Paths.get("F:/测试数据.csv");
        Path dest = Paths.get("F:/test/Copy测试数据.csv");
        Files.copy(src, dest);
    }

    /**
     * 读取权限见上面示例,设置权限
     */
    @Test
    public void writePermission() throws IOException {
        Path path = Paths.get("F:/test/导出测试数据.xlsx");
        Set<PosixFilePermission> permissionSet = new HashSet<>();
        permissionSet.add(PosixFilePermission.GROUP_WRITE);
        permissionSet.add(PosixFilePermission.OWNER_EXECUTE);
        Files.setPosixFilePermissions(path, permissionSet);
    }

/**
	 * 判断方法
	 * @throws IOException
	 */
	@Test
	public void judge() throws IOException {
		Path path1 = Paths.get("f:\\test", "Copy测试数据.csv");
		Path path2 = Paths.get("f:\\测试数据.csv");
//		boolean exists(Path path, LinkOption … opts) : 判断文件是否存在
		System.out.println(Files.exists(path2, LinkOption.NOFOLLOW_LINKS));//true

//		boolean isDirectory(Path path, LinkOption … opts) : 判断是否是目录
		//不要求此path对应的物理文件存在。
		System.out.println(Files.isDirectory(path1, LinkOption.NOFOLLOW_LINKS));//false

//		boolean isRegularFile(Path path, LinkOption … opts) : 判断是否是文件

//		boolean isHidden(Path path) : 判断是否是隐藏文件
		//要求此path对应的物理上的文件需要存在。才可判断是否隐藏。否则,抛异常。
//		System.out.println(Files.isHidden(path1));

//		boolean isReadable(Path path) : 判断文件是否可读
		System.out.println(Files.isReadable(path1));//true
//		boolean isWritable(Path path) : 判断文件是否可写
		System.out.println(Files.isWritable(path1));//true
//		boolean notExists(Path path, LinkOption … opts) : 判断文件是否不存在
		System.out.println(Files.notExists(path1, LinkOption.NOFOLLOW_LINKS));//false
	}
}


	/**
	 * StandardOpenOption.READ:表示对应的Channel是可读的。
	 * StandardOpenOption.WRITE:表示对应的Channel是可写的。
	 * StandardOpenOption.CREATE:如果要写出的文件不存在,则创建。如果存在,忽略
	 * StandardOpenOption.CREATE_NEW:如果要写出的文件不存在,则创建。如果存在,抛异常
	 */
	@Test
	public void ioStream() throws IOException {
		Path path1 = Paths.get("f:\\test", "copyTest.txt");

//		InputStream newInputStream(Path path, OpenOption…how):获取 InputStream 对象
		InputStream inputStream = Files.newInputStream(path1, StandardOpenOption.READ);

//		OutputStream newOutputStream(Path path, OpenOption…how) : 获取 OutputStream 对象
		OutputStream outputStream = Files.newOutputStream(path1, StandardOpenOption.WRITE, StandardOpenOption.CREATE);


//		SeekableByteChannel newByteChannel(Path path, OpenOption…how) : 获取与指定文件的连接,how 指定打开方式。
		SeekableByteChannel channel = Files.newByteChannel(path1, StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE);

//		DirectoryStream<Path>  newDirectoryStream(Path path) : 打开 path 指定的目录
		Path path2 = Paths.get("f:\\test");
		DirectoryStream<Path> directoryStream = Files.newDirectoryStream(path2);
		Iterator<Path> iterator = directoryStream.iterator();
		while (iterator.hasNext()) {
			System.out.println(iterator.next());
		}
	}

FileTime对象

表示文件时间戳属性的值,可能会在设置文件最后更新属性时使用到

static FileTime fromMillis(long value) 
//返回一个 FileTime以 FileTime单位表示给定值。  
long toMillis() 
//返回以毫秒为单位的值。
String toString() 
//返回此 FileTime的字符串表示 FileTime 。  

例子:

/**
 * 可能你要从文件属性中的FileTime或者到一个Date对象
 */
Path pathObj = Paths.get("C:/a/b/c.txt");
BasicFileAttributes attrs = Files.readAttributes(pathObj, BasicFileAttributes.class);
Data date = new Date(attrs.lastModifiedTime().toMillis());

/**
 * 又或者可能你要人为地修改这个文件时间属性,需要一个FileTime
 */
Path path = Paths.get("C:/a/b/c.txt");
long time = System.currentTimeMillis();
FileTime fileTime = FileTime.fromMillis(time);
try{
  Files.setAttribute(path, "basic:lastModifiedTime", fileTime,LinkOption.NOFOLLOW_LINKS);
}catch (IOException e) {
    System.err.println(e);
}

RandomAccessFile(大文件)

RandomAccessFile既可以读取文件内容,也可以向文件输出数据。同时,RandomAccessFile支持“随机访问”的方式,程序快可以直接跳转到文件的任意地方来读写数据。

由于RandomAccessFile可以自由访问文件的任意位置,所以如果需要访问文件的部分内容,而不是把文件从头读到尾,使用RandomAccessFile将是更好的选择。

与OutputStream、Writer等输出流不同的是,RandomAccessFile允许自由定义文件记录指针,RandomAccessFile可以不从开始的地方开始输出,因此RandomAccessFile可以向已存在的文件后追加内容。如果程序需要向已存在的文件后追加内容,则应该使用RandomAccessFile。

RandomAccessFile的方法虽然多,但它有一个最大的局限,就是只能读写文件,不能读写其他IO节点。

RandomAccessFile 使用

RandomAccessFile raf = new RandomAccessFile(文件,r);
RandomAccessFile raf = new RandomAccessFile(文件,rw);

RandomAccessFile类有两个构造函数,其实这两个构造函数基本相同,只不过是指定文件的形式不同——一个需要使用String参数来指定文件名,一个使用File参数来指定文件本身。除此之外,创建RandomAccessFile对象时还需要指定一个mode参数,该参数指定RandomAccessFile的访问模式,一共有4种模式。

  1. “r” : 以只读方式打开。调用结果对象的任何 write 方法都将导致抛出 IOException。 
  2. “rw”: 打开以便读取和写入。 
  3. “rws”: 打开以便读取和写入。相对于 “rw”,”rws” 还要求对“文件的内容”或“元数据”的每个更新都同步写入到基础存储设备。 
  4. “rwd” : 打开以便读取和写入,相对于 “rw”,”rwd” 还要求对“文件的内容”的每个更新都同步写入到基础存储设备。

RandomAccessFile既可以读文件,也可以写文件,所以类似于InputStream的read()方法,以及类似于OutputStream的write()方法,RandomAccessFile都具备。除此之外,RandomAccessFile具备两个特有的方法,来支持其随机访问的特性。

RandomAccessFile对象包含了一个记录指针,用以标识当前读写处的位置,当程序新创建一个RandomAccessFile对象时,该对象的文件指针记录位于文件头(也就是0处),当读/写了n个字节后,文件记录指针将会后移n个字节。除此之外,RandomAccessFile还可以自由移动该记录指针。下面就是RandomAccessFile具有的两个特殊方法,来操作记录指针,实现随机访问:

long getFilePointer( ):返回文件记录指针的当前位置 void seek(long pos ):将文件指针定位到pos位置

大文件上传

后端:

上传的文件存储信息

FileInfo 实体类

/**
 * 上传的文件存储信息
 * @author xujiahui
 */
class FileInfo {

    /** 文件的哈希值,或者MD5值 */
    var hash:String?=null


    /**  "文件的名称" */
    var  name:String?=null

    /** "文件类型" */
    var  type:String?=null

    /** "文件上传路径" */
    var  path:String?=null

    /** "文件创建时间" */
    var  createTime:Long?=null

}

上传文件需要用的基本参数

UploadFileParam 实体类

import org.springframework.web.multipart.MultipartFile;

/**
 * 上传文件需要用的基本参数
 * @author xujiahui
 */
class UploadFileParam {

    /**
     * "任务ID"
     */
    var id: String?=null

    /**
     * 总分片数量
     */
    var chunks: Int?=null

    /**
     * 当前为第几块分片(第一个块是 0,注意不是从 1 开始的)
     */
    var chunk: Int?=null

    /**
     * 当前分片大小
     */
    var size: Long = 0L

    /**
     * 当前文件名称
     */
    var name: String?=null

    /**
     * 当前文件的分片对象
     */
    var file: MultipartFile?=null

    /**
     * 当前文件的MD5,不是分片的
     */
    var md5: String?=null

}

用于根据文件确定文件的mimeType的实用工具类

MimeTypes 工具类

import cn.hutool.core.io.FileTypeUtil;
import cn.hutool.core.util.StrUtil;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

import java.io.*;
import java.util.HashMap;
import java.util.StringTokenizer;

/**
 * <p>用于根据文件确定文件的mimeType的实用工具类</p>
 * <p>2020-12-20 13:02</p>
 **/
@Slf4j
public class MimeTypes {

    /**
     * The default MIME type
     */
    private static final String DEFAULT_MIMETYPE = "application/octet-stream";

    /**
     * 当前实例[单例]
     */
    private static MimeTypes mimetypes = null;

    /**
     * 对mime类型的映射
     */
    private final HashMap<String, String> extensionToMimetypeMap = new HashMap<>();

    /**
     * 禁止实例化
     */
    private MimeTypes() {
    }

    /**
     * 获取 MimeTypes 实例
     */
    @SneakyThrows
    public synchronized static MimeTypes getInstance() {
        if (mimetypes != null) {
            return mimetypes;
        }

        mimetypes = new MimeTypes();
        try (InputStream is = mimetypes.getClass().getResourceAsStream("/mime.types")) {
            if (is != null) {
                // 加载文件 mime.types
                log.debug("Loading mime types from file in the classpath: mime.types");
                mimetypes.loadMimeTypes(is);
            } else {
                // 找不到文件 mime.types
                log.warn("Unable to find 'mime.types' file in classpath");
            }
        }

        return mimetypes;
    }

    /**
     * 自定义需要加载的 mime.types
     *
     * @param is 包含有mime类型的流
     */
    @SneakyThrows
    public void loadMimeTypes(InputStream is) {
        BufferedReader br = new BufferedReader(new InputStreamReader(is));
        String line;

        while ((line = br.readLine()) != null) {
            line = line.trim();

            // 忽略注释和空行
            if (line.startsWith("#") || line.length() == 0) {
                continue;
            }

            StringTokenizer st = new StringTokenizer(line, " \t");
            if (st.countTokens() <= 1) {
                continue;
            }

            String extension = st.nextToken();
            if (st.hasMoreTokens()) {
                String mimetype = st.nextToken();
                extensionToMimetypeMap.put(extension.toLowerCase(), mimetype);
            }
        }
    }

    /**
     * 根据文件名获取文件 mime 类型
     *
     * @param fileName 文件名
     * @return 返回文件对应的mime类型
     */
    public String getMimeTypes(String fileName) {
        int lastPeriodIndex = fileName.lastIndexOf(".");
        if (lastPeriodIndex > 0 && lastPeriodIndex + 1 < fileName.length()) {
            String ext = fileName.substring(lastPeriodIndex + 1).toLowerCase();
            if (extensionToMimetypeMap.containsKey(ext)) {
                return extensionToMimetypeMap.get(ext);
            }
        }

        return DEFAULT_MIMETYPE;
    }

    /**
     * 获取当前文件对应的 mime 类型
     *
     * @param file {@link File} 需要获取的文件
     * @return 返回文件对应的mime类型
     */
    public String getMimeTypes(File file) {
        if (!file.exists()) {
            return DEFAULT_MIMETYPE;
        }

        // 获取当前文件的扩展名
        String ext = FileTypeUtil.getType(file);
        if (StrUtil.isBlank(ext)) {
            return DEFAULT_MIMETYPE;
        }

        if (extensionToMimetypeMap.containsKey(ext.toLowerCase())) {
            return extensionToMimetypeMap.get(ext.toLowerCase());
        }

        return DEFAULT_MIMETYPE;
    }
}

文件上传接口

LocalUpload 接口

import com.linktopa.corex.ktsupport.localuploadfile.entity.UploadFileParam
import com.linktopa.corex.multipart.core.MultipartData
import org.springframework.web.multipart.MultipartFile
import java.io.File
import javax.servlet.http.HttpServletRequest

/**
 * LocalUpload
 * 本地文字上传接口
 * @constructor Create empty LocalUpload
 */
interface LocalUpload{

    /**
     * defaultPath
     * 默认存储路径
     */
    var defaultPath: String


    /**
     * checkFileMd5
     *
     * 秒传、断点的文件MD5验证
     * 根据文件路径获取要上传的文件夹下的 文件名.conf 文件
     * 通过判断 *.conf 文件状态来验证(有条件的可以使用redis来记录上传状态和文件地址)
     *
     * @param fileMd5      文件的MD5
     * @param fileName     文件名(包含文件格式)
     * @param confFilePath 分片配置文件全路径(不包含文件名)
     * @param tmpFilePath  上传的缓存文件全路径(不包含文件名)
     * @return
     */
    fun  checkFileMd5(
        fileMd5:String,
        fileName:String,
        confFilePath:String=defaultPath+"confFile",
        tmpFilePath:String=defaultPath+"file"
    ) : MultipartData

    /**
     * fragmentFileUploader
     *
     * 文件分片、断点续传上传程序
     * 创建 文件名.conf 文件记录已上传分片信息
     * 使用 RandomAccessFile(随机访问文件) 类随机指定位置写入文件,类似于合成分片
     * 检验分片文件是否全部上传完成,重命名缓存文件
     *
     * @param param        上传文件时 需要接收的基本参数信息
     * @param confFilePath 分片配置文件的路径,考虑到配置文件与缓存文件分开的情况(不包含文件名)
     * @param filePath     上传文件的路径,同时也是生成缓存文件的路径(不包含文件名)
     * @param chunkSize    每块分片的大小,单位:字节(这个值需要与前端JS的值保持一致) 5M=5242880
     * @param request      HTTP Servlet请求
     * @return
     */
    fun fragmentFileUploader(
        param: UploadFileParam,
        chunkSize:Long,
        request: HttpServletRequest,
        confFilePath:String=defaultPath+"confFile",
        filePath:String=defaultPath+"file"
    ):MultipartData

    /**
     * regularFileUploader
     *
     * 普通的文件上传程序、不使用分片、断点续传
     *
     * @param param    上传文件时 需要接收的基本参数信息
     * @param filePath 上传文件的路径,不包含文件名 log/uploader
     * @return
     */
    fun regularFileUploader(
        param: UploadFileParam,
        filePath:String=defaultPath+"file"
    ):MultipartData

    /**
     * regularFileUploader
     *
     * 普通的文件上传程序、使用默认上传路径
     *
     * @param file 文件流信息
     * @return
     */
    fun regularFileUploader(
        file: MultipartFile,
        filePath:String=defaultPath+"file"
    ):MultipartData

    /**
     * renameFile
     *
     * 用于上传成功后重命名文件
     *
     * @param toBeRenamed   需要重命名的文件对象
     * @param toFileNewName 文件新的名字
     * @return
     */
    fun renameFile(
        toBeRenamed:File,
        toFileNewName:String
    ):Boolean
}

本地上传文件实现类

LocalUploadImpl 实现类

import cn.hutool.core.io.FileUtil
import com.alibaba.fastjson.JSON
import com.alibaba.fastjson.JSONArray
import com.linktopa.corex.ktsupport.localuploadfile.entity.FileInfo
import com.linktopa.corex.ktsupport.localuploadfile.LocalUpload
import com.linktopa.corex.ktsupport.localuploadfile.MimeTypes
import com.linktopa.corex.ktsupport.localuploadfile.entity.UploadFileParam
import com.linktopa.corex.multipart.core.MultipartData
import org.apache.commons.fileupload.servlet.ServletFileUpload
import org.apache.commons.io.FileUtils.readFileToByteArray
import org.springframework.http.HttpStatus
import org.springframework.web.multipart.MultipartFile
import java.io.File
import java.io.IOException
import java.io.RandomAccessFile
import javax.servlet.http.HttpServletRequest
import kotlin.experimental.and

/**
 * LocalUploadImpl
 *
 * 本地上传文件实现类
 *
 * @constructor Create empty LocalUploadImpl
 */
class LocalUploadImpl : LocalUpload {

    override var defaultPath: String= System.getProperty("user.dir") + File.separatorChar + "files" + File.separatorChar

    override fun checkFileMd5(
        fileMd5: String,
        fileName: String,
        confFilePath: String,
        tmpFilePath: String
    ): MultipartData {
        val isParamEmpty = (fileMd5.isBlank()
                || fileName.isBlank()
                || confFilePath.isBlank()
                || tmpFilePath.isBlank())
        if (isParamEmpty) {
            throw Exception("参数值为空")
        }
        // 构建分片配置文件对象
        val confFile = File(confFilePath + File.separatorChar + fileName + ".conf")

        // 布尔值:上传的文件缓存对象是否存在
        val isTmpFileEmpty = File(
            tmpFilePath
                    + File.separatorChar + fileName + "_tmp"
        ).exists()

        // 分片记录文件 和 文件缓存文件 同时存在 则 状态码定义为 206
        if (confFile.exists() && isTmpFileEmpty) {
            val completeList: ByteArray = readFileToByteArray(confFile)
            val missChunkList: MutableList<String> = ArrayList()
            for (i in completeList.indices) {
                if (completeList[i] != Byte.MAX_VALUE) {
                    missChunkList.add(i.toString())
                }
            }
            return MultipartData().include("code",HttpStatus.PARTIAL_CONTENT.value())
                .include("message","文件已经上传了一部分")
                .include("data",JSONArray.parseArray(JSON.toJSONString(missChunkList)))
        }

        // 布尔值:上传的文件对象是否存在
        val isFileEmpty = File(tmpFilePath + File.separatorChar + fileName)
            .exists()
        // 上传的文件 和 配置文件 同时存在 则 当前状态码为 200
        return if (isFileEmpty && confFile.exists()) {
            MultipartData().include("code",HttpStatus.OK.value())
                .include("message", "文件已上传成功")
        } else MultipartData().include("code",HttpStatus.NOT_FOUND.value())
            .include("message", "文件不存在")
    }

    override fun fragmentFileUploader(
        param: UploadFileParam,
        chunkSize: Long,
        request: HttpServletRequest,
        confFilePath: String,
        filePath: String
    ): MultipartData {
        val isParamEmpty = (filePath.isBlank()
                ||confFilePath.isBlank() && param.file == null)
        if (isParamEmpty) {
            throw Exception("参数值为空")
        }
        // 判断enctype属性是否为multipart/form-data
        val isMultipart: Boolean = ServletFileUpload.isMultipartContent(request)
        require(isMultipart) { "上传内容不是有效的multipart/form-data类型." }

        return try {
            // 分片配置文件
            val confFile: File = FileUtil.file(
                FileUtil.mkdir(confFilePath),
                "${param.name}.conf"
            )
            val accessConfFile = RandomAccessFile(confFile, "rw")
            // 把该分段标记为 true 表示完成
            accessConfFile.setLength(param.chunks!!.toLong())
            accessConfFile.seek(param.chunk!!.toLong())
            accessConfFile.write(Byte.MAX_VALUE.toInt())
            accessConfFile.close()
            // _tmp的缓存文件对象
            val tmpFile: File = FileUtil.file(
                FileUtil.mkdir(filePath),
                "${param.name}_tmp"
            )
            // 随机位置写入文件
            val accessTmpFile = RandomAccessFile(tmpFile, "rw")
            val offset = chunkSize * param.chunk!!
            // 定位到该分片的偏移量、写入该分片数据、释放
            accessTmpFile.seek(offset)
            accessTmpFile.write(param.file!!.bytes)
            accessTmpFile.close()
            // 检查是否全部分片都成功上传
            val completeList = readFileToByteArray(confFile)
            var isComplete = Byte.MAX_VALUE
            var i = 0
            while (i < completeList.size && isComplete == Byte.MAX_VALUE) {
                // 与运算, 如果有部分没有完成则 isComplete 不是 Byte.MAX_VALUE
                isComplete = (isComplete and completeList[i])
                i++
            }
            if (isComplete != Byte.MAX_VALUE) {
                return MultipartData().include("code", HttpStatus.OK.value())
                    .include("message","文件上传成功")
            }
            val isSuccess: Boolean = param.name?.let { renameFile(tmpFile, it) } == true
            if (!isSuccess) {
                throw Exception("文件重命名时失败")
            }
            // 全部上传成功后构建文件对象
            val fileInfo: FileInfo = FileInfo().apply {
                hash=param.md5
                name=param.name
                type= MimeTypes.getInstance().getMimeTypes(param.name)
                path=tmpFile.parent + File.separatorChar + param.name
                createTime=System.currentTimeMillis()
            }
            MultipartData().include("code", HttpStatus.CREATED.value())
                    .include("message","文件上传完成")
                    .include("data",fileInfo)
        } catch (e: IOException) {
            e.printStackTrace()
            MultipartData().include("code",500).include("message","文件上传失败")
        }
    }

    override fun regularFileUploader(param: UploadFileParam, filePath: String): MultipartData {
        val isParamEmpty = (filePath.isBlank()
                || param.name==null && param.file == null)
        if (isParamEmpty) {
            throw Exception("参数值为空")
        }

        // 上传的文件夹
        val uploadFolder = File(filePath)
        // 创建文件夹
        if (!uploadFolder.exists() && !uploadFolder.mkdirs()) {
            return MultipartData().include("code", HttpStatus.FORBIDDEN.value())
                .include("message", "上传所需文件夹创建失败")
        }

        // 上传的文件
        val uploadFile = File(filePath + File.separatorChar + param.name)

        // 写入文件
        param.file?.transferTo(uploadFile)

        // 校验文件是否上传成功
        if (uploadFile.length() != param.file!!.size) {
            return MultipartData().include("code",500).include("message","文件上传失败")
        }

        // 上传成功后构建文件对象
        val fileInfo: FileInfo = FileInfo().apply {
            hash=param.md5
            name=param.name
            type= MimeTypes.getInstance().getMimeTypes(param.name)
            path=uploadFile.path
            createTime=System.currentTimeMillis()
        }
        return MultipartData().include("code", HttpStatus.CREATED.value())
            .include("message","文件上传完成")
            .include("data",fileInfo)
    }

    override fun regularFileUploader(file: MultipartFile,filePath: String): MultipartData {
        return regularFileUploader(
            UploadFileParam().apply {
                this.file=file
                this.name=file.originalFilename
            },filePath
        )
    }

    override fun renameFile(toBeRenamed: File, toFileNewName: String): Boolean {
        // 检查要重命名的文件是否存在,是否是文件
        if (!toBeRenamed.exists() || toBeRenamed.isDirectory) {
            return false
        }
        val newFile = File(
            toBeRenamed.parent
                    + File.separatorChar + toFileNewName
        )
        // 修改文件名
        return toBeRenamed.renameTo(newFile)
    }
}

前端

demo.js

let count = 0;
// 初始化上传控件
let uploader = $.uploaderInit({
    server: 'http://127.0.0.1:1000/core/uploader/demo1',
    pick: {
        id: '#picker',
        multiple: true
    },
    chunked: true,
    fileQueued: (file) => {
        count++;
        $("#thelist table>tbody").append(`
        <tr id="${file.id}" class="item" flag=0>
            <td class="index">${count}</td>
            <td class="info">${file.name}</td>
            <td class="size">${WebUploader.Base.formatSize(file.size)}</td>
            <td class="state">等待上传...</td>
            <td class="percentage"></td>
            <td class="operate">
                <button name="upload" data-type="start" data-fid="${file.id}" class="btn btn-warning up-start">开始</button>
                <button name="delete" data-fid="${file.id}" class="btn btn-error">删除</button>
            </td>
        </tr>`);
        // 绑定事件之前先解除绑定
        $("button[name=upload]").unbind('click');
        $("button[name=delete]").unbind('click');
        $("button[name=upload]").on('click', function () {
            let state = $(this).data('type');
            console.log(new Date().getTime());
            switch (state) {
                case 'start':
                    $(this).data('type', 'stop');
                    $(this).text('暂停');
                    uploader.upload(uploader.getFile($(this).data('fid'), true))
                    break;
                case 'stop':
                    $(this).data('type', 'retry')
                    $(this).text('开始');
                    uploader.stop(true);
                    break;
                case 'retry':
                    $(this).data('type', 'stop');
                    $(this).text('暂停');
                    uploader.upload(uploader.getFile($(this).data('fid'), true).id);
                    break;
            }
            return false;
        })
        $("button[name=delete]").on('click', function () {
            uploader.removeFile(uploader.getFile($(this).data('fid'), true));
            console.log($(this).data('fid'))
            $("#" + $(this).data('fid')).remove();
        })
    },
    uploadBeforeSend: (object, data, headers) => {
        let file = object.file;
        data.md5 = file.md5 || '';
        data.uid = file.uid;
    },
    uploadProgress: (file, percentage) => {
        $('#' + file.id).find('td.percentage').text(
            '上传中 '
            + Math.round(percentage * 100) + '%'
            + '(' + WebUploader.Base.formatSize(file.uploadRate) + '/s)');
    },
    uploadSuccess: (file, response) => {
        $('#' + file.id).find('td.state').text('已上传');
        console.log(response);
    },
    uploadError: (file, reason) => {
        $('#' + file.id).find('td.state').text('上传出错');
        console.error(reason)
    },
    beforeInit: () => {
        // 这个必须要写在实例化前面
        WebUploader.Uploader.register({
            'before-send-file': 'beforeSendFile',
            'before-send': 'beforeSend'
        }, {
            // 时间点1:所有分块进行上传之前调用此函数
            beforeSendFile: function (file) {
                let deferred = WebUploader.Deferred();
                (new WebUploader.Uploader()).md5File(file, 0, 5242880).progress(function (percentage) {
                    // 显示计算进度
                    console.log('计算md5进度:', percentage);
                    $('#' + file.id).find("td.state").text("校验MD5中...");
                }).then(function (val) {
                    file.md5 = val;
                    file.uid = WebUploader.Base.guid();
                    // 进行md5判断
                    $.ajax({
                        url: 'http://127.0.0.1:1000/core/uploader/checkFile',
                        type: 'GET',
                        showError: false,
                        global: false,
                        data: {
                            fileName: file.name,
                            md5: file.md5
                        },
                        success: (data) => {
                            console.log(data);
                            let status = data.code;
                            deferred.resolve();
                            switch (status) {
                                case "200":
                                    // 忽略上传过程,直接标识上传成功;
                                    uploader.skipFile(file);
                                    file.pass = true;
                                    break;
                                case "206":
                                    // 部分已经上传到服务器了,但是差几个模块。
                                    file.missChunks = data.data;
                                    console.log(file.missChunks);
                                    break;
                                default:
                                    break;
                            }
                        }
                    })
                })
                return deferred.promise();
            },
            // 时间点2:如果有分块上传,则每个分块上传之前调用此函数
            beforeSend: function (block) {
                let deferred = WebUploader.Deferred();
                // 当前未上传分块
                let missChunks = block.file.missChunks;
                // 当前分块
                let blockChunk = block.chunk;
                if (missChunks !== null && missChunks !== undefined && missChunks !== '') {
                    let flag = true;
                    for (let i = 0; i < missChunks.length; i++) {
                        if (blockChunk === parseInt(missChunks[i])) {
                            // 存在还未上传的分块
                            flag = false;
                            break;
                        }
                    }
                    if (flag) {
                        deferred.reject();
                    } else {
                        deferred.resolve();
                    }
                } else {
                    deferred.resolve();
                }
                return deferred.promise();
            }
        });
    }
});

demo.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>自定义 分片、缓存 文件路径 分片上传、断点续传 文件</title>

    <link rel="stylesheet" type="text/css" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css">
    <style type="text/css">

    .webuploader-container {
        position: relative;
    }

    .webuploader-element-invisible {
        position: absolute !important;
        clip: rect(1px 1px 1px 1px); /* IE6, IE7 */
        clip: rect(1px, 1px, 1px, 1px);
    }

    .webuploader-pick {
        position: relative;
        display: inline-block;
        cursor: pointer;
        /*background: #00b7ee;*/
        padding: 5px 5px;
        color: #fff;
        text-align: center;
        border-radius: 3px;
        overflow: hidden;
    }

    .webuploader-pick-hover {
        /*background: #00a2d4;*/
    }

    .webuploader-pick-disable {
        opacity: 0.6;
        pointer-events: none;
    }

    #picker {
        width: 86px;
        height: 40px;
        display: inline-block;
        line-height: 1.428571429;
        vertical-align: middle;
        margin: 0 12px 0 0;
    }

    .webuploader-domain {
        top: 0px !important;
        left: 0px !important;
    }
</style>
</head>
<body>


<div id="uploader" class="container">
    <pre>
    封装 web-uploader 上传控件 分片上传、断点续传大文件
    由于上传控件自身问题 暂停和开始 存在一定程度BUG
    适用于没有文件 暂停和开始的 业务需求
    </pre>
    <!--用来存放文件信息-->
    <div id="thelist" class="row">
        <div class="panel panel-primary">
            <div class="panel-heading">文件列表</div>
            <table class="table table-striped table-bordered" id="uploadTable">
                <thead>
                <tr>
                    <th>序号</th>
                    <th>文件名称</th>
                    <th>文件大小</th>
                    <th>上传状态</th>
                    <th>上传进度</th>
                    <th>操作</th>
                </tr>
                </thead>
                <tbody></tbody>
            </table>
            <div class="panel-footer">
                <div id="picker" class="btn btn-info">选择文件</div>
                <button id="btn" class="btn btn-default">开始上传</button>
                <button id="stop" class="btn btn-default">停止上传</button>
            </div>
        </div>
    </div>

    <div id="uploader-doc"></div>

</div>

<script src="https://static-page-1255518771.cos.ap-shanghai.myqcloud.com/common/js/jquery/jquery-3.4.1.min.js"></script>

<!-- 上传控件所需js -->
<script src="https://qcloud-1256166828.cos.ap-shanghai.myqcloud.com/script/javascript/uploader/web-uploader/0.1.6/web-uploader.min.js"></script>
<script src="https://qcloud-1256166828.cos.ap-shanghai.myqcloud.com/script/javascript/uploader/web-uploader/uploader.js"></script>
<script src="js/demo1.js"></script>

<script>

    $("#btn").on('click', function () {
        if (uploader.getFiles().length > 0) {
            $(".up-start").data('type', 'stop');
            $(".up-start").text('暂停');
            uploader.upload();
        }
        return false;
    });



    $("#stop").on('click', function () {
        if (uploader.getFiles().length > 0) {
            $(".up-start").data('type', 'start');
            $(".up-start").text('开始');
            uploader.stop(true);
        }
        return false;
    });

    $("#uploader-doc").load("https://qcloud-1256166828.cos.ap-shanghai.myqcloud.com/script/javascript/uploader/web-uploader/uploader-doc.html")

</script>
</body>
</html>

如本文“对您有用”,欢迎随意打赏作者,让我们坚持创作!

1 打赏
Enamiĝu al vi
不要为明天忧虑.因为明天自有明天的忧虑.一天的难处一天当就够了。
543文章 68评论 294点赞 593831浏览

随机文章
Java—并发编程(六)JUC锁 – (6)LockSupport
4年前
Java—HashMap
5年前
SpringCloud—Consul(一)(Ribbon)
5年前
Kotlin-类型初步—智能类型转换(十)
4年前
Ajax——省市联动
5年前
博客统计
  • 日志总数:543 篇
  • 评论数目:68 条
  • 建站日期:2020-03-06
  • 运行天数:1927 天
  • 标签总数:23 个
  • 最后更新:2024-12-20
Copyright © 2025 网站备案号: 浙ICP备20017730号 身体没有灵魂是死的,信心没有行为也是死的。
主页
页面
  • 归档
  • 摘要
  • 杂图
  • 问题随笔
博主
Enamiĝu al vi
Enamiĝu al vi 管理员
To be, or not to be
543 文章 68 评论 593831 浏览
测试
测试
看板娘
赞赏作者

请通过微信、支付宝 APP 扫一扫

感谢您对作者的支持!

 支付宝 微信支付