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

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

在平时开发中,我们常常会遇到一些需要调用接口,使用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()获取准确值
相关推荐
涡能增压发动积14 小时前
同样的代码循环 10次正常 循环 100次就抛异常?自定义 Comparator 的 bug 让我丢尽颜面
后端
云烟成雨TD14 小时前
Spring AI Alibaba 1.x 系列【6】ReactAgent 同步执行 & 流式执行
java·人工智能·spring
Csvn14 小时前
🌟 LangChain 30 天保姆级教程 · Day 13|OutputParser 进阶!让 AI 输出自动转为结构化对象,并支持自动重试!
python·langchain
Wenweno0o14 小时前
0基础Go语言Eino框架智能体实战-chatModel
开发语言·后端·golang
于慨14 小时前
Lambda 表达式、方法引用(Method Reference)语法
java·前端·servlet
石小石Orz14 小时前
油猴脚本实现生产环境加载本地qiankun子应用
前端·架构
swg32132114 小时前
Spring Boot 3.X Oauth2 认证服务与资源服务
java·spring boot·后端
从前慢丶14 小时前
前端交互规范(Web 端)
前端
tyung14 小时前
一个 main.go 搞定协作白板:你画一笔,全世界都看见
后端·go
gelald14 小时前
SpringBoot - 自动配置原理
java·spring boot·后端