# Java 文件系统
众所周知 Java 是一个跨平台的语言,不同的操作系统有着完全不一样的文件系统和特性。JDK 会根据不同的操作系统 (
AIX,Linux,MacOSX,Solaris,Unix,Windows
) 编译成不同的版本。在 Java 语言中对文件的任何操作最终都是通过
JNI
调用C语言
函数实现的。Java 为了能够实现跨操作系统对文件进行操作抽象了一个叫做 FileSystem 的对象出来,不同的操作系统只需要实现起抽象出来的文件操作方法即可实现跨平台的文件操作了。
# Java FileSystem
在 Java SE 中内置了两类文件系统: java.io
和 java.nio
, java.nio
的实现是 sun.nio
,文件系统底层的 API 实现如下图:
# Java IO 文件系统
Java 抽象出了一个叫做文件系统的对象: java.io.FileSystem
,不同的操作系统有不一样的文件系统,例如 Windows
和 Unix
就是两种不一样的文件系统: java.io.UnixFileSystem
、 java.io.WinNTFileSystem
。
java.io.FileSystem
是一个抽象类,它抽象了对文件的操作,不同操作系统版本的 JDK 会实现其抽象的方法从而也就实现了跨平台的文件的访问操作。
示例中的 java.io.UnixFileSystem
最终会通过 JNI 调用 native 方法来实现对文件的操作:
由此我们可以得出 Java 只不过是实现了对文件操作的封装而已,最终读写文件的实现都是通过调用 native 方法实现的。
不过需要特别注意一下几点:
- 并不是所有的文件操作都在
java.io.FileSystem
中定义,文件的读取最终调用的是java.io.FileInputStream#read0、readBytes
、java.io.RandomAccessFile#read0、readBytes
, 而写文件调用的是java.io.FileOutputStream#writeBytes
、java.io.RandomAccessFile#write0
。 - Java 有两类文件系统 API!一个是基于
阻塞模式的IO
的文件系统,另一是 JDK7 + 基于NIO.2
的文件系统。
# Java NIO.2 文件系统
Java 7 提出了一个基于 NIO 的文件系统,这个 NIO 文件系统和阻塞 IO 文件系统两者是完全独立的。 java.nio.file.spi.FileSystemProvider
对文件的封装和 java.io.FileSystem
同理。
NIO 的文件操作在不同的系统的最终实现类也是不一样的,比如 Mac 的实现类是: sun.nio.fs.UnixNativeDispatcher
, 而 Windows 的实现类是 sun.nio.fs.WindowsNativeDispatcher
。
合理的利用 NIO 文件系统这一特性我们可以绕过某些只是防御了 java.io.FileSystem
的 WAF
/ RASP
# Java IO/NIO 多种读写文件方式
我们通常读写文件都是使用的阻塞模式,与之对应的也就是 java.io.FileSystem
。 java.io.FileInputStream
类提供了对文件的读取功能,Java 的其他读取文件的方法基本上都是封装了 java.io.FileInputStream
类,比如: java.io.FileReader
。
package org.example; | |
import java.io.*; | |
public class FileInputStreamInputDemo { | |
public static void main(String[] args) throws IOException{ | |
File file = new File("/etc/passwd"); | |
// 打开文件对象并创建文件输入流 | |
FileInputStream fis = new FileInputStream(file); | |
// 定义每次输入流读取到的字节数对象 | |
int a = 0; | |
// 定义缓冲区大小 | |
byte[] bytes = new byte[1024]; | |
// 创建二进制输出流对象 | |
ByteArrayOutputStream out = new ByteArrayOutputStream(); | |
// 循环读取文件内容,将文件内容读取到 bytes 数组中 | |
while ((a = fis.read(bytes)) != -1) { | |
// 截取缓冲区数组中的内容,(bytes, 0, a) 其中的 0 表示从 bytes 数组的 | |
// 下标 0 开始截取,a 表示输入流 read 到的字节数。 | |
out.write(bytes, 0, a); | |
} | |
System.out.println(out.toString()); | |
} | |
} |
调用过程:
- 指定文件路径,创建文件对象
- 创建文件输入流,利用创建好的文件对象创建输入流对象
- 定义缓冲区
byte[] bytes = new byte[1024];
- 创建二进制输出对象
- 从输入流中将数据循环读取到缓冲区(二进制数组)中
- 将缓冲区的数据循环读取到输出流(二进制输出对象)
- 输出二进制输出对象的内容
程序流程图:
调用链如下:
java.io.FileInputStream.readBytes(FileInputStream.java:219) | |
java.io.FileInputStream.read(FileInputStream.java:233) | |
com.anbai.sec.filesystem.FileInputStreamDemo.main(FileInputStreamDemo.java:27) |
其中的 readBytes 是 native 方法,文件的打开、关闭等方法也都是 native 方法:
private native int readBytes(byte b[], int off, int len) throws IOException; | |
private native void open0(String name) throws FileNotFoundException; | |
private native int read0() throws IOException; | |
private native long skip0(long n) throws IOException; | |
private native int available0() throws IOException; | |
private native void close0() throws IOException; |
java.io.FileInputStream
类对应的 native 实现如下:
JNIEXPORT void JNICALL | |
Java_java_io_FileInputStream_open0(JNIEnv *env, jobject this, jstring path) { | |
fileOpen(env, this, path, fis_fd, O_RDONLY); | |
} | |
JNIEXPORT jint JNICALL | |
Java_java_io_FileInputStream_read0(JNIEnv *env, jobject this) { | |
return readSingle(env, this, fis_fd); | |
} | |
JNIEXPORT jint JNICALL | |
Java_java_io_FileInputStream_readBytes(JNIEnv *env, jobject this, | |
jbyteArray bytes, jint off, jint len) { | |
return readBytes(env, this, bytes, off, len, fis_fd); | |
} | |
JNIEXPORT jlong JNICALL | |
Java_java_io_FileInputStream_skip0(JNIEnv *env, jobject this, jlong toSkip) { | |
jlong cur = jlong_zero; | |
jlong end = jlong_zero; | |
FD fd = GET_FD(this, fis_fd); | |
if (fd == -1) { | |
JNU_ThrowIOException (env, "Stream Closed"); | |
return 0; | |
} | |
if ((cur = IO_Lseek(fd, (jlong)0, (jint)SEEK_CUR)) == -1) { | |
JNU_ThrowIOExceptionWithLastError(env, "Seek error"); | |
} else if ((end = IO_Lseek(fd, toSkip, (jint)SEEK_CUR)) == -1) { | |
JNU_ThrowIOExceptionWithLastError(env, "Seek error"); | |
} | |
return (end - cur); | |
} | |
JNIEXPORT jint JNICALL | |
Java_java_io_FileInputStream_available0(JNIEnv *env, jobject this) { | |
jlong ret; | |
FD fd = GET_FD(this, fis_fd); | |
if (fd == -1) { | |
JNU_ThrowIOException (env, "Stream Closed"); | |
return 0; | |
} | |
if (IO_Available(fd, &ret)) { | |
if (ret > INT_MAX) { | |
ret = (jlong) INT_MAX; | |
} else if (ret < 0) { | |
ret = 0; | |
} | |
return jlong_to_jint(ret); | |
} | |
JNU_ThrowIOExceptionWithLastError(env, NULL); | |
return 0; | |
} |
# FileOutputStream
使用 FileOutputStream 实现写文件 Demo:
package org.example; | |
import java.io.File; | |
import java.io.FileOutputStream; | |
import java.io.IOException; | |
public class FileOutputStreamDemo { | |
public static void main(String[] args) { | |
try { | |
// 创建文件对象 | |
File file = new File("src/main/resources/static/test.txt"); | |
// 输出文件绝对路径 | |
System.out.println(file.getAbsolutePath()); | |
// 创建文件输出流对象 | |
FileOutputStream fos = new FileOutputStream(file); | |
// 写入文件 | |
fos.write("Hello, World!".getBytes()); | |
// 关闭输出流 | |
fos.close(); | |
} catch (IOException e) { | |
e.printStackTrace(); | |
} | |
} | |
} |
# RandomAccessFile
Java 提供了一个非常有趣的读取文件内容的类: java.io.RandomAccessFile
, 这个类名字面意思是任意文件内容访问,特别之处是这个类不仅可以像 java.io.FileInputStream
一样读取文件,而且还可以写文件。
RandomAccessFile 读取文件测试代码:
package org.example; | |
import java.io.ByteArrayOutputStream; | |
import java.io.FileOutputStream; | |
import java.io.IOException; | |
import java.io.RandomAccessFile; | |
public class RandomAccessFileDemo { | |
public static void main(String[] args) { | |
try { | |
// 创建一个 RandomAccessFile 对象 | |
RandomAccessFile file = new RandomAccessFile("src/main/resources/static/test.txt", "rw"); | |
// 写入文件 | |
byte[] bytes = new byte[1024]; | |
ByteArrayOutputStream out = new ByteArrayOutputStream(); | |
int a = 0; | |
while ((a = file.read(bytes, 0, 1024)) != -1) { | |
out.write(bytes, 0, a); | |
} | |
System.out.println(out.toString()); | |
// 关闭 RandomAccessFile | |
file.close(); | |
} catch (IOException e) { | |
e.printStackTrace(); | |
} | |
} | |
} |
任意文件读取特性体现在如下方法:
// 获取文件描述符 | |
public final FileDescriptor getFD() throws IOException | |
// 获取文件指针 | |
public native long getFilePointer() throws IOException; | |
// 设置文件偏移量 | |
private native void seek0(long pos) throws IOException; |
java.io.RandomAccessFile
类中提供了几十个 readXXX
方法用以读取文件系统,最终都会调用到 read0
或者 readBytes
方法,我们只需要掌握如何利用 RandomAccessFile
读 / 写文件就行了。
RandomAccessFile 写文件测试代码:
package org.example; | |
import java.io.IOException; | |
import java.io.RandomAccessFile; | |
public class RandomAccessFileOutputStream { | |
public static void main(String[] args) { | |
// Create a RandomAccessFile object | |
try { | |
RandomAccessFile file = new RandomAccessFile("src/main/resources/static/test.txt", "rw"); | |
System.out.println(file.length()); | |
file.write("Hello, World^_^".getBytes()); | |
// Close RandomAccessFile | |
file.close(); | |
} catch (IOException e) { | |
e.printStackTrace(); | |
} | |
} | |
} |
# FileSystemProvider
前面章节提到了 JDK7 新增的 NIO.2 的 java.nio.file.spi.FileSystemProvider
, 利用 FileSystemProvider
我们可以利用支持异步的通道 ( Channel
) 模式读取文件内容。
FileSystemProvider 读取文件内容示例:
package com.anbai.sec.filesystem; | |
import java.io.IOException; | |
import java.nio.file.Files; | |
import java.nio.file.Path; | |
import java.nio.file.Paths; | |
/** | |
* Creator: yz | |
* Date: 2019/12/4 | |
*/ | |
public class FilesDemo { | |
public static void main(String[] args) { | |
// 通过 File 对象定义读取的文件路径 | |
// File file = new File("/etc/passwd"); | |
// Path path1 = file.toPath(); | |
// 定义读取的文件路径 | |
Path path = Paths.get("/etc/passwd"); | |
try { | |
byte[] bytes = Files.readAllBytes(path); | |
System.out.println(new String(bytes)); | |
} catch (IOException e) { | |
e.printStackTrace(); | |
} | |
} | |
} |
java.nio.file.Files
是 JDK7 开始提供的一个对文件读写取非常便捷的 API,其底层实在是调用了 java.nio.file.spi.FileSystemProvider
来实现对文件的读写的。最为底层的实现类是 sun.nio.ch.FileDispatcherImpl#read0
。
基于 NIO 的文件读取逻辑是:打开 FileChannel-> 读取 Channel 内容。
打开 FileChannel 的调用链为:
sun.nio.ch.FileChannelImpl.<init>(FileChannelImpl.java:89) | |
sun.nio.ch.FileChannelImpl.open(FileChannelImpl.java:105) | |
sun.nio.fs.UnixChannelFactory.newFileChannel(UnixChannelFactory.java:137) | |
sun.nio.fs.UnixChannelFactory.newFileChannel(UnixChannelFactory.java:148) | |
sun.nio.fs.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:212) | |
java.nio.file.Files.newByteChannel(Files.java:361) | |
java.nio.file.Files.newByteChannel(Files.java:407) | |
java.nio.file.Files.readAllBytes(Files.java:3152) | |
com.anbai.sec.filesystem.FilesDemo.main(FilesDemo.java:23) |
文件读取的调用链为:
sun.nio.ch.FileChannelImpl.read(FileChannelImpl.java:147) | |
sun.nio.ch.ChannelInputStream.read(ChannelInputStream.java:65) | |
sun.nio.ch.ChannelInputStream.read(ChannelInputStream.java:109) | |
sun.nio.ch.ChannelInputStream.read(ChannelInputStream.java:103) | |
java.nio.file.Files.read(Files.java:3105) | |
java.nio.file.Files.readAllBytes(Files.java:3158) | |
com.anbai.sec.filesystem.FilesDemo.main(FilesDemo.java:23) |
FileSystemProvider 写文件示例:
package com.anbai.sec.filesystem; | |
import java.io.IOException; | |
import java.nio.file.Files; | |
import java.nio.file.Path; | |
import java.nio.file.Paths; | |
/** | |
* Creator: yz | |
* Date: 2019/12/4 | |
*/ | |
public class FilesWriteDemo { | |
public static void main(String[] args) { | |
// 通过 File 对象定义读取的文件路径 | |
// File file = new File("/etc/passwd"); | |
// Path path1 = file.toPath(); | |
// 定义读取的文件路径 | |
Path path = Paths.get("/tmp/test.txt"); | |
// 定义待写入文件内容 | |
String content = "Hello World."; | |
try { | |
// 写入内容二进制到文件 | |
Files.write(path, content.getBytes()); | |
} catch (IOException e) { | |
e.printStackTrace(); | |
} | |
} | |
} |
# 文件读写总结
Java 内置的文件读取方式大概就是这三种方式,其他的文件读取 API 可以说都是对这几种方式的封装而已 (依赖数据库、命令执行、自写 JNI 接口不算,本人个人理解,如有其他途径还请告知)。本章我们通过深入基于 IO 和 NIO 的 Java 文件系统底层 API,希望大家能够通过以上 Demo 深入了解到文件读写的原理和本质。