前段时间,因为一个需求,为避免过多占用用户手机空间,需要对文件进行压缩。同时,这些文件将来可能需要上传到服务器,,如果使用HTTP自带的分片压缩机制,当发生网络异常或者分片部分丢失时,会面临整个压缩文件无法组装和解压,这样就因为一个分片丢失了整个文件的信息。
因此,最终的方案是首先将源文件压缩成指定大小的若干个独立的压缩文件,然后分别上传。因为每个文件都可以各自独立的被解压出源文件的一个片段,进行阅读。所有即使丢失部分分片,也不至于失去全部信息。
开始我是一脸懵逼的,因为平时接触文件操作并不多,所以对java的IO流操作也是一知半解,幸好得到同事的讲解(再次感谢!),让我对它有了更深入的认识。
1、 从BufferOutputStream讲起
如果要你写一个写文件的方法,每次调用时将会传入一个byte数据,你会怎么写?
对此,我相信大部分同学会和我一样,不假思索的采用BufferOutputStream。
为什么采用BufferOutputStream?教科书般的回答是这样:BufferOutputStream是带缓冲区的输出流,能够减少访问磁盘的次数,提高文件的写入效率。
然后让我们来看看BufferOutputStream的write方法。方法代码很简洁,首先判断当前的缓冲数组是否已满,如果不是,就先保存在缓存数据中;否则,先将数据写出到构造方法中传入的输出流中(本处为fileOutputStream),然后再将数据保存缓冲数组。
通过上述代码可以看到,BufferOutputStream只是在传入的输出流对象上增加了一项数据缓冲的职责,每当有新的输入时,BufferOutputStream首先进行了保存处理,当达到一定一定数量后,再将数据传递给真正的输出对象。这是否让你想起了装饰模式?
装饰模式:动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更为灵活。
2、 实现分片定量压缩
理解了BufferOutputStream中的装饰模式,要想实现最前面提出的分片定量压缩也就不是难事了。我们要做的就是像BufferOutputStream一样,写一个对象,给真正的输出流FileOutputStream再增加一项职责,当发现输出字节数到达指定大小的时候,就完成本文件,打开一个新的文件并输出。
先是一个简单的CountFileOutputStream类,继承自FileOutputStream,唯一功能是记录当前写入到输出流中的字节数,代码如下:
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
/**继承自FileOutputStream,唯一功能是记录当前写入到输出流中的字节数
* Created by supercqzhou on 2017/3/15.
*/
public class CountFileOutputStream extends FileOutputStream {
private volatile long mCurrentSize=0;
private Object mLock=new Object();
public CountFileOutputStream(String path) throws FileNotFoundException {
super(path);
}
@Override
public void write(byte[] buffer, int byteOffset, int byteCount) throws IOException {
super.write(buffer, byteOffset, byteCount);
synchronized (mLock){
mCurrentSize+=byteCount;
}
}
@Override
public void write(int oneByte) throws IOException {
super.write(oneByte);
synchronized (mLock){
mCurrentSize++;
}
}
@Override
public void write(byte[] buffer) throws IOException {
super.write(buffer);
synchronized (mLock){
if (buffer!=null)
mCurrentSize+=buffer.length;
}
}
public long getCurrentSize(){
return mCurrentSize;
}
}
接下来是分片定量压缩的主类QuantitativeOutputStream。
首先了解下初始化环境,首先会创建一个CountFileOutputStream流对象,并作为创建GZIPOutputStream流对象的一个参数。
然后是正文,当有数据需要写入文件时,首先会根据经过重写的CountFileOutputStream对象获取当前已写入的字节数,如果未达到指定的分片大小,则继续写入;否则,先将当前GZIPOutputStream流对象中的数据写入到文件流中并关闭,再另外打开一个新的文件,写入剩余的内容。
根据本方法,实际得到的文件大小均会大于等于实际指定的分片大小,在实际测试中发现,误差在30%以内。
拓展:加密
在后续的功能中,还会引入加密功能。那这里照着现在的思路就很简单了,在真正写入到文件流之前,加入一个新的职责对象,加密!
3、 总结
通过本例,可以更加轻松的理解java中“流”类库的设计思想,即装饰模式,动态添加职责。当然这样设计的让人迷惑的一点就是:创建单一的结果流,却需要创建多个对象(在本例中的CountFileOutputStream就是为了少写一个类而直接继承自FileOutputStream)。