微服務之間如何通過feign調用接口上傳文件

具體需求:

我們的項目是基於springboot框架的springcloud微服務搭建的,後端服務技術層面整體上分為business服務和core服務,business服務用於作為應用層,直接連接客戶端,通常用於聚合數據,core服務用來客戶端具體操作不同需求來控制數據庫,文件上傳是通過客戶端上傳接口,通過business服務,由服務端調用feign接口,也是第一次做這種文件中轉,遇到各種問題,下面是我自己的解決方案,不喜勿噴,代碼小白一枚;

一、core服務層接口@requestmapping

屬性加上consumes=MediaType.MULTIPART_FORM_DATA_VALUE如下代碼

@PostMapping(value = "/upload",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    @ResponseBody
    public Result<TbFile> upload(@RequestPart(value = "file",required = true) MultipartFile file,
                         @RequestParam(name = "id",required = true) Integer id,
                         @RequestParam(name = "desc",required = false)  String desc,
                         @RequestParam(name = "fileId",required = false) Integer fileId )

解釋:@RequestMapping存在以下兩個屬性:

1.String[] consumes() default {};

2.String[] produces() default {};

兩個屬性的解釋及參考例子:

① 屬性produces:指定返回值類型,並且可以設置返回值類型和返回值的字符編碼;代碼例子參考如下:

屬性produces=”application/json”時,返回json數據

屬性produces=”MediaType.APPLICATION_JSON_VALUE;charset=utf-8″時,設置返回數據的字符編碼為utf-8

@Controller    
@RequestMapping(value = "/getperson", method = RequestMethod.GET, produces="application/json")    
public Object getPerson(int id) {       
    //實現自己的邏輯調用
  Person p= new person();
 
    return p;
} 

特別說明:produces=”application/json”和註解@ResponseBody是一樣的效果,使用瞭註解其實可以不使用該屬性瞭

② 屬性consumes: 指定處理請求當中的提交內容類型(Content-Type):application/json, text/html等;

代碼例子參考如下:

 @PostMapping(value = "/upload",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    @ResponseBody
    public Result<TbFile> upload(@RequestPart(value = "file",required = true) MultipartFile file,
                         @RequestParam(name = "id",required = true) Integer id,
                         @RequestParam(name = "desc",required = false)  String desc,
                         @RequestParam(name = "fileId",required = false) Integer fileId ){
}

解釋: MediaType.MULTIPART_FORM_DATA_VALUE 代表的值為multipart/form-data它會將表單的數據處理為一條消息,以標簽為單元,用分隔符分開。既可以上傳鍵值對,也可以上傳文件。當上傳的字段是文件時,會有Content-Type來表名文件類型;content-disposition,用來說明字段的一些信息;

二、business客戶層接口@requestmapping

屬性加上consumes=MediaType.MULTIPART_FORM_DATA_VALUE如下代碼

   @PostMapping(value = "/upload",produces = MediaType.APPLICATION_JSON_UTF8_VALUE,
            consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    @ResponseBody
    Result<TbFile> upload(@RequestPart(value = "file",required = true) MultipartFile file,
                  @RequestParam(name = "id",required = true) Integer id,
                  @RequestParam(name = "desc",required = false)  String desc,
                  @RequestParam(name = "fileId",required = false) Integer fileId );

具體大概就這麼多。能力有限,多多指教!!!

feign微服務間文件上傳(Finchley版本)

在Spring Cloud 的Feign組件中並不支持文件的傳輸,會出現這樣的錯誤提示:

feign.codec.EncodeException: class [Lorg.springframework.web.multipart.MultipartFile; is not a type supported by this encoder.
    at feign.codec.Encoder$Default.encode(Encoder.java:90) ~[feign-core-9.5.1.jar:na]
    at feign.form.FormEncoder.encode(FormEncoder.java:87) ~[feign-form-3.3.0.jar:3.3.0]
    at feign.form.spring.SpringFormEncoder.encode(SpringFormEncoder.java:64) ~[feign-form-spring-3.3.0.jar:3.3.0]

但是我們可以通過使用Feign的擴展包實現這個功能。

一. 示例介紹

服務名 端口號 角色
feign_upload_first 8100 feign服務提供者
feign_upload_second 8101 feign服務消費者

我們調用feign_upload_second的上傳文件接口上傳文件,feign_upload_second內部使用feign調用feign_upload_first實現文件上傳。

二 、單文件上傳

2.1 feign_upload_first服務提供者

文件上傳的服務提供者接口比較簡單,如下所示:

@SpringBootApplication
public class FeignUploadFirstApplication {
  @RestController
  public class UploadController {    
    @RequestMapping(value = "/uploadFile",method = RequestMethod.POST, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public String handleFileUpload(@RequestPart(value = "file") MultipartFile file) {
      return file.getOriginalFilename();
    }
  }
  public static void main(String[] args) {
    SpringApplication.run(FeignUploadFirstApplication.class, args);
  }
}

2.2 feign_upload_second服務消費者

增加擴展包依賴

    <dependency>
      <groupId>io.github.openfeign.form</groupId>
      <artifactId>feign-form</artifactId>
      <version>3.3.0</version>
    </dependency>
    <dependency>
      <groupId>io.github.openfeign.form</groupId>
      <artifactId>feign-form-spring</artifactId>
      <version>3.3.0</version>
    </dependency>
    <dependency>
      <groupId>commons-fileupload</groupId>
      <artifactId>commons-fileupload</artifactId>
      <version>1.3.3</version>
    </dependency>

新增feign實現文件上傳的配置類

@Configuration
public class FeignSupportConfig {
  @Bean
  public Encoder feignFormEncoder() {
    return new SpringFormEncoder();
  }
}

feign遠程調用接口

@FeignClient(name = "file",url = "http://localhost:8100",configuration = FeignSupportConfig.class)
public interface UploadService {
  @RequestMapping(value = "/uploadFile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
  String handleFileUpload(@RequestPart(value = "file") MultipartFile file);
}

上傳文件接口

@RestController
public class UploadController {
  @Autowired
  UploadService uploadService;
  
  @RequestMapping(value = "/uploadFile",method = RequestMethod.POST, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
  public String handleFileUpload(@RequestPart(value = "file") MultipartFile file) {
    return uploadService.handleFileUpload(file);
  }
}

2.3 測試

使用postman進行測試,可以正常上傳文件

三、多文件上傳

既然單個文件可以上傳,那麼多文件應該也沒問題吧,我們對上面的代碼進行修改

3.1 feign_upload_first服務提供者

文件上傳的服務提供者接口比較簡單,如下所示:

@SpringBootApplication
public class FeignUploadFirstApplication {
  @RestController
  public class UploadController {
    
    @RequestMapping(value = "/uploadFile",method = RequestMethod.POST, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public String handleFileUpload(@RequestPart(value = "file") MultipartFile file) {
      return file.getOriginalFilename();
    }
    
    @RequestMapping(value = "/uploadFile2",method = RequestMethod.POST, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public String handleFileUpload(@RequestPart(value = "file") MultipartFile[] file) {
      String fileName = "";
      for(MultipartFile f : file){
        fileName += f.getOriginalFilename()+"---";
      }
      return fileName;
    }
  }
  public static void main(String[] args) {
    SpringApplication.run(FeignUploadFirstApplication.class, args);
  }
}

3.2 feign_upload_second服務消費者

feign遠程調用接口

@FeignClient(name = "file",url = "http://localhost:8100",configuration = FeignSupportConfig.class)
public interface UploadService {
  @RequestMapping(value = "/uploadFile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
  String handleFileUpload(@RequestPart(value = "file") MultipartFile file);
  
  @RequestMapping(value = "/uploadFile2", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
  String handleFileUpload(@RequestPart(value = "file") MultipartFile[] file);
}

上傳文件接口

@RestController
public class UploadController {
  @Autowired
  UploadService uploadService;
  
  @RequestMapping(value = "/uploadFile",method = RequestMethod.POST, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
  public String handleFileUpload(@RequestPart(value = "file") MultipartFile file) {
    return uploadService.handleFileUpload(file);
  }
  
  @RequestMapping(value = "/uploadFile2",method = RequestMethod.POST, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
  public String handleFileUpload2(@RequestPart(value = "file") MultipartFile[] file) {
    return uploadService.handleFileUpload(file);
  }
}

3.3 測試

經過測試發現,無法上傳多個文件。經過檢查,發現源碼裡底層是有對MultipartFile[]類型的支持的,源碼中有個類叫SpringManyMultipartFilesWriter,是專門針對文件數組類型進行操作的,但是配置到項目裡的SpringFormEncoder類裡卻沒有對文件數組類型的判斷,以致不能支持文件數組的上傳

SpringManyMultipartFilesWriter源碼

public class SpringManyMultipartFilesWriter extends AbstractWriter {
  private final SpringSingleMultipartFileWriter fileWriter = new SpringSingleMultipartFileWriter();
  public SpringManyMultipartFilesWriter() {
  }
  public void write(Output output, String boundary, String key, Object value) throws Exception {
    if (value instanceof MultipartFile[]) {
      MultipartFile[] files = (MultipartFile[])((MultipartFile[])value);
      MultipartFile[] var6 = files;
      int var7 = files.length;
      for(int var8 = 0; var8 < var7; ++var8) {
        MultipartFile file = var6[var8];
        this.fileWriter.write(output, boundary, key, file);
      }
    } else if (value instanceof Iterable) {
      Iterable<?> iterable = (Iterable)value;
      Iterator var11 = iterable.iterator();
      while(var11.hasNext()) {
        Object file = var11.next();
        this.fileWriter.write(output, boundary, key, file);
      }
    }
  }
  public boolean isApplicable(Object value) {
    if (value == null) {
      return false;
    } else if (value instanceof MultipartFile[]) {
      return true;
    } else {
      if (value instanceof Iterable) {
        Iterable<?> iterable = (Iterable)value;
        Iterator<?> iterator = iterable.iterator();
        if (iterator.hasNext() && iterator.next() instanceof MultipartFile) {
          return true;
        }
      }
      return false;
    }
  }
}

SpringFormEncoder源碼

public class SpringFormEncoder extends FormEncoder {
  public SpringFormEncoder() {
    this(new Default());
  }
  public SpringFormEncoder(Encoder delegate) {
    super(delegate);
    MultipartFormContentProcessor processor = (MultipartFormContentProcessor)this.getContentProcessor(ContentType.MULTIPART);
    processor.addWriter(new SpringSingleMultipartFileWriter());
    processor.addWriter(new SpringManyMultipartFilesWriter());
  }
  public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
    if (!bodyType.equals(MultipartFile.class)) {
      super.encode(object, bodyType, template);
    } else {
      MultipartFile file = (MultipartFile)object;
      Map<String, Object> data = Collections.singletonMap(file.getName(), object);
      super.encode(data, MAP_STRING_WILDCARD, template);
    }
  }
}

從上面SpringFormEncoder的源碼上可以看到SpringFormEncoder類構造時把SpringManyMultipartFilesWriter實例添加到瞭處理器列表裡瞭,但是在encode方法裡又隻判斷瞭MultipartFile類型,沒有判斷數組類型,底層有對數組的支持但上層卻缺少瞭相應判斷。那麼我們可以自己去擴展FormEncoder,仿照SpringFormEncoder源碼,隻修改encode方法。

3.3 擴展FormEncoder支持多文件上傳

擴展FormEncoder,命名為FeignSpringFormEncoder

public class FeignSpringFormEncoder extends FormEncoder {
  /**
   * Constructor with the default Feign's encoder as a delegate.
   */
  public FeignSpringFormEncoder() {
    this(new Default());
  }  
  
  /**
   * Constructor with specified delegate encoder.
   *
   * @param delegate delegate encoder, if this encoder couldn't encode object.
   */
  public FeignSpringFormEncoder(Encoder delegate) {
    super(delegate);
    
    MultipartFormContentProcessor processor = (MultipartFormContentProcessor) getContentProcessor(ContentType.MULTIPART);
    processor.addWriter(new SpringSingleMultipartFileWriter());
    processor.addWriter(new SpringManyMultipartFilesWriter());
  }  
  
  @Override
  public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
    if (bodyType.equals(MultipartFile.class)) {
      MultipartFile file = (MultipartFile) object;
      Map data = Collections.singletonMap(file.getName(), object);
      super.encode(data, MAP_STRING_WILDCARD, template);
      return;
    } else if (bodyType.equals(MultipartFile[].class)) {
      MultipartFile[] file = (MultipartFile[]) object;
      if(file != null) {
        Map data = Collections.singletonMap(file.length == 0 ? "" : file[0].getName(), object);
        super.encode(data, MAP_STRING_WILDCARD, template);
        return;
      }
    }
    super.encode(object, bodyType, template);
  }
}

註冊配置類

@Configuration
public class FeignSupportConfig {
  @Bean
  public Encoder feignFormEncoder() {
    return new FeignSpringFormEncoder();
  }
}

經過測試可以上傳多個文件。

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。

推薦閱讀: