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

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

在平时开发中,我们常常会遇到一些需要调用接口,使用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()获取准确值
相关推荐
我命由我123451 分钟前
Function Calling 模型
人工智能·python·学习·ai·学习方法·ai编程·ai写作
有味道的男人7 分钟前
搜好货平台关键词商品搜索接口:分页容错与商品数据轻量化提取方案
服务器·前端·javascript
silvamux_10 分钟前
Spring Boot 调用大模型 api 最小可跑教程
java·spring boot·后端
今年下半年12 分钟前
【Spring Boot 】通过 OpenFeign 请求第三方接口
java·spring boot·后端·openfeign
范什么特西22 分钟前
亲和路由(inspect)
java
她说可以呀24 分钟前
Docker 镜像的两种创建方式
java·分布式·docker
霸道流氓气质34 分钟前
AgentFlow 入门与实战:Task-Based 并发、EWMA 动态调度与四道门禁的 Java 生产级编排
java·开发语言
XMXFS41 分钟前
小红书推荐算法底层逻辑解析:用数据思维做内容优化
python·算法·新媒体运营·推荐算法
hans77488296843 分钟前
仅根据BMI反推身高和体重?还真能做到!只需要简单的数论知识~
python·数学
UrbanJazzerati44 分钟前
AutoHotkey v2 零基础快速入门 Part 3 像素亮度检测完全指南
后端