阿里巴巴OSS讲解 https://help.aliyun.com/zh/oss/how-to-obtain-oss-resources-by-segmenting-http-range-requests
一、先讲最重要的:InputStream 的本质是什么(脱离代码)
1、一句话定义(非常重要)
InputStream 不是"文件",而是:
👉 一个"按顺序读取字节的抽象通道"
它只关心三件事:
- 有没有数据
- 下一批数据在哪
- 什么时候读完(返回 -1)
2、InputStream 的三大"本质特性"
1️⃣ 它是「顺序模型」,不是「随机访问」
int b = inputStream.read();
你永远只能:读"下一个字节"
你不能:跳到任意位置(那是 RandomAccessFile 的事)
所以 InputStream 天然适合:
- 网络
- 管道
- 大文件
- 流式处理
2️⃣ 它不关心数据来源
下面这些 本质上完全一样:
java
new FileInputStream(...)
new SocketInputStream(...)
new GZIPInputStream(...)
new ByteArrayInputStream(...)
因为: InputStream 只是一个"水龙头接口"
至于水是井水、矿泉水、雨水,它不关心
3️⃣ read() = "向前消费"
int read(byte[] buf)
这意味着:
读过的数据:就"消失了"不能倒退
非常适合:大文件,实时数据
二、下面实现一个分片下载的FileInputStream
FileInputStream 在"逻辑上"做了什么?
一句话总览 把一个远程大文件 拆成多个 10MB 分片 后台并发下载 前台按
InputStream 顺序一个字节一个字节读
们把这个类拆成 5 层理解
🧱 第 1 层:文件分片模型
private static final long PART_SIZE = 10 * 1024 * 1024;
1、每个分片 10MB, 文件被拆成:
filePartCount = ceil(fileSize / PART_SIZE)
2、 Future = "未来会出现的文件"
List<Future > filePathList;
含义是: index 含义
0 第 0 个分片文件(还没下载)
1 第 1 个分片文件 ... ...
每一个 Future 表示:
"这个分片会在未来被下载到某个 Path"
3、线程池 = 后台下载引擎
Executors.newFixedThreadPool(Math.min(filePartCount, 5))
最多 5 个并发下载
控制带宽、内存、文件句柄
4、getInputStream() = 核心魔法
Path path = pathFuture.get();
return Files.newInputStream(path);
这里发生了什么?
如果这个分片没下载: 提交下载任务
阻塞等待当前分片完成
打开本地临时文件
返回一个 真正的文件 InputStream
👉 这一步是"假流 → 真文件流"的转换点
5、read() = 把多个流拼成一个
关键逻辑:
java
int byteRead = currentInputStream.read(...);
if (byteRead == -1) {
// 当前分片读完
close
deleteCurrentFile
切换下一个分片
}
也就是说:
分片0 InputStream\] → EOF ↓ 自动切换 ↓ \[分片1 InputStream\] → EOF ↓ 自动切换
对调用方来说:
感觉就像一个连续的大文件
完全感知不到分片存在
6、这个 FileInputStream "欺骗"了谁?
它成功地"骗"了:
GZIPInputStream
InputStreamReader
BufferedReader
因为:
它完美遵守了 InputStream 的契约
👉 read() / -1 / 顺序读取
7、为什么这种设计非常高级?
1️⃣ 极度省内存
不下载完整文件
不缓存整个 gzip
只保留当前分片
2️⃣ IO 与计算并行
后台:下载
前台:解压 + 解析
3️⃣ 完美适配 Java IO 生态
任何接受 InputStream 的地方都能用
GZIP / JSON / CSV / XML 全通吃
8、InputStream 的"哲学总结"
InputStream 不是"文件读取工具",
而是 Java 世界里最重要的一种"数据抽象协议"
你这份代码,本质是在说:
"不管数据从哪里来,只要我能按顺序吐字节,
整个 Java IO 世界都会配合我工作。"
java
@Getter
@Slf4j
public class FileInputStream extends InputStream {
private static final long PART_SIZE = 10 * 1024 * 1024;
private final String fileName;
private final Long fileSize;
private final String fileUrl;
private final int filePartCount;
private final List<Future<Path>> filePathList;
private final ExecutorService threadPool;
private int currentPartIndex;
private InputStream currentInputStream;
public FileInputStream(String fileName, Long fileSize, String fileUrl) {
this.fileName = fileName;
this.fileSize = fileSize;
this.fileUrl = fileUrl;
this.filePartCount = filePartCount(this.fileSize);
this.filePathList = Lists.newArrayListWithCapacity(filePartCount);
this.threadPool = Executors.newFixedThreadPool(Math.min(filePartCount, 5));
this.currentPartIndex = 0;
for (int i = 0; i < this.filePartCount; i++) {
this.filePathList.add(null);
}
}
private static int filePartCount(long totalSize) {
return (int) Math.ceil((double) totalSize / PART_SIZE);
}
@Override
public int read(@NotNull byte[] b, int off, int len) throws IOException {
try {
if (currentInputStream == null) {
if (currentPartIndex >= filePartCount) {
return -1;
}
currentInputStream = getInputStream();
}
int byteRead = currentInputStream.read(b, off, len);
if (byteRead == -1 || byteRead < len) {
// 当前 InputStream 读取完毕,立即关闭
currentInputStream.close();
deleteCurrentFile();
if (currentPartIndex + 1 < filePartCount) {
// 切换到下一个 InputStream
currentPartIndex++;
currentInputStream = getInputStream();
int remainBytes = len - Math.max(byteRead, 0);
byteRead += currentInputStream.read(b, byteRead, remainBytes);
} else {
// 所有 InputStream 读取完毕
currentInputStream = null;
}
}
return byteRead;
} catch (Exception e) {
throw new IOException(e);
}
}
public int read() throws IOException {
try {
if (currentInputStream == null) {
if (currentPartIndex >= filePartCount) {
return -1;
}
currentInputStream = getInputStream();
}
int byteRead = currentInputStream.read();
if (byteRead == -1) {
// 当前 InputStream 读取完毕,立即关闭
currentInputStream.close();
deleteCurrentFile();
if (currentPartIndex + 1 < filePartCount) {
// 切换到下一个 InputStream
currentPartIndex++;
currentInputStream = getInputStream();
byteRead = currentInputStream.read();
} else {
// 所有 InputStream 读取完毕
currentInputStream = null;
}
}
return byteRead;
} catch (Exception e) {
throw new IOException(e);
}
}
@Override
public void close() throws IOException {
if (currentInputStream != null) {
currentInputStream.close();
deleteCurrentFile();
}
if (threadPool != null) {
threadPool.shutdownNow();
}
if (CollectionUtils.isNotEmpty(filePathList)) {
filePathList.clear();
}
deleteTempDir();
}
private void deleteCurrentFile() throws IOException {
try {
Future<Path> pathFuture = filePathList.get(currentPartIndex);
if (pathFuture == null) {
return;
}
Path path = pathFuture.get();
if (path == null) {
return;
}
File file = path.toFile();
if (file.isFile() && file.exists()) {
if (!file.delete()) {
file.deleteOnExit();
}
log.info("[DM] file deleted " + file.getPath());
}
} catch (Exception e) {
throw new IOException(e);
}
}
private void deleteTempDir() throws IOException {
Path path = getTempPath(this.fileName);
if (Files.notExists(path)) {
return;
}
deleteDirectory(path.toFile());
}
private void deleteDirectory(File dic) {
File[] files = dic.listFiles();
if (files != null) {
for (File file : files) {
deleteDirectory(file);
}
}
dic.delete();
}
private InputStream getInputStream() throws Exception {
if (currentPartIndex >= this.filePathList.size()) {
throw new IOException("超出文件分片总数");
}
Future<Path> pathFuture = this.filePathList.get(currentPartIndex);
if (pathFuture == null) {
pathFuture = this.threadPool.submit(buildDownloadTask(currentPartIndex));
this.filePathList.set(currentPartIndex, pathFuture);
}
if (currentPartIndex + 1 < filePartCount) {
// 下载下一个文件
filePathList.set(currentPartIndex + 1, threadPool.submit(buildDownloadTask(currentPartIndex + 1)));
}
// 开始解析
Path path = pathFuture.get();
return Files.newInputStream(path);
}
private DownloadTask buildDownloadTask(int partIndex) {
long startPos = partIndex * PART_SIZE;
long endPos = startPos + PART_SIZE - 1;
if (partIndex == filePartCount - 1) {
endPos = fileSize;
}
return new DownloadTask(startPos, endPos, partIndex);
}
private class DownloadTask implements Callable<Path> {
private static final int MAX_RETRY_TIMES = 5;
private static final int RETRY_SLEEP_MILLIS = 1000;
private final long startPos;
private final long endPos;
private final int serialNum;
public DownloadTask(long startPos, long endPos, int serialNum) {
this.startPos = startPos;
this.endPos = endPos;
this.serialNum = serialNum;
}
@Override
public Path call() throws Exception {
int retryTimes = 0;
do {
try {
return doCall();
} catch (Exception e) {
// 所有异常都重试
log.error("下载文件 {} 分片 {} 失败", fileName, serialNum, e);
if (!shouldRetry(retryTimes)) {
break;
}
}
} while (retryTimes++ < MAX_RETRY_TIMES);
log.error("下载文件 {} 分片 {} 重试达到最大次数 {}", fileName, serialNum, MAX_RETRY_TIMES);
throw new Exception("下载文件异常,超出重试次数");
}
private boolean shouldRetry(int retryTimes) {
if (retryTimes + 1 > MAX_RETRY_TIMES) {
//最后一次重试失败后,直接抛出异常,不再等待
return false;
}
int sleepMillis = RETRY_SLEEP_MILLIS * (1 << retryTimes);
try {
log.info("下载文件 {} 分片 {} 失败,将在 {} ms 后重试(第{}次)", fileName, serialNum, sleepMillis, retryTimes + 1);
Thread.sleep(sleepMillis);
} catch (InterruptedException e1) {
Thread.currentThread().interrupt();
}
return true;
}
public Path doCall() throws Exception {
String rangeStr = "bytes=" + startPos + "-" + endPos;
log.info("begin download file {} serialNum is {} range is {}", fileName, serialNum, rangeStr);
CloseableHttpClient httpClient = FileHttpHelper.getClient(fileUrl);
HttpGet httpGet = new HttpGet(fileUrl);
httpGet.addHeader("Range", rangeStr);
httpGet.setConfig(RequestConfig.custom()
// 建立连接的时间
.setConnectTimeout(30 * 1000)
// 读取数据的时间,一个分片大小10M,给30分钟的时间足够了
.setSocketTimeout(30 * 1000 * 60)
.build());
try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
int code = response.getStatusLine().getStatusCode();
boolean success = code >= 200 && code < 300;
if (!success) {
throw new Exception("Failed to download file. Server returned HTTP status code: " + code);
}
InputStream inputStream = response.getEntity().getContent();
Path tmpPath = getTempPath(fileName);
if (Files.notExists(tmpPath)) {
log.info("create temp directory:{}", tmpPath);
Files.createDirectories(tmpPath);
}
Path tempFilePath = tmpPath.resolve(String.valueOf(serialNum));
// 将下载的内容写到临时文件,每个任务写一个临时文件
log.info("begin write to temp file:{}", tempFilePath);
long byteSize = Files.copy(inputStream, tempFilePath, StandardCopyOption.REPLACE_EXISTING);
if (byteSize != PART_SIZE && startPos + byteSize != endPos) {
log.info("write to temp file:{} size:{} error", tempFilePath, byteSize);
throw new Exception("byteSizeError");
}
log.info("write to temp file:{} finished", tempFilePath);
inputStream.close();
return tempFilePath;
}
}
}
private static Path getTempPath(String fileName) {
String tmpDirPath = System.getProperty("java.io.tmpdir");
return Paths.get(tmpDirPath, "dmDownload", fileName);
}
}
三、流式解析FileInputStream.
如下代码:
用通俗易懂的语言讲一下
1、InputStream = 水龙头(出水口)调用 read() = 接水,你只管"有没有水流出来"
2、GZIPInputStream 本身不是"水源", 它是一个"装在水龙头前面的过滤器 / 转换器"。 换句话说: 它既是一个InputStream, 又"消费"另一个 InputStream。
3、FileInputStream(分片下载实现)= 水源 + 水管 new FileInputStream(fileName, fileSize, fileUrl)
水源:远程 gzip 文件
水管:分片下载 + 本地缓存 + 顺序拼接
对外:稳定、连续地出"压缩过的水"
4、GZIPInputStream 要"包"一个 InputStream?因为它遵守一个非常牛的设计模式:
👉 装饰器模式(Decorator)
特点:
接口不变:还是 InputStream
功能增强:多了"解压"
可无限叠加
5、完整"水流路径"示意图(重点) 远程 gzip 文件
远程 gzip 文件
↓ [你的 FileInputStream] (分片下载 + 拼接)
↓ 压缩字节 [GZIPInputStream] (解压)
↓ 原始字节 [InputStreamReader] (字节 → 字符)
↓ [BufferedReader] (按行读)
6、InputStreamReader 是什么?和BufferedReader区别是什么
本质定义
InputStreamReader extends Reader
它是"字符流世界"与"字节流世界"的桥梁
3️⃣ 用水流模型理解
字节流(byte)
↓ UTF-8 / GBK / ISO-8859-1
InputStreamReader
↓ 字符流(char)
和BufferedReader 区别是什么?
BufferedReader extends Reader
它是一个**"字符流的缓存增强器"**
7、 没有 BufferedReader 会怎样?
reader.read(); // 每次一个字符
每次 read:都可能触发一次底层 IO 性能极差
BufferedReader 干了什么? char[] buffer = new char[8192];
一次性读一大块存到内存
上层:
一个字符一个字符地用
👉 典型的 空间换时间
BufferedReader 的"杀手级能力" ⭐ readLine() String line =
bufferedReader.readLine();
自动识别:
\n
\r\n
非常适合:
日志
CSV
JSON
文本文件
一句话区分:
| 类 | 解决的问题 | 装饰的是什么 |
|---|---|---|
| InputStreamReader | 字节 → 字符 | 数据"语义" |
| BufferedReader | 读得快、读得巧 | 读取"方式" |
java
@SneakyThrows
@Override protected BufferedReader initReader() {
String fileName = RandomUtils.generateUUIDString();
String fileUrl = fileInfo.getUrl();
InputStream fis = new GZIPInputStream(new FileInputStream(fileName, fileInfo.getFileSize(), fileUrl));
return new BufferedReader(new InputStreamReader(fis, StandardCharsets.UTF_8));
}
java
@Override
public void start() {
// 使用示例
this.bufferedReader = initReader();
// 这里使用新的CsvReader Builder方式
CsvReader<CsvRecord> csvReader = CsvReader.builder()
.quoteCharacter('`')
.ofCsvRecord(bufferedReader);
csvRecords = csvReader.iterator();
// 解析表结构
readHead();
parseDictEncode();
// 跳过表头
csvRecords.next();
// 初始化读取数据迭代器
initDataIterator();
}