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
相关产品推荐
相关产品推荐

