【问题】
在服务器上的两个模块,一个专门处理文件(阿里云OSS)下载的模块A,另一个拿这些文件去处理业务需求的模块B。于是A模块先把文件下载下来,放到服务器上,然后B模块再根据所传过来的文件对象转换成流去处理业务。但是这样一来,服务器上就会多出来很多文件。如图:
如果请求用户一多,文件岂不是要大量积压在这里?怎么办呢?
【思考】
首先,能不能业务模块B处理完了业务之后把文件都删了呢?但是这样的话,还是很麻烦,而且还要考虑用不同的文件路径来存放不同的用户文件,万一文件名称相同,可能每个请求都要创建一个单独的文件路径来存放,这种方式的代价还是代价太大了......
或者可不可以直接不生成文件返回一个二进制的数组,比如byte[],又或者返回一个数据流对象?
说起来,对于Java IO相关的内容还是理解地不透彻。为什么Java中都是用各种InputStream和OutputStream来处理流,而不是直接把文件转换为完整的二进制数组来传输呢?使用流对象的过程中为什么总是用:
while((length=inputStream.read(byteArray))!=-1)
{
...
}
来处理流之间的转换,这个字节数组为什么长度很多时候都定义为1024?我们在程序中经常使用的System.out.print到底是个啥?可能甚至见过在Servlet中,使用了out.println将html写入了容器的响应对象response......
【查阅资料】
- IO这个概念,就是输入和输出的缩写,凡是接触到计算机这个体系的都不陌生。首先,要明确一点,对于小文件,直接返回一组byte[]是可行的,此时我们只需要将不同的输入流转换为字节数组即可。如代码所示:
public class FileIOTransferTest {
public static void main(String[] args) throws IOException {
testByteToFile();
}
private static void testByteToFile() throws IOException {
File file = new File("C:\\Users\\Administrator\\Desktop\\1123123.png");
FileOutputStream fileOutputStream = new FileOutputStream(file);
byte[] byteArrayFromFile = new FileIOTransferTest().getByteArrayFromFile();
fileOutputStream.write(byteArrayFromFile, 0, byteArrayFromFile.length);
fileOutputStream.flush();
fileOutputStream.close();
}
public byte[] getByteArrayFromFile() {
File file = new File("C:\\Users\\Administrator\\Desktop\\123.png");
try {
FileInputStream fileInputStream = new FileInputStream(file);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
int len;
byte[] buffer = new byte[1024];
while ((len = fileInputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, len);
}
return byteArrayOutputStream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return new byte[0];
}
}
但是,为什么要用流来处理而不是用一个完整的字节数组?我们可以想一下:
1.如果文件很大,而内存设置很小,一个完整的字节数组可能会比整个内存空间都大,那这种情况岂不是处理不了?尤其是一些嵌入式的设备,内存空间是很宝贵的;
2.对于网络传输的字节流,本来就是分批传的,一个完整的字节组,可能会根据协议分好几批传入到系统中,所以必须用这种方式来进行处理;
3.Java的流处理,实际上是基于操作系统内核提供的api进行的,所以很大程度上也是会根据OS的api特性来进行设计。如linux的IO流,read、write等等。
- 而1024这个问题,其实是为了设置一个合适的缓冲区,避免每次只去读取一个字节而频繁使用IO(一个很耗时的操作)。我们可以看看对read的描述
/**
* Reads a byte of data from this input stream. This method blocks
* if no input is yet available.
*
* @return the next byte of data, or <code>-1</code> if the end of the
* file is reached.
* @exception IOException if an I/O error occurs.
*/
public int read() throws IOException {
return read0();
}
private native int read0() throws IOException;
这里是一个字节一个字节去读取的,所以这个read()方法,如果是一般的场景千万不要直接使用,而是去设置一个缓冲区,一批一批地拿数据。这里我们可以参考BufferedInputStream和BufferedOutputStream的设计,它在内部设置的默认缓冲区的大小是8192byte。这里就是一个平衡,设置太小,那么IO会特别频繁,肯定性能是不好的,如果设置地过大,很多时候会考虑一些场景的限制,比如Dubbo的接口限制数据量大小为8M,所以这里可以根据场景来进行设置。就像HashMap中可以自己定义一个初始的容量大小,避免在数据装填过程中频繁的扩展数组...
- 至于不同的方法将数据输出到了不同的位置,也是可以直接溯源这部分代码的源头,看看这个流的destination设置在了哪儿。
【扩展体系】
在对以上问题梳理的过程中,其实应该搞明白的是,这一块儿到底包含了哪些比较重要的内容,我认为比较重要的是以下几个部分:
- JDK体系中的字节流和字符流体系是很么样子的,首先是4个抽象基类:
字节输入流:InputStream 字符输入流:Reader
字节输出流:OutputStream 字符输出流:Writer
然后是整个体系结构:
分类 |
字节输入流 |
字节输出流 |
字符输入流 |
字符输出流 |
抽象基类 |
InputStream |
OutputStream |
Reader |
Writer |
访问文件 |
FileInputStream |
FileOutputStream |
FileReader |
FileWriter |
访问数组 |
ByteArrayInputStream |
ByteArrayOutputStream |
CharArrayReader |
CharArrayWriter |
访问管道 |
PipedInputStream |
PipedOutputStream |
PipedReader |
PipedWriter |
缓冲流 |
BufferedInputStream |
BufferedOutputStream |
BufferedReader |
BufferedWriter |
转换流 |
InputStreamReader |
OutputStreamWriter |
||
对象流 |
ObjectInputStream |
ObjectOutputStream |
||
抽象基类 |
FilterInputStream |
FilterOutputStream |
FilterReader |
FilterWriter |
打印流 |
PrintStream |
PrintWriter |
||
特殊流 |
DataInputStream |
DataOutputStream |
这里,我们需要知道的是每一个重要的分支为什么会存在,有什么不一样的特性和使用场景。囿于篇幅,这里不做展开。
- 由于Java API封装到了底层其实是native方法,比如read,write,open等,为了深入了解其中的原理,我们还需要去了解操作系统的IO处理流程,如Linux的IO流是如何处理的,内核中的缓冲区怎么处理,到了JVM中(进程级别)的缓冲区又是怎么处理的。知道大体的处理流程。
- 其实传统的IO处理,一直是阻塞的模式,直到NIO的出现,所以,NIO也要着重去理解。
以上基本上就是比较完整的理论体系了。如果对以上比较清楚,掌握原理,也就可以说对于IO这一块儿基本OK了。但是对于系统的性能分析,我们可能还需要多一些实践的工具,比如Linux上去分析IO的性能瓶颈在哪里,怎么查询,思路等等。理论和实践基本搞定的话,我们整个知识体系就立起来了,后续的复杂的问题,只是在这里体系上不断地扩展,开枝散叶。