由来
填自己技术漏洞时扫到 AsynchronousFileChannel
涉及到一个 AsynchronousFileChannel
+ CompletionHandler
读文件的 case
但网上类似 case 大多从简实现,或干脆没有代码……
自己写一写吧,发现踩了坑,好大一溜儿坑…………………………
这个例子 不完善,切勿直接用于正式研发生产环境
需求
通过 AsynchronousFileChannel
+ CompletionHandler
读一个比较长的文本文件,正确打印文件内容
这个需求不应该对应具体的实际需求,在最后的 总结 中说
关键点说明
read
就读尽
AsynchronousFileChannel
,异步通道AsynchronousFileChannel
是异步的
ArrayList
,每读一次就 add 一次即可,但 AsynchronousFileChannel
是异步的System.identityHashCode
System.identityHashCode
又存在碰撞的可能(虽然很小),因此需要及时清除处理完的 buffer 的 hashConcurrentHashMap
从以前的帖子里随手截取的一段
INDEX
§1 概述
§2 重要成员
§2.1 property
§2.2 method
§3 示例
§1 概述
缓冲区是一块内存空间,可以将它直观的理解为一个数组
这块内存空间直接和 Channel 相连接
可以向这块内存空间中写或读取数据
常用实现
ByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
MappedByteBuffer
常规使用流程
向 Buffer 中写入数据
调用 flip() 方法,将 Buffer 从写方向切换为读方向
从 Buffer 中读取数据
调用 clear() 或 compact() 方法清空 Buffer
clear() 清空整个 Buffer
compact() 清空 Buffer 中读过的部分
常规使用流程示例
012345678901234567890123456789
CompletionHandler
实现
用二维数组 汇总 文本文件的用于读场景的 CompletionHandler
public class ByteMatricTextReadCompletionHandler implements CompletionHandler<Integer, ByteBuffer> {
private long capacity; // file total size
private long window; // buffer size
// byte[] array for each read
private byte[][] metrix = null;
// position for file
private long position = 0;
private Charset charset = StandardCharsets.UTF_8;
// buffer 的一致性 hash 与 position 的映射,防字节数组乱序
private Map<Integer,Long> bufferPositions = new ConcurrentHashMap<>();
// 整个文件是否还没读完
public boolean isNotDone(){
return position < capacity;
}
// 获取整体读取结果
public String text(){
return text(this.charset);
}
// 获取整体读取结果
public String text(Charset charset){
return new String(ArrayUtil.addAll(metrix), charset);
}
@Override
public void completed(Integer result, ByteBuffer buffer) {
System.out.println(Thread.currentThread().getName());
buffer.flip();
/* *******************************
* 每一个 buffer 读取后的动作
* 及时移除:
* 帮助 GC
* 防 System.identityHashCode 碰撞
******************************* */
metrix[(int) (bufferPositions.remove(System.identityHashCode(buffer))/window)] = ArrayUtil.sub(buffer.array(),0,buffer.limit());
buffer.clear();
}
// 任意一次读失败,都肯能导致不能拼接出完整文件,直接异常
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
throw new IllegalStateException("error read: cause can not to assemble the complete target file");
}
/* *******************************
* 以下是工具
******************************* */
// 提交 position,注释的部分可以适用于调用 read 的部分也是多线程的场景
public ByteBuffer commit(){
// long pos = position.getAndAdd(window);
// ByteBuffer buf = ByteBuffer.allocate((int) window);
// bufferPositions.put(System.identityHashCode(buf),pos);
ByteBuffer buf = ByteBuffer.allocate((int) window);
bufferPositions.put(System.identityHashCode(buf),position);
position+=window;
return buf;
}
// 获取 buffer 对应的文件的 position
public long pos(ByteBuffer buf){
return bufferPositions.get(System.identityHashCode(buf));
}
// 创建,保持 NIO 风格
public static ByteMatricTextReadCompletionHandler open(long capacity,long window){
return new ByteMatricTextReadCompletionHandler(capacity,window);
}
/* *******************************
* 以下是构造
******************************* */
public ByteMatricTextReadCompletionHandler(long capacity,long window) {
this.capacity = capacity;
this.window = window;
this.metrix = new byte[(int) ((capacity + window-1)/window)][];
}
}
真正读取的方法
这里到是相对简洁的多
public void readByHandler(String path, String file, int size) {
try (
AsynchronousFileChannel channel = AsynchronousFileChannel.open(Paths.get(path+file), StandardOpenOption.READ)
){
ByteMatricTextReadCompletionHandler handler = ByteMatricTextReadCompletionHandler.open(channel.size(),100);
while(handler.isNotDone()){
System.out.println(Thread.currentThread().getName());
ByteBuffer buf = handler.commit();
channel.read(buf, handler.pos(buf),buf,handler);
}
// 不是必须的,但没有这句,测试用例等不到文件读完
TimeUnit.SECONDS.sleep(1);
System.out.println(handler.text());
} catch (Exception e){ /* 异常处理 */ }
}
异步效果
可以看到一堆 main 是 readByHandler
中打印的
其他的乱七八糟的是 ByteMatricTextReadCompletionHandler
的回调中打印的
侧面说明 AsynchronousFileChannel
的异步效果:read 和 handler 异步,handler 与 handler 之间异步
读取效果
一个正常的 JDK 按理说不应该提供实际使用中这么不友好的 API
因此,一个较大概率的可能是,AsynchronousFileChannel + CompletionHandler 并不是用来处理类似场景的
可能的实际场景: