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

Spring Reactive使用WebClient传输大图片时出现NoSuchFileException

问题分析与修复方案

问题根源

当上传的图片超过Spring WebFlux默认内存存储阈值时,框架会将文件写入系统临时目录。但你的代码在返回响应后,Spring会立即清理这些临时文件,而异步处理还在尝试读取文件,因此触发NoSuchFileException。

修复方案

方案一:提前读取文件内容,摆脱临时文件依赖

通过将文件内容提前读取到内存(或自定义临时存储),避免依赖Spring自动清理的临时文件。修改代码如下:

异步服务方法修改

public Mono<Void> handlerAsync(MultiValueMap<String, Part> fileParts) {
    return Flux.fromIterable(fileParts.entrySet())
            .flatMap(entry -> {
                FilePart filePart = (FilePart) entry.getValue().get(0);
                // 读取文件内容到内存ByteArrayResource
                return filePart.content()
                        .collectList()
                        .map(dataBuffers -> {
                            byte[] bytes = DataBufferUtils.join(dataBuffers)
                                    .map(buffer -> {
                                        byte[] result = new byte[buffer.readableByteCount()];
                                        buffer.read(result);
                                        DataBufferUtils.release(buffer);
                                        return result;
                                    })
                                    .orElse(new byte[0]);
                            return new AbstractMap.SimpleEntry<>(entry.getKey(), new FilePartResource(filePart.filename(), new ByteArrayResource(bytes)));
                        });
            })
            .collectMap(Map.Entry::getKey, Map.Entry::getValue)
            .flatMap(preparedParts -> {
                MultipartBodyBuilder multipartBodyBuilder = new MultipartBodyBuilder();
                preparedParts.forEach((key, filePartResource) -> {
                    multipartBodyBuilder.part(key, filePartResource.getResource())
                            .filename(filePartResource.getFilename())
                            .header("DocType", "INSPECTION");
                });
                MultiValueMap<String, HttpEntity<?>> requestBody = multipartBodyBuilder.build();
                return WebClient.builder()
                        .codecs(clientCodecConfigurer -> clientCodecConfigurer.defaultCodecs().maxInMemorySize(1024 * 1024 * 16))
                        .build()
                        .post().uri("http://localhost:8081/receive")
                        .bodyValue(requestBody)
                        .retrieve()
                        .onStatus(httpStatusCode -> httpStatusCode.is4xxClientError() || httpStatusCode.is5xxServerError(), cr ->
                                cr.createException().map(webClientResponseException -> {
                                    System.out.println(webClientResponseException.getResponseBodyAsString());
                                    System.out.println(webClientResponseException.getStatusCode());
                                    return webClientResponseException;
                                }))
                        .bodyToMono(Void.class)
                        .retryWhen(Retry.fixedDelay(2, Duration.ofSeconds(2)))
                        .onErrorResume(throwable -> {
                            if (Exceptions.isRetryExhausted(throwable)) {
                                return Mono.error(new RuntimeException("retry exhausted"));
                            }
                            return Mono.error(new RuntimeException("error"));
                        });
            });
}

// 辅助类:包装文件名与内存资源
static class FilePartResource {
    private final String filename;
    private final ByteArrayResource resource;

    public FilePartResource(String filename, ByteArrayResource resource) {
        this.filename = filename;
        this.resource = resource;
    }

    public String getFilename() {
        return filename;
    }

    public ByteArrayResource getResource() {
        return resource;
    }
}

请求处理方法优化

@PostMapping(value = "/submit")
public Mono<ResponseEntity<String>> handleFormSubmit(ServerWebExchange serverWebExchange) {
    return serverWebExchange.getMultipartData()
            .filter(map -> !CollectionUtils.isEmpty(map))
            .doOnNext(map -> {
                // 指定后台线程池执行异步任务,不阻塞请求响应
                asynchronousService.handlerAsync(map)
                        .subscribeOn(Schedulers.boundedElastic())
                        .subscribe(
                                null,
                                error -> System.err.println("异步处理失败: " + error.getMessage())
                        );
            })
            .map(filePart -> ResponseEntity.ok("success"))
            .switchIfEmpty(Mono.just(ResponseEntity.badRequest().body("bad")));
}

方案二:调整Spring Multipart临时文件配置(不推荐)

通过配置关闭请求结束后的临时文件自动清理,或自定义临时目录。但此方式需手动管理文件清理,否则会导致磁盘空间占用过高:

spring:
  webflux:
    multipart:
      max-in-memory-size: 10MB
      temp-dir: /custom-multipart-temp
      cleanup-after-request: false

关键说明

  • 核心矛盾是Spring临时文件生命周期与异步处理周期不匹配:请求结束后临时文件被清理,而异步任务尚未完成。
  • 方案一通过预读取文件内容到内存,彻底解决依赖问题,是更可靠的生产级方案。若文件超过内存阈值,可扩展为写入自定义临时目录,处理完成后手动删除。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 17:35:54