概述

由来

IO的操作包括,应用场景包括网络操作和文件操作。IO操作在Java中是一个较为复杂的过程,我们在面对不同的场景时,要选择不同的InputStreamOutputStream实现来完成这些操作。而如果想读写字节流,还需要ReaderWriter的各种实现类。这些繁杂的实现类,一方面给我我们提供了更多的灵活性,另一方面也增加了复杂性。

封装

io包的封装主要针对流、文件的读写封装,主要以工具类为主,提供常用功能的封装,这包括:

  • IoUtil 流操作工具类
  • FileUtil 文件读写和操作的工具类。
  • FileTypeUtil 文件类型判断工具类
  • WatchMonitor 目录、文件监听,封装了JDK1.7中的WatchService
  • ClassPathResource针对ClassPath中资源的访问封装
  • FileReader 封装文件读取
  • FileWriter 封装文件写入

    流扩展

    除了针对JDK的读写封装外,还针对特定环境和文件扩展了流实现。
    包括:

  • BOMInputStream针对含有BOM头的流读取

  • FastByteArrayOutputStream 基于快速缓冲FastByteBuffer的OutputStream,随着数据的增长自动扩充缓冲区(from blade)
  • FastByteBuffer 快速缓冲,将数据存放在缓冲集中,取代以往的单一数组(from blade)

IO工具类-IoUtil

由来

IO工具类的存在主要针对InputStream、OutputStream、Reader、Writer封装简化,并对NIO相关操作做封装简化。总体来说,Hutool对IO的封装,主要是工具层面,我们努力做到在便捷、性能和灵活之间找到最好的平衡点。

方法

拷贝

流的读写可以总结为从输入流读取,从输出流写出,这个过程我们定义为拷贝。这个是一个基本过程,也是文件、流操作的基础。
以文件流拷贝为例:

  1. BufferedInputStream in = FileUtil.getInputStream("d:/test.txt");
  2. BufferedOutputStream out = FileUtil.getOutputStream("d:/test2.txt");
  3. long copySize = IoUtil.copy(in, out, IoUtil.DEFAULT_BUFFER_SIZE);

copy方法同样针对Reader、Writer、Channel等对象有一些重载方法,并提供可选的缓存大小。默认的,缓存大小为1024个字节,如果拷贝大文件或流数据较大,可以适当调整这个参数。
针对NIO,提供了copyByNIO方法,以便和BIO有所区别。我查阅过一些资料,使用NIO对文件流的操作有一定的提升,我并没有做具体实验。相关测试请参阅博客:http://www.cnblogs.com/gaopeng527/p/4896783.html

Stream转Reader、Writer

  • IoUtil.getReader:将InputStream转为BufferedReader用于读取字符流,它是部分readXXX方法的基础。
  • IoUtil.getWriter:将OutputStream转为OutputStreamWriter用于写入字符流,它是部分writeXXX的基础。

本质上这两个方法只是简单new一个新的Reader或者Writer对象,但是封装为工具方法配合IDE的自动提示可以大大减少查阅次数(例如你对BufferedReader、OutputStreamWriter不熟悉,是不需要搜索一下相关类?)

读取流中的内容

读取流中的内容总结下来,可以分为read方法和readXXX方法。

  1. read方法有诸多的重载方法,根据参数不同,可以读取不同对象中的内容,这包括:
    • InputStream
    • Reader
    • FileChannel

这三个重载大部分返回String字符串,为字符流读取提供极大便利。

  1. readXXX方法主要针对返回值做一些处理,例如:
    • readBytes 返回byte数组(读取图片等)
    • readHex 读取16进制字符串
    • readObj 读取序列化对象(反序列化)
    • readLines 按行读取
  2. toStream方法则是将某些对象转换为流对象,便于在某些情况下操作:
    • String 转换为ByteArrayInputStream
    • File 转换为FileInputStream

      写入到流

  • IoUtil.write方法有两个重载方法,一个直接调用OutputStream.write方法,另一个用于将对象转换为字符串(调用toString方法),然后写入到流中。
  • IoUtil.writeObjects 用于将可序列化对象序列化后写入到流中。

write方法并没有提供writeXXX,需要自己转换为String或byte[]。

关闭

对于IO操作来说,使用频率最高(也是最容易被遗忘)的就是close操作,好在Java规范使用了优雅的Closeable接口,这样我们只需简单封装调用此接口的方法即可。
关闭操作会面临两个问题:

  1. 被关闭对象为空
  2. 对象关闭失败(或对象已关闭)

IoUtil.close方法很好的解决了这两个问题。
在JDK1.7中,提供了AutoCloseable接口,在IoUtil中同样提供相应的重载方法,在使用中并不能感觉到有哪些不同。

文件工具类-FileUtil

简介

在IO操作中,文件的操作相对来说是比较复杂的,但也是使用频率最高的部分,我们几乎所有的项目中几乎都躺着一个叫做FileUtil或者FileUtils的工具类,我想Hutool应该将这个工具类纳入其中,解决用来解决大部分的文件操作问题。
总体来说,FileUtil类包含以下几类操作工具:

  1. 文件操作:包括文件目录的新建、删除、复制、移动、改名等
  2. 文件判断:判断文件或目录是否非空,是否为目录,是否为文件等等。
  3. 绝对路径:针对ClassPath中的文件转换为绝对路径文件。
  4. 文件名:主文件名,扩展名的获取
  5. 读操作:包括类似IoUtil中的getReader、readXXX操作
  6. 写操作:包括getWriter和writeXXX操作

在FileUtil中,我努力将方法名与Linux相一致,例如创建文件的方法并不是createFile,而是touch,这种统一对于熟悉Linux的人来说,大大提高了上手速度。当然,如果你不熟悉Linux,那FileUtil工具类的使用则是在帮助你学习Linux命令。这些类Linux命令的方法包括:

  • ls 列出目录和文件
  • touch 创建文件,如果父目录不存在也自动创建
  • mkdir 创建目录,会递归创建每层目录
  • del 删除文件或目录(递归删除,不判断是否为空),这个方法相当于Linux的delete命令
  • copy 拷贝文件或目录

这些方法提供了人性化的操作,例如touch方法,在创建文件的情况下会自动创建上层目录(我想对于使用者来说这也是大部分情况下的需求),同样mkdir也会创建父目录。

需要注意的是,del方法会删除目录而不判断其是否为空,这一方面方便了使用,另一方面也可能造成一些预想不到的后果(比如拼写错路径而删除不应该删除的目录),所以请谨慎使用此方法。

关于FileUtil中更多工具方法,请参阅API文档。

文件类型判断-FileTypeUtil

由来

在文件上传时,有时候我们需要判断文件类型。但是又不能简单的通过扩展名来判断(防止恶意脚本等通过上传到服务器上),于是我们需要在服务端通过读取文件的首部几个二进制位来判断常用的文件类型。

使用

这个工具类使用非常简单,通过调用FileTypeUtil.getType即可判断,这个方法同时提供众多的重载方法,用于读取不同的文件和流。

  1. File file = FileUtil.file("d:/test.jpg");
  2. String type = FileTypeUtil.getType(file);
  3. //输出 jpg则说明确实为jpg文件
  4. Console.log(type);

原理和局限性

这个类是通过读取文件流中前N个byte值来判断文件类型,在类中我们通过Map形式将常用的文件类型做了映射,这些映射都是网络上搜集而来。也就是说,我们只能识别有限的几种文件类型。但是这些类型已经涵盖了常用的图片、音频、视频、Office文档类型,可以应对大部分的使用场景。

对于某些文本格式的文件我们并不能通过首部byte判断其类型,比如JSON,这类文件本质上是文本文件,我们应该读取其文本内容,通过其语法判断类型。

自定义类型

为了提高FileTypeUtil的扩展性,我们通过putFileType方法可以自定义文件类型。

  1. FileTypeUtil.putFileType("ffd8ffe000104a464946", "new_jpg");

第一个参数是文件流的前N个byte的16进制表示,我们可以读取自定义文件查看,选取一定长度即可(长度越长越精确),第二个参数就是文件类型,然后使用FileTypeUtil.getType即可。

注意 xlsx、docx本质上是各种XML打包为zip的结果,因此会被识别为zip格式。

文件监听-WatchMonitor

由来

很多时候我们需要监听一个文件的变化或者目录的变动,包括文件的创建、修改、删除,以及目录下文件的创建、修改和删除,在JDK7前我们只能靠轮询方式遍历目录或者定时检查文件的修改事件,这样效率非常低,性能也很差。因此在JDK7中引入了WatchService。不过考虑到其API并不友好,于是Hutool便针对其做了简化封装,使监听更简单,也提供了更好的功能,这包括:

  • 支持多级目录的监听(WatchService只支持一级目录),可自定义监听目录深度
  • 延迟合并触发支持(文件变动时可能触发多次modify,支持在某个时间范围内的多次修改事件合并为一个修改事件)
  • 简洁易懂的API方法,一个方法即可搞定监听,无需理解复杂的监听注册机制。
  • 多观察者实现,可以根据业务实现多个Watcher来响应同一个事件(通过WatcherChain)

    WatchMonitor

    在Hutool中,WatchMonitor主要针对JDK7中WatchService做了封装,针对文件和目录的变动(创建、更新、删除)做一个钩子,在Watcher中定义相应的逻辑来应对这些文件的变化。

    内部应用

    在hutool-setting模块,使用WatchMonitor监测配置文件变化,然后自动load到内存中。WatchMonitor的使用可以避免轮询,以事件响应的方式应对文件变化。

    使用

    WatchMonitor提供的事件有:

  • ENTRY_MODIFY 文件修改的事件

  • ENTRY_CREATE 文件或目录创建的事件
  • ENTRY_DELETE 文件或目录删除的事件
  • OVERFLOW 丢失的事件

这些事件对应StandardWatchEventKinds中的事件。
下面我们介绍WatchMonitor的使用:

监听指定事件

  1. File file = FileUtil.file("example.properties");
  2. //这里只监听文件或目录的修改事件
  3. WatchMonitor watchMonitor = WatchMonitor.create(file, WatchMonitor.ENTRY_MODIFY);
  4. watchMonitor.setWatcher(new Watcher(){
  5. @Override
  6. public void onCreate(WatchEvent<?> event, Path currentPath) {
  7. Object obj = event.context();
  8. Console.log("创建:{}-> {}", currentPath, obj);
  9. }
  10. @Override
  11. public void onModify(WatchEvent<?> event, Path currentPath) {
  12. Object obj = event.context();
  13. Console.log("修改:{}-> {}", currentPath, obj);
  14. }
  15. @Override
  16. public void onDelete(WatchEvent<?> event, Path currentPath) {
  17. Object obj = event.context();
  18. Console.log("删除:{}-> {}", currentPath, obj);
  19. }
  20. @Override
  21. public void onOverflow(WatchEvent<?> event, Path currentPath) {
  22. Object obj = event.context();
  23. Console.log("Overflow:{}-> {}", currentPath, obj);
  24. }
  25. });
  26. //设置监听目录的最大深入,目录层级大于制定层级的变更将不被监听,默认只监听当前层级目录
  27. watchMonitor.setMaxDepth(3);
  28. //启动监听
  29. watchMonitor.start();

监听全部事件

其实我们不必实现Watcher的所有接口方法,Hutool同时提供了SimpleWatcher类,只需重写对应方法即可。
同样,如果我们想监听所有事件,可以:

  1. WatchMonitor.createAll(file, new SimpleWatcher(){
  2. @Override
  3. public void onModify(WatchEvent<?> event, Path currentPath) {
  4. Console.log("EVENT modify");
  5. }
  6. }).start();

createAll方法会创建一个监听所有事件的WatchMonitor,同时在第二个参数中定义Watcher来负责处理这些变动。

延迟处理监听事件

在监听目录或文件时,如果这个文件有修改操作,JDK会多次触发modify方法,为了解决这个问题,我们定义了DelayWatcher,此类通过维护一个Set将短时间内相同文件多次modify的事件合并处理触发,从而避免以上问题。

  1. WatchMonitor monitor = WatchMonitor.createAll("d:/", new DelayWatcher(watcher, 500));
  2. monitor.start();

文件读取-FileReader

由来

FileUtil中本来已经针对文件的读操作做了大量的静态封装,但是根据职责分离原则,我觉得有必要针对文件读取单独封装一个类,这样项目更加清晰。当然,使用FileUtil操作文件是最方便的。

使用

在JDK中,同样有一个FileReader类,但是并不如想象中的那样好用,于是Hutool便提供了更加便捷FileReader类。

  1. //默认UTF-8编码,可以在构造中传入第二个参数做为编码
  2. FileReader fileReader = new FileReader("test.properties");
  3. String result = fileReader.readString();

FileReader提供了以下方法来快速读取文件内容:

  • readBytes
  • readString
  • readLines

同时,此类还提供了以下方法用于转换为流或者BufferedReader:

相应的,文件读取有了,自然有文件写入类,使用方式与FileReader也类似:

  1. FileWriter writer = new FileWriter("test.properties");
  2. writer.write("test");

写入文件分为追加模式和覆盖模式两类,追加模式可以用append方法,覆盖模式可以用write方法,同时也提供了一个write方法,第二个参数是可选覆盖模式。
同样,此类提供了:

  • getOutputStream
  • getWriter
  • getPrintWriter

这些方法用于转换为相应的类提供更加灵活的写入操作。

文件追加-FileAppender

由来

顾名思义,FileAppender类表示文件追加器。此对象持有一个一个文件,在内存中积累一定量的数据后统一追加到文件,此类只有在写入文件时打开文件,并在写入结束后关闭之。因此此类不需要关闭。
在调用append方法后会缓存于内存,只有超过容量后才会一次性写入文件,因此内存中随时有剩余未写入文件的内容,在最后必须调用flush方法将剩余内容刷入文件。
也就是说,这是一个支持缓存的文件内容追加器。此类主要用于类似于日志写出这类需求所用。

使用

  1. FileAppender appender = new FileAppender(file, 16, true);
  2. appender.append("123");
  3. appender.append("abc");
  4. appender.append("xyz");
  5. appender.flush();
  6. appender.toString();

文件跟随-Tailer

由来

有时候我们要启动一个线程实时“监控”文件的变化,比如有新内容写出到文件时,我们可以及时打印出来,这个功能非常类似于Linux下的tail -f命令。

使用

  1. Tailer tailer = new Tailer(FileUtil.file("f:/test/test.log"), Tailer.CONSOLE_HANDLER, 2);
  2. tailer.start();

其中Tailer.CONSOLE_HANDLER表示文件新增内容默认输出到控制台。

  1. /**
  2. * 命令行打印的行处理器
  3. *
  4. * @author looly
  5. * @since 4.5.2
  6. */
  7. public static class ConsoleLineHandler implements LineHandler {
  8. @Override
  9. public void handle(String line) {
  10. Console.log(line);
  11. }
  12. }

我们也可以实现自己的LineHandler来处理每一行数据。

注意 此方法会阻塞当前线程