Why does the HttpExchange (declarative HttpClient) in Spring 6 use Jackson when trying to send multipart form-data?

huangapple go评论75阅读模式
英文:

Why does the HttpExchange (declarative HttpClient) in Spring 6 use Jackson when trying to send multipart form-data?

问题

I am trying to implement a REST API client using the new declarative HttpClient in Spring 6.

我正在尝试使用Spring 6中的新声明式HttpClient来实现REST API客户端。

I have defined my client like that:

我已经定义了我的客户端如下:

Then I have declared a Bean for that client:

然后我为该客户端声明了一个Bean:

And then I am trying to use the client to upload a file from Google Cloud Storage:

然后我尝试使用客户端从Google Cloud Storage上传文件:

But something strange happens. I get an error that Jackson cannot encode the value:

但出现了奇怪的情况。我收到一个错误,Jackson无法对该值进行编码:

Why is Jackson involved here? The body of the request should be multipart form data. So no JSON conversion should be done here...

为什么这里涉及到Jackson?请求的主体应该是多部分表单数据。因此,这里不应该进行JSON转换...

I have also tried to implement it via manual WebClient instantiation, and that worked without problems. But I really want to use the new declarative client.

我还尝试了通过手动创建WebClient实例来实现它,而且没有任何问题。但我真的想使用新的声明式客户端。

英文:

i am trying to implement a REST API client using the new declarative HttpClient in Spring 6.

I have defined my client like that:

@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") MultipartFile file);
}

Then i have declared a Bean for that client:

    @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())))
                .build();

        return HttpServiceProxyFactory
                .builder(WebClientAdapter.forClient(webClient))
                .build()
                .createClient(MyClient.class);
    }

And then i am trying to use the client to upload a file from Google Cloud Storage:

...
        try (ReadChannel reader = storage.reader(blobId)) {
            var inputStream = Channels.newInputStream(reader);

            MultipartFile file = new MultipartFile() {
                @Override
                public String getName() {
                    return webhookRequest.name();
                }

                @Override
                public String getOriginalFilename() {
                    return webhookRequest.name();
                }

                @Override
                public String getContentType() {
                    return null;
                }

                @Override
                public boolean isEmpty() {
                    return false;
                }

                @Override
                public long getSize() {
                    return webhookRequest.size();
                }

                @Override
                public byte[] getBytes() throws IOException {
                    return FileCopyUtils.copyToByteArray(inputStream);
                }

                @Override
                public InputStream getInputStream() throws IOException {
                    return inputStream;
                }

                @Override
                public void transferTo(File dest) throws IOException, IllegalStateException {
                    FileCopyUtils.copy(inputStream, Files.newOutputStream(dest.toPath()));
                }
            };

            myClient.submitFile(wildfireApiKey, file);

But something strange happens. I get an error that Jackson cannot encode the value:

org.springframework.core.codec.CodecException: Type definition error: [simple type, class sun.nio.ch.ChannelInputStream]
	at org.springframework.http.codec.json.AbstractJackson2Encoder.encodeValue(AbstractJackson2Encoder.java:256) ~[spring-web-6.0.10.jar:6.0.10]
	at org.springframework.http.codec.json.AbstractJackson2Encoder.lambda$encode$0(AbstractJackson2Encoder.java:158) ~[spring-web-6.0.10.jar:6.0.10]
...

Why is Jackson involved here? The body of the request should be multipart form data. So no JSON conversion should be done here...

I halso have tried to implement it via manual WebClient instantiation and that worked without problems. But i really want to use the new declarative client.

答案1

得分: 0

多部分支持目前还不可行,或者至少还没有添加MultipartFile支持。请参见此增强请求,它将被添加。

支持已经在此提交中添加,因此看起来它将成为Spring Framework 6.1的一部分,因此可能也是Spring Boot 3.2的一部分。

英文:

Multipart support isn't possible yet, or at least MultipartFile support hasn't been added. See this enhancement request which will add.

Support has been added with this commit, so it appears as it will be part of Spring Framework 6.1 and therefor probably Spring Boot 3.2.

答案2

得分: 0

@M.Deinum 是正确的。在 Spring Boot 版本低于 3.2 的情况下,不支持多部分请求。

我通过遵循 此提交 来解决了这个问题,该提交被认为在 Spring Boot 3.2 中解决了这个问题,然后我在我的项目中复制了这个解决方案,该项目使用的是 Spring Boot 3.0.1

所以他们所做的是引入了一个

> MultipartFileArgumentResolver

下面是我的 HttpClient 配置以及我如何在 Spring Boot 3.2 中适配它的解决方案。

PS:我正在使用 Spring Security,因此在我的 WebClient 上有 OAuth2AuthorizedClientManager

之前:

@Configuration
@RequiredArgsConstructor
public class OAuth2ClientConfig { 

    // ... (原有的代码)

    @Bean
    WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
        ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
            new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
        oauth2Client.setDefaultClientRegistrationId("keycloak");
        return WebClient.builder()
                .baseUrl(applicationConfigurations.getServiceDiscovery().getGatewayService())
                .clientConnector(new ReactorClientHttpConnector())
                .apply(oauth2Client.oauth2Configuration())
                .build();
    }

    // ... (原有的代码)

}

之后:

只需将 MultipartFileArgumentResolver 类添加到您的项目中,并导入必要的包。

public class MultipartFileArgumentResolver extends AbstractNamedValueArgumentResolver {

    private static final String MULTIPART_FILE_LABEL = "multipart file";

    // ... (原有的代码)

    @Override
    protected void addRequestValue(@NotNull String name, @NotNull Object value, @NotNull MethodParameter parameter,
                                   HttpRequestValues.Builder requestValues) {
        Assert.state(value instanceof MultipartFile,
                     "The value has to be of type 'MultipartFile'");

        MultipartFile file = (MultipartFile) value;
        requestValues.addRequestPart(name, toHttpEntity(name, file));
    }

    private HttpEntity<Resource> toHttpEntity(String name, MultipartFile file) {
        HttpHeaders headers = new HttpHeaders();
        if (file.getOriginalFilename() != null) {
            headers.setContentDispositionFormData(name, file.getOriginalFilename());
        }
        if (file.getContentType() != null) {
            headers.add(HttpHeaders.CONTENT_TYPE, file.getContentType());
        }
        return new HttpEntity<>(file.getResource(), headers);
    }
}

然后在您的 HttpClient 接口代理中使用已经可用的 customArgumentResolver() 方法将其添加。

@Bean
GatewayServiceConnectorClient gatewayServiceConnectorClient() {
    HttpServiceProxyFactory httpServiceProxyFactory = HttpServiceProxyFactory
                .builder(WebClientAdapter.forClient(webClient(authorizedClientManager())))
                .customArgumentResolver(new MultipartFileArgumentResolver())
                .build();
    return httpServiceProxyFactory.createClient(GatewayServiceConnectorClient.class);
}

最后,在您的 HttpClient 定义中。

@HttpExchange
public interface GatewayServiceConnectorClient {
     @PostExchange(value = "/upload", contentType = MediaType.MULTIPART_FORM_DATA_VALUE)
    ResponseEntity<String> uploadFacilities(@RequestPart(value = "file") MultipartFile file);
}

完成!

英文:

@M.Deinum is right. Multipart support isn't possible in Spring Boot versions below 3.2

I resolved this by following the commit that is supposed to solve this in Spring Boot 3.2, then I replicated it in my project which is using Spring Boot 3.0.1

So what they did was to introduce a

> MultipartFileArgumentResolver

Below is my HttpClient configuration and how I adapted it to the solution in Spring Boot 3.2

PS: I am using Spring Security hence the OAuth2AuthorizedClientManager on my Webclient

Before:

@Configuration
@RequiredArgsConstructor
public class OAuth2ClientConfig { 

    private final OAuth2AuthorizedClientService clientService;
    private final ApplicationConfigurations applicationConfigurations;
    private final ClientRegistrationRepository clientRegistrationRepository;

    @Bean
    public OAuth2AuthorizedClientManager authorizedClientManager() {
    OAuth2AuthorizedClientProvider authorizedClientProvider =
            OAuth2AuthorizedClientProviderBuilder
                    .builder()
                    .refreshToken()
                    .clientCredentials()
                    .build();

    AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager =
            new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, clientService);

    authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
    return authorizedClientManager;
    }

    @Bean
    WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
        ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
            new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
    oauth2Client.setDefaultClientRegistrationId(&quot;keycloak&quot;);
    return WebClient.builder()
            .baseUrl(applicationConfigurations.getServiceDiscovery().getGatewayService())
            .clientConnector(new ReactorClientHttpConnector())
            .apply(oauth2Client.oauth2Configuration())
            .build();
    }

    @Bean
    GatewayServiceConnectorClient gatewayServiceConnectorClient() {
        HttpServiceProxyFactory httpServiceProxyFactory = HttpServiceProxyFactory
            .builder(WebClientAdapter.forClient(webClient(authorizedClientManager())))
            .build();
    return httpServiceProxyFactory.createClient(GatewayServiceConnectorClient.class);
    }

}

After:

Simply add the MultipartFileArgumentResolver class to your project and import the necessary packages.

public class MultipartFileArgumentResolver extends AbstractNamedValueArgumentResolver {

    private static final String MULTIPART_FILE_LABEL = &quot;multipart file&quot;;

    @Override
    protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
        if (!parameter.nestedIfOptional().getNestedParameterType().equals(MultipartFile.class)) {
            return null;
        }
        return new NamedValueInfo(&quot;&quot;, true, null, MULTIPART_FILE_LABEL, true);

    }

    @Override
    protected void addRequestValue(@NotNull String name, @NotNull Object value, @NotNull MethodParameter parameter,
                                   HttpRequestValues.Builder requestValues) {
        Assert.state(value instanceof MultipartFile,
                     &quot;The value has to be of type &#39;MultipartFile&#39;&quot;);

        MultipartFile file = (MultipartFile) value;
        requestValues.addRequestPart(name, toHttpEntity(name, file));
    }

    private HttpEntity&lt;Resource&gt; toHttpEntity(String name, MultipartFile file) {
        HttpHeaders headers = new HttpHeaders();
        if (file.getOriginalFilename() != null) {
            headers.setContentDispositionFormData(name, file.getOriginalFilename());
        }
        if (file.getContentType() != null) {
            headers.add(HttpHeaders.CONTENT_TYPE, file.getContentType());
        }
        return new HttpEntity&lt;&gt;(file.getResource(), headers);
    }
}

then add it to your HttpClient interface proxy using the already available customArgumentResolver() method.

@Bean
GatewayServiceConnectorClient gatewayServiceConnectorClient() {
     HttpServiceProxyFactory httpServiceProxyFactory = HttpServiceProxyFactory
                .builder(WebClientAdapter.forClient(webClient(authorizedClientManager())))
                .customArgumentResolver(new MultipartFileArgumentResolver())
                .build();
     return httpServiceProxyFactory.createClient(GatewayServiceConnectorClient.class);
 }

then finally in your HttpClient definition.

@HttpExchange
public interface GatewayServiceConnectorClient {
     @PostExchange(value = &quot;/upload&quot;, contentType = MediaType.MULTIPART_FORM_DATA_VALUE)
    ResponseEntity&lt;String&gt; uploadFacilities(@RequestPart(value = &quot;file&quot;) MultipartFile file);
}

And Voila!

huangapple
  • 本文由 发表于 2023年6月27日 21:29:47
  • 转载请务必保留本文链接:https://go.coder-hub.com/76565392.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定