You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring WebFlux用WebClient与AsynchronousFileChannel下载出现空文件问题

问题根因定位

核心原因1:反应式管道从未被触发执行

  • 你在saveFileOnComputer方法中调用DataBufferUtils.write(...)返回的是冷Mono<Void>类型的写入任务流,但你仅链式调用了.then(),既没有调用subscribe()触发执行,也没有调用block()等待任务完成。
  • 按照反应式流的规范,没有订阅者的冷Publisher不会执行任何内部逻辑:WebClient不会实际发起HTTP请求拉取图片内容,DataBuffer的写入操作完全不会触发。
  • 空文件出现的本质是:代码提前执行了AsynchronousFileChannel.open(),仅完成了空文件的创建动作,没有任何数据写入流程执行,自然文件大小为0。

核心原因2:DataBuffer释放逻辑错误

  • 原代码中.doOnNext(DataBufferUtils.releaseConsumer())的回调触发时机存在问题:doOnNext会在每个DataBuffer从流中取出、还未执行写入逻辑时就触发内存释放,会直接导致底层内存被回收,就算管道被正常订阅,也可能出现写入内容为空、数据损坏的问题。

次要隐患

  • 手动打开的AsynchronousFileChannel依赖管道执行后的回调关闭,管道未订阅时会造成文件句柄泄漏
  • for循环中串行创建下载流的逻辑没有并发控制,后续如果触发大量任务执行,容易一次性打满HTTP连接池与文件句柄资源

保留AsynchronousFileChannel方案的修正实现
public void downloadFilesFromUrl() throws IOException {
    List<Photo> notDownloadedFiles = // 获取待下载的照片列表
    for (Photo photo : notDownloadedFiles) {
        Path path = Paths.get(pathToFiles + File.separator + photo.getPhotoName());
        WebClient client = WebClient.builder().baseUrl(photo.getLoadSource()).build();
        Flux<DataBuffer> dataBufferFlux = client
                .get().accept(MediaType.APPLICATION_OCTET_STREAM)
                .retrieve()
                .bodyToFlux(DataBuffer.class);
        // 显式触发订阅,等待单文件写入流程完整执行
        saveFileOnComputer(path, dataBufferFlux).block();
    }
}

private Mono<Void> saveFileOnComputer(Path path, Flux<DataBuffer> dataBufferFlux) throws IOException {
    // 补充TRUNCATE_EXISTING参数,避免写入时残留旧文件内容
    AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(
        path,
        StandardOpenOption.CREATE,
        StandardOpenOption.WRITE,
        StandardOpenOption.TRUNCATE_EXISTING
    );
    return DataBufferUtils.write(dataBufferFlux, asynchronousFileChannel)
            // 仅在DataBuffer被丢弃、未完成写入时触发释放,避免提前回收内存
            .doOnDiscard(DataBuffer.class, DataBufferUtils.releaseConsumer())
            .doAfterTerminate(() -> {
                try {
                    asynchronousFileChannel.close();
                } catch (IOException ignored) { }
            });
}

对照验证

你之前替换为DataBufferUtils.write(dataBufferFlux, path, StandardOpenOption.CREATE).block()可以正常运行,本质是两个关键点同时满足:

  1. 最后显式调用了.block(),触发了整个反应式管道的订阅执行
  2. 方法内部自动完成了AsynchronousFileChannel的创建、位置维护、资源关闭全流程,没有提前释放DataBuffer的逻辑错误

内容的提问来源于stack exchange,提问作者izzager

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.24 04:45:03