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

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

在平时开发中,我们常常会遇到一些需要调用接口,使用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()获取准确值
相关推荐
aiopencode2 小时前
上架 iOS 应用到底在做什么?从准备工作到上架的流程
后端
未来之窗软件服务2 小时前
幽冥大陆(四十九)PHP打造Java的Jar实践——东方仙盟筑基期
java·php·jar·仙盟创梦ide·东方仙盟·东方仙盟sdk·东方仙盟一体化
普通网友2 小时前
深入探讨Linux驱动开发:字符设备驱动开发与测试_linux 驱动开发设备号(2)
java·linux·驱动开发
4Forsee2 小时前
【Android】动态操作 Window 的背后机制
android·java·前端
HappRobot2 小时前
python类和对象
开发语言·python
用户90443816324602 小时前
从40亿设备漏洞到AI浏览器:藏在浏览器底层的3个“隐形”原理
前端·javascript·浏览器
小二李2 小时前
第12章 koa框架重构篇 - Koa框架项目重构
java·前端·重构
cike_y2 小时前
JavaBean&MVC三层架构
java·架构·mvc·javaweb·java开发
鸡吃丸子2 小时前
React Native入门详解
开发语言·前端·javascript·react native·react.js