快速自定义一个带进度监控的文件资源类
在平时开发中,我们常常会遇到一些需要调用接口,使用form-data方式向接口上传本地文件的request操作。今天记录一个好用的带进度监控的文件资源管理类的开发,实现代码如下:
import org.springframework.core.io.AbstractResource;
import org.springframework.core.io.InputStreamSource;
import java.io.InputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class ProgressMonitorFileResource extends AbstractResource implements InputStreamSource {
private final File file;
private final ProgressCallback callback;
// 进度回调接口
public interface ProgressCallback {
void update(long bytesRead, long contentLength);
}
public ProgressMonitorFileResource(File file, ProgressCallback callback) {
this.file = file;
this.callback = callback;
}
@Override
public String getDescription() {
return "File: " + file.getAbsolutePath();
}
@Override
public InputStream getInputStream() throws IOException {
return new ProgressMonitoringInputStream(
new FileInputStream(file),
file.length(),
callback
);
}
@Override
public long contentLength() throws IOException {
return file.length();
}
@Override
public String getFilename() {
return file.getName();
}
// 带进度监控的输入流
private static class ProgressMonitoringInputStream extends InputStream {
private final InputStream delegate;
private final long totalBytes;
private long bytesRead = 0;
private final ProgressCallback callback;
public ProgressMonitoringInputStream(InputStream delegate,
long totalBytes,
ProgressCallback callback) {
this.delegate = delegate;
this.totalBytes = totalBytes;
this.callback = callback;
}
@Override
public int read() throws IOException {
int data = delegate.read();
if (data != -1) {
bytesRead++;
notifyProgress();
}
return data;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int bytes = delegate.read(b, off, len);
if (bytes != -1) {
bytesRead += bytes;
notifyProgress();
}
return bytes;
}
private void notifyProgress() {
if (callback != null) {
callback.update(bytesRead, totalBytes);
}
}
@Override
public void close() throws IOException {
delegate.close();
}
}
}
在现有代码中的集成方式
可以很方便地在我们已有的代码里面调用它:
private HttpEntity<MultiValueMap<String, Object>> buildMultipartRequest(File pdfFile) {
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
// 使用带进度监控的资源
body.add("file", new ProgressMonitorFileResource(pdfFile, (read, total) -> {
double progress = (double) read / total * 100;
System.out.printf("上传进度: %.2f%% (%d/%d bytes)%n",
progress, read, total);
}));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
return new HttpEntity<>(body, headers);
}
实现效果示例
当上传一个100MB的PDF文件时,控制台会输出:
上传进度: 0.24% (262144/104857600 bytes)
上传进度: 0.49% (524288/104857600 bytes)
上传进度: 0.73% (786432/104857600 bytes)
...
上传进度: 99.99% (104857600/104857600 bytes)
实现原理剖析
-
自定义Resource类:
- 继承
AbstractResource实现Spring的资源抽象 - 实现
InputStreamSource接口提供输入流 - 重写
getInputStream()返回带监控的输入流
- 继承
-
进度监控流:
- 包装原始
FileInputStream - 重写
read()方法统计读取字节数 - 每次读取后通过回调接口通知进度
- 包装原始
-
回调机制:
- 使用函数式接口
ProgressCallback - 支持Lambda表达式实现自定义处理逻辑
- 使用函数式接口
高级配置选项
1. 进度采样(降低回调频率)
private void notifyProgress() {
if (callback != null && (bytesRead % 65536 == 0 || bytesRead == totalBytes)) {
callback.update(bytesRead, totalBytes);
}
}
2. 线程安全处理(适用于UI更新)
// 在回调中使用Platform.runLater更新UI
body.add("file", new ProgressMonitorFileResource(pdfFile, (read, total) -> {
Platform.runLater(() -> {
progressBar.setProgress((double) read / total);
});
}));
3. 上传速度计算
// 在回调中添加时间统计
private long lastUpdateTime = System.currentTimeMillis();
private void notifyProgress() {
long now = System.currentTimeMillis();
long elapsed = now - lastUpdateTime;
if (elapsed > 1000) { // 每秒更新一次
double speed = (bytesRead - lastBytes) / (elapsed / 1000.0);
System.out.printf("速度: %.2f KB/s%n", speed / 1024);
lastUpdateTime = now;
lastBytes = bytesRead;
}
}
生产环境注意事项
-
性能优化:
- 避免在回调中执行耗时操作
- 使用采样率控制回调频率
- 对大量小文件上传禁用进度监控
-
异常处理:
@Override
public int read(byte[] b, int off, int len) throws IOException {
try {
// ...原有逻辑
} catch (IOException e) {
callback.onError(e);
throw e;
}
} -
资源释放:
@Override
public void close() throws IOException {
try {
delegate.close();
} finally {
callback.onComplete();
}
} -
大文件支持:
-
使用内存映射文件优化读取性能
public InputStream getInputStream() throws IOException {
return new ProgressMonitoringInputStream(
Files.newInputStream(file.toPath()),
file.length(),
callback
);
}
完整调用示例
public static void main(String[] args) {
File pdfFile = new File("large-file.pdf");
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("file", new ProgressMonitorFileResource(pdfFile,
(read, total) -> {
System.out.println("已上传: " + humanReadableByteCount(read)
+ "/" + humanReadableByteCount(total));
}));
// 发送请求(使用RestTemplate)
// ...
}
// 辅助方法:格式化字节显示
private static String humanReadableByteCount(long bytes) {
int unit = 1024;
if (bytes < unit) return bytes + " B";
int exp = (int) (Math.log(bytes) / Math.log(unit));
String pre = "KMGTPE".charAt(exp-1) + "i";
return String.format("%.1f %sB",
bytes / Math.pow(unit, exp),
pre);
}
常见问题解决方案
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 进度回调不触发 | 文件太小(<1MB) | 增加采样间隔 |
| 上传速度异常 | 网络波动 | 添加平滑处理(移动平均) |
| 内存占用过高 | 未使用流式上传 | 确保使用InputStream方式 |
| 进度超过100% | 文件大小计算不准确 | 使用File.length()获取准确值 |