Spring 6声明式HttpExchange发送Multipart表单为何调用Jackson?
问题分析
出现Jackson编码sun.nio.ch.ChannelInputStream的异常,核心原因是声明式Http客户端没有正确选择Multipart请求的编码器,反而 fallback到Jackson JSON编码器来处理MultipartFile的输入流:
- 自定义
MultipartFile实现中getContentType()返回null,导致Spring无法识别该Part的类型,无法触发MultipartHttpMessageWriter; - 自定义
MultipartFile的getInputStream()和getBytes()共享同一个流,流被读取一次后就会耗尽,后续处理时Spring尝试序列化已耗尽的流对象,引发Jackson无法处理的类型错误; - 手动WebClient能正常工作是因为手动构建Multipart请求时,明确通过
MultipartBodyBuilder指定了Part的处理逻辑,强制使用Multipart编码器,而声明式客户端依赖参数注解和类型自动匹配编码器。
解决方案
方案一:修复自定义MultipartFile实现
调整MultipartFile的实现,确保contentType有效,同时避免流被重复读取耗尽:
try (ReadChannel reader = storage.reader(blobId)) { // 先将流读取到字节数组,避免流耗尽问题 byte[] fileBytes = FileCopyUtils.copyToByteArray(Channels.newInputStream(reader)); MultipartFile file = new MultipartFile() { @Override public String getName() { return webhookRequest.name(); } @Override public String getOriginalFilename() { return webhookRequest.name(); } // 返回有效的MIME类型,让Spring识别为文件Part @Override public String getContentType() { return MediaType.APPLICATION_OCTET_STREAM_VALUE; // 若知道文件类型,可替换为对应类型,比如MediaType.IMAGE_JPEG_VALUE } @Override public boolean isEmpty() { return fileBytes.length == 0; } @Override public long getSize() { return fileBytes.length; } @Override public byte[] getBytes() throws IOException { return fileBytes; } // 每次返回新的ByteArrayInputStream,避免流耗尽 @Override public InputStream getInputStream() throws IOException { return new ByteArrayInputStream(fileBytes); } @Override public void transferTo(File dest) throws IOException, IllegalStateException { FileCopyUtils.copy(fileBytes, Files.newOutputStream(dest.toPath())); } }; myClient.uploadFile(wildfireApiKey, file); }
方案二:改用Resource类型代替MultipartFile
Spring对Resource类型的支持更完善,可直接适配Multipart请求:
- 修改客户端接口:
@HttpExchange( url = "/publicapi", contentType = MediaType.MULTIPART_FORM_DATA_VALUE, accept = MediaType.TEXT_XML_VALUE ) public interface MyClient { @PostExchange(url = "/submit/file", contentType = MediaType.MULTIPART_FORM_DATA_VALUE) ResponseEntity<Void> uploadFile(@RequestPart String apiKey, @RequestPart(value = "file") Resource file); }
- 调用时包装GCS流为
InputStreamResource:
try (ReadChannel reader = storage.reader(blobId)) { InputStream inputStream = Channels.newInputStream(reader); Resource file = new InputStreamResource(inputStream) { @Override public String getFilename() { return webhookRequest.name(); } @Override public long contentLength() throws IOException { return webhookRequest.size(); } }; myClient.uploadFile(wildfireApiKey, file); }
方案三:显式配置WebClient的Multipart编码器
确保WebClient优先使用MultipartHttpMessageWriter处理Multipart请求:
@Bean MyClient myClient(MyClientProperties properties) { WebClient webClient = WebClient.builder() .baseUrl(properties.getUrl()) .defaultStatusHandler( httpStatusCode -> HttpStatus.NOT_FOUND == httpStatusCode, response -> Mono.empty()) .defaultStatusHandler( HttpStatusCode::is5xxServerError, response -> Mono.error(new RuntimeException("WildfireClient request failed. Code: " + response.statusCode().value()))) .codecs(configurer -> { // 显式注册Multipart编码器,确保优先级 MultipartHttpMessageWriter multipartWriter = new MultipartHttpMessageWriter(); configurer.customCodecs().register(multipartWriter); }) .build(); return HttpServiceProxyFactory .builder(WebClientAdapter.forClient(webClient)) .build() .createClient(MyClient.class); }
内容的提问来源于stack exchange,提问作者Max Bachhuber
相关产品推荐
相关产品推荐

