使用JSON请求部分进行Feign的多部分请求

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

Feign multipart with Json request part

问题

以下是翻译好的部分:

我在一个服务中有一个带有方法的 Feign 客户端:

  1. @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
  2. MyDto uploadDocument(@RequestPart("file") MultipartFile file,
  3. @RequestPart("myDto") String myDto);

我在另一个服务中有一个控制器:

  1. @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
  2. public ResponseEntity<MyDto> uploadDocument(@RequestParam("file") MultipartFile file,
  3. @RequestPart("myDto") MyDto myDto) {
  4. // ... 这里是一些代码
  5. }

我面临的问题是,Feign 使用 Content-type: text/plain 发送 myDto,导致出现 HttpMediaTypeNotSupportedException 错误。

是否可能将 @RequestPart("myDto") String myDto 以 Content-type: application/json 的形式发送?

期望的原始请求:

  1. ----------------------------boundary
  2. Content-Disposition: form-data; name="file"; filename="fileName"
  3. <file>
  4. ----------------------------boundary
  5. Content-Disposition: form-data; name="myDto";
  6. **Content-Type: application/json**
  7. {"myDto": ""}

当前的原始请求:

  1. ----------------------------boundary
  2. Content-Disposition: form-data; name="file"; filename="fileName"
  3. <file>
  4. ----------------------------boundary
  5. Content-Disposition: form-data; name="myDto";
  6. **Content-Type: text/plain**
  7. {"myDto": ""}
英文:

I have Feign client in one service with a method

  1. @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
  2. MyDto uploadDocument(@RequestPart(&quot;file&quot;) MultipartFile file,
  3. @RequestPart(&quot;myDto&quot;) String myDto);

I have a controller in another service

  1. @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
  2. public ResponseEntity&lt;MyDto&gt; uploadDocument(@RequestParam(&quot;file&quot;) MultipartFile file,
  3. @RequestPart(&quot;myDto&quot;) MyDto myDto) {
  4. .... some code here
  5. }

The issue I faced is that Feign sends myDto with Content-type : text/plain and I have HttpMediaTypeNotSupportedException

Is it possible to send @RequestPart(&quot;myDto&quot;) String myDto with Content-type : application/json ?

expected Raw request:

  1. ----------------------------boundary
  2. Content-Disposition: form-data; name=&quot;file&quot;; filename=&quot;fileName&quot;
  3. &lt;file&gt;
  4. ----------------------------boundary
  5. Content-Disposition: form-data; name=&quot;myDto&quot;
  6. **Content-Type: application/json**
  7. {&quot;myDto&quot;: &quot;&quot;}

Current raw request:

  1. ----------------------------boundary
  2. Content-Disposition: form-data; name=&quot;file&quot;; filename=&quot;fileName&quot;
  3. &lt;file&gt;
  4. ----------------------------boundary
  5. Content-Disposition: form-data; name=&quot;myDto&quot;
  6. **Content-Type: text/plain**
  7. {&quot;myDto&quot;: &quot;&quot;}

答案1

得分: 10

成功解决方法是通过替换feign-form的PojoWriter。默认情况下,它会将对象的每个字段序列化为单独的部分。

  1. @Bean
  2. public Encoder feignEncoder() {
  3. return new MyFormEncoder(objectMapper, new SpringEncoder(messageConverters));
  4. }
  1. public class MyFormEncoder extends SpringFormEncoder {
  2. /**
  3. * 使用指定的委托编码器构造函数。
  4. *
  5. * @param delegate 委托编码器,在无法对对象进行编码时使用。
  6. */
  7. public MyFormEncoder(ObjectMapper objectMapper, Encoder delegate) {
  8. super(delegate);
  9. val processor = (MultipartFormContentProcessor) getContentProcessor(MULTIPART);
  10. processor.addFirstWriter(new MyPojoWriter(objectMapper));
  11. }
  12. }
  1. @FieldDefaults(level = PRIVATE, makeFinal = true)
  2. public class MyPojoWriter extends AbstractWriter {
  3. private ObjectMapper objectMapper;
  4. public MyPojoWriter(ObjectMapper objectMapper) {
  5. this.objectMapper = objectMapper;
  6. }
  7. @Override
  8. public boolean isApplicable(Object object) {
  9. return isUserPojo(object);
  10. }
  11. @Override
  12. protected void write(Output output, String key, Object value) throws EncodeException {
  13. var data = "";
  14. try {
  15. data = objectMapper.writeValueAsString(value);
  16. } catch (JsonProcessingException e) {
  17. }
  18. val string = new StringBuilder()
  19. .append("Content-Disposition: form-data; name=\"").append(key).append('"').append(CRLF)
  20. .append("Content-Type: application/json; charset=").append(output.getCharset().name()).append(CRLF)
  21. .append(CRLF)
  22. .append(data)
  23. .toString();
  24. output.write(string);
  25. }
  26. private boolean isUserPojo(@NonNull Object object) {
  27. val type = object.getClass();
  28. val typePackage = type.getPackage();
  29. return typePackage != null && typePackage.getName().startsWith("com.my-package.");
  30. }
  31. }
英文:

Managed to solve this by replacing the feign-form PojoWriter. By default it's serializing each field of an object as a separate part.

  1. @Bean
  2. public Encoder feignEncoder () {
  3. return new MyFormEncoder(objectMapper, new SpringEncoder(messageConverters));
  4. }
  1. public class MyFormEncoder extends SpringFormEncoder {
  2. /**
  3. * Constructor with specified delegate encoder.
  4. *
  5. * @param delegate delegate encoder, if this encoder couldn&#39;t encode object.
  6. */
  7. public MyFormEncoder(ObjectMapper objectMapper, Encoder delegate) {
  8. super(delegate);
  9. val processor = (MultipartFormContentProcessor) getContentProcessor(MULTIPART);
  10. processor.addFirstWriter(new MyPojoWriter(objectMapper));
  11. }
  12. }
  1. @FieldDefaults(level = PRIVATE, makeFinal = true)
  2. public class MyPojoWriter extends AbstractWriter {
  3. private ObjectMapper objectMapper;
  4. public MyPojoWriter(ObjectMapper objectMapper) {
  5. this.objectMapper = objectMapper;
  6. }
  7. @Override
  8. public boolean isApplicable(Object object) {
  9. return isUserPojo(object);
  10. }
  11. @Override
  12. protected void write(Output output, String key, Object value) throws EncodeException {
  13. var data = &quot;&quot;;
  14. try {
  15. data = objectMapper.writeValueAsString(value);
  16. } catch (JsonProcessingException e) {
  17. }
  18. val string = new StringBuilder()
  19. .append(&quot;Content-Disposition: form-data; name=\&quot;&quot;).append(key).append(&#39;&quot;&#39;).append(CRLF)
  20. .append(&quot;Content-Type: application/json; charset=&quot;).append(output.getCharset().name()).append(CRLF)
  21. .append(CRLF)
  22. .append(data)
  23. .toString();
  24. output.write(string);
  25. }
  26. private boolean isUserPojo(@NonNull Object object) {
  27. val type = object.getClass();
  28. val typePackage = type.getPackage();
  29. return typePackage != null &amp;&amp; typePackage.getName().startsWith(&quot;com.my-package.&quot;);
  30. }
  31. }

答案2

得分: 5

Update to 2021.

  1. //spring-cloud-openfeign-core
  2. import org.springframework.cloud.openfeign.support.JsonFormWriter;
  3. @Import(JsonFormWriter.class)
  4. public class MyConfig {
  5. @Bean
  6. Encoder feignEncoder(JsonFormWriter jsonFormWriter) {
  7. return new SpringFormEncoder() {{
  8. var processor = (MultipartFormContentProcessor) getContentProcessor(ContentType.MULTIPART);
  9. processor.addFirstWriter(jsonFormWriter);
  10. processor.addFirstWriter(new SpringSingleMultipartFileWriter());
  11. processor.addFirstWriter(new SpringManyMultipartFilesWriter());
  12. }};
英文:

Update to 2021.

  1. //spring-cloud-openfeign-core
  2. import org.springframework.cloud.openfeign.support.JsonFormWriter;
  3. @Import(JsonFormWriter.class)
  4. public class MyConfig {
  5. @Bean
  6. Encoder feignEncoder(JsonFormWriter jsonFormWriter) {
  7. return new SpringFormEncoder() {{
  8. var processor = (MultipartFormContentProcessor) getContentProcessor(ContentType.MULTIPART);
  9. processor.addFirstWriter(jsonFormWriter);
  10. processor.addFirstWriter(new SpringSingleMultipartFileWriter());
  11. processor.addFirstWriter(new SpringManyMultipartFilesWriter());
  12. }};

答案3

得分: 3

你需要在你的Feign客户端配置中定义名为JsonFormWriter的Bean。

以下是客户端的示例代码:

  1. @FeignClient(
  2. name = "my-client",
  3. configuration = MyClientConfiguration.class
  4. )
  5. public interface MyClient {
  6. @PostMapping(value = "/file/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
  7. void uploadFile(@RequestPart("request") MyFileUploadRequest request,
  8. @RequestPart("file") MultipartFile file);
  9. }
  10. public class MyClientConfiguration {
  11. @Bean
  12. JsonFormWriter jsonFormWriter() {
  13. return new JsonFormWriter();
  14. }
  15. }

以及控制器的示例代码:

  1. @RestController
  2. public class FileUploadApi {
  3. @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
  4. public void uploadFile(
  5. @RequestPart("request") MyFileUploadRequest request,
  6. @RequestPart("file") MultipartFile file) {
  7. }

此功能是在此 Pull Request 的范围内添加的:https://github.com/spring-cloud/spring-cloud-openfeign/pull/314

英文:

You need to define bean JsonFormWriter in your feign client's configuration.

Here is an example of the client:

  1. @FeignClient(
  2. name = &quot;my-client&quot;,
  3. configuration = MyClientConfiguration.class
  4. )
  5. public interface MyClient {
  6. @PostMapping(value = &quot;/file/upload&quot;, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
  7. void uploadFile(@RequestPart(&quot;request&quot;) MyFileUploadRequest request,
  8. @RequestPart(&quot;file&quot;) MultipartFile file);
  9. }
  10. public class MyClientConfiguration {
  11. @Bean
  12. JsonFormWriter jsonFormWriter() {
  13. return new JsonFormWriter();
  14. }
  15. }

And an example of the controller:

  1. @RestController
  2. public class FileUploadApi {
  3. @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
  4. public void uploadFile(
  5. @RequestPart(&quot;request&quot;) MyFileUploadRequest request,
  6. @RequestPart(&quot;file&quot;) MultipartFile file) {
  7. }

This feature was added in scope of this PR: https://github.com/spring-cloud/spring-cloud-openfeign/pull/314

答案4

得分: 0

使用 @PathVariable 注解,并且使用已注册的 SpringFormEncoder,你需要将 "myDto" 转换为 MultipartFile。

客户端:

  1. @PostMapping(value = "/files/upload", consumes = MULTIPART_FORM_DATA_VALUE)
  2. MyDto uploadDocument(@PathVariable("file") MultipartFile file, @PathVariable("myDto") MultipartFile myDto)

编码器:

  1. @RequiredArgsConstructor
  2. public class FeignClientConfiguration {
  3. private final ObjectFactory<HttpMessageConverters> messageConverters;
  4. // 为了支持多文件上传
  5. @Bean
  6. public Encoder feignFormEncoder() {
  7. return new SpringFormEncoder(new SpringEncoder(messageConverters));
  8. }
  9. }

从 DTO 创建 MultipartFile:

  1. public MultipartFile createMultipartFile(@NotNull MyDto myDto) throws JsonProcessingException {
  2. return new org.springframework.mock.web.MockMultipartFile(
  3. "fileName",
  4. "originalFileName",
  5. MediaType.APPLICATION_JSON.toString(),
  6. objectMapper.writeValueAsBytes(myDto));
  7. }

关于为什么使用 @PathVariable 的这种解决方案可以工作,可以在这里找到说明:https://github.com/spring-cloud/spring-cloud-netflix/issues/867

英文:

Using the @PathVariable annotation and with a registered SpringFormEncoder you need to convert the "myDto" into a MultipartFile.

The client:

  1. @PostMapping(value = &quot;/files/upload&quot;, consumes = MULTIPART_FORM_DATA_VALUE)
  2. MyDto uploadDocument(@PathVariable(&quot;file&quot;) MultipartFile file, @PathVariable(&quot;myDto&quot;) MultipartFile myDto)

The encoder:

  1. @RequiredArgsConstructor
  2. public class FeignClientConfiguration {
  3. private final ObjectFactory&lt;HttpMessageConverters&gt; messageConverters;
  4. //To support multipart file upload
  5. @Bean
  6. public Encoder feignFormEncoder() {
  7. return new SpringFormEncoder(new SpringEncoder(messageConverters));
  8. }
  9. }

Creating MultipartFile from the DTO:

  1. public MultipartFile createMultipartFile(@NotNull MyDto myDto) throws JsonProcessingException {
  2. return new org.springframework.mock.web.MockMultipartFile(
  3. &quot;fileName&quot;,
  4. &quot;originalFileName&quot;,
  5. MediaType.APPLICATION_JSON.toString(),
  6. objectMapper.writeValueAsBytes(myDto));
  7. }

Why this solution with @PathVariable works is described here https://github.com/spring-cloud/spring-cloud-netflix/issues/867

huangapple
  • 本文由 发表于 2020年7月24日 17:20:44
  • 转载请务必保留本文链接:https://go.coder-hub.com/63070641.html
匿名

发表评论

匿名网友

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

确定