快速自定义一个带进度监控的文件资源类

快速自定义一个带进度监控的文件资源类

在平时开发中,我们常常会遇到一些需要调用接口,使用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)

实现原理剖析

  1. 自定义Resource类

    • 继承AbstractResource实现Spring的资源抽象
    • 实现InputStreamSource接口提供输入流
    • 重写getInputStream()返回带监控的输入流
  2. 进度监控流

    • 包装原始FileInputStream
    • 重写read()方法统计读取字节数
    • 每次读取后通过回调接口通知进度
  3. 回调机制

    • 使用函数式接口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;
    }
}

生产环境注意事项

  1. 性能优化

    • 避免在回调中执行耗时操作
    • 使用采样率控制回调频率
    • 对大量小文件上传禁用进度监控
  2. 异常处理

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
    try {
    // ...原有逻辑
    } catch (IOException e) {
    callback.onError(e);
    throw e;
    }
    }

  3. 资源释放

    @Override
    public void close() throws IOException {
    try {
    delegate.close();
    } finally {
    callback.onComplete();
    }
    }

  4. 大文件支持

  • 使用内存映射文件优化读取性能

    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()获取准确值
相关推荐
dhdjjsjs1 分钟前
Day58 PythonStudy
开发语言·python·机器学习
七七powerful3 分钟前
docker28.1.1和docker-compose v.2.35.1安装
java·docker·eureka
你真的可爱呀4 分钟前
自定义颜色选择功能
开发语言·前端·javascript
小王和八蛋8 分钟前
JS中 escape urlencodeComponent urlencode 区别
前端·javascript
奔跑的web.9 分钟前
TypeScript类型系统核心速通:从基础到常用复合类型包装类
开发语言·前端·javascript·typescript·vue
Misnice9 分钟前
Webpack、Vite 、Rsbuild 区别
前端·webpack·node.js
小白学大数据12 分钟前
百科词条结构化抓取:Java 正则表达式与 XPath 解析对比
java·开发语言·爬虫·正则表达式
Kagol13 分钟前
🎉历时1年,TinyEditor v4.0 正式发布!
前端·typescript·开源
丶一派胡言丶13 分钟前
02-VUE介绍和指令
前端·javascript·vue.js
C_心欲无痕15 分钟前
网络相关 - 跨域解决方式
前端·网络