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

Spring 6声明式HttpExchange发送Multipart表单为何调用Jackson?

问题分析

出现Jackson编码sun.nio.ch.ChannelInputStream的异常,核心原因是声明式Http客户端没有正确选择Multipart请求的编码器,反而 fallback到Jackson JSON编码器来处理MultipartFile的输入流:

  1. 自定义MultipartFile实现中getContentType()返回null,导致Spring无法识别该Part的类型,无法触发MultipartHttpMessageWriter;
  2. 自定义MultipartFile的getInputStream()和getBytes()共享同一个流,流被读取一次后就会耗尽,后续处理时Spring尝试序列化已耗尽的流对象,引发Jackson无法处理的类型错误;
  3. 手动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请求:

  1. 修改客户端接口:
@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);
}
  1. 调用时包装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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 23:03:23