Spring Cloud Feign Client 實現MultipartFile上傳文件功能


這兩天老大突然交給一個任務,就是當用戶關注我們的微信號時,我們應該將其微信頭像下載下來,然后上傳到公司內部的服務器上。如果直接保存微信頭像的鏈接,當用戶更換微信頭像時,我們的產品在獲取用戶頭像很可能會出現404異常。

由於公司運用的技術棧為spring Cloud(一些Eureka, Feign)進行服務注冊和遠程調用。

重點來了。。。。但直接使用FeignClient去遠程調用注冊中心上的上傳文件接口,會一直報錯。

@PostMapping
    @ApiOperation(value = "上傳文件")
    public String fileUpload(@ApiParam(value = "文件", required = true) @RequestParam("file") MultipartFile multipartFile,
            @ApiParam(value = "usage(目錄)", required = false) @RequestParam(value = "usage", required = false) String usage,
            @ApiParam(value = "同步(可選,默認false)") @RequestParam(value = "sync", required = false, defaultValue = "false") boolean sync) {
        if (multipartFile == null) {
            throw new IllegalArgumentException("參數異常");
        }
        String url = map.get(key).doUpload(multipartFile, usage, sync);
        return UploadResult.builder().url(url).build();
    }

遠程的上傳文件的接口。

@FeignClient("dx-commons-fileserver")
public interface FileServerService {


@RequestMapping(value="/file", method = RequestMethod.POST)
    public String fileUpload(
    @RequestParam("file") MultipartFile multipartFile,
    @RequestParam(value = "usage", required = false) String usage,
            @RequestParam(value = "sync", required = false, defaultValue = "false") boolean sync);
}

普通的FeignClient遠程調用代碼。但是這樣的實現,在去調用的時候一直拋異常:MissingServletRequestPartException,"Required request part  'file' is not present"

這里去跟蹤:fileServerService.fileUpload(multipartFile, null, true)源碼發現發送的url是將multipartFile以url的方式拼接在query string上。所以這樣的調用肯定是不行的。

 

那從百度搜索了一下關鍵詞: feign upload 會看到有這樣一種解決方案:

(原文轉自:http://www.jianshu.com/p/dfecfbb4a215)

 

maven

        <dependency> <groupId>io.github.openfeign.form</groupId> <artifactId>feign-form</artifactId> <version>2.1.0</version> </dependency> <dependency> <groupId>io.github.openfeign.form</groupId> <artifactId>feign-form-spring</artifactId> <version>2.1.0</version> </dependency>

feign config

@Configuration public class FeignMultipartSupportConfig { @Bean @Primary @Scope("prototype") public Encoder multipartFormEncoder() { return new SpringFormEncoder(); } @Bean public feign.Logger.Level multipartLoggerLevel() { return feign.Logger.Level.FULL; } }

feign client

@FeignClient(name = "xxx",configuration = FeignMultipartSupportConfig.class) public interface OpenAccountFeignClient { @RequestMapping(method = RequestMethod.POST, value = "/xxxxx",produces = {MediaType.APPLICATION_JSON_UTF8_VALUE},consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity<?> ocrIdCard(@RequestPart(value = "file") MultipartFile file); }

 

 

這種方案很好很強大,照搬過來就很好的解決了問題。也實現了文件上傳的遠程調用。

 

但是問題又來了。因為上面的成功是很大一部分源於那個配置類,里面的Encoder Bean。但我的這個項目里不止需要遠程調用上傳的接口,還需要調用其他的接口。這樣的話會發現其他FeignClient一調用,就會拋異常。真的是一波未平一波又起。心碎的感覺。跟蹤源碼發現:

SpringFormEncoder的encode方法當傳送的對象不是MultipartFile的時候,就會調用Encoder.Default類的encode方法。。。。。。。。。。。

public class SpringFormEncoder extends FormEncoder {
    
    private final Encoder delegate;


    public SpringFormEncoder () {
        this(new Encoder.Default());
    }


    public SpringFormEncoder(Encoder delegate) {
        this.delegate = delegate;
    }
    
    @Override
    public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
        if (!bodyType.equals(MultipartFile.class)) {
            delegate.encode(object, bodyType, template);
            return;
        }
        
        MultipartFile file = (MultipartFile) object;
        Map<String, Object> data = Collections.singletonMap(file.getName(), object);
        new SpringMultipartEncodedDataProcessor().process(data, template);
    }

}

而這個Encoder.Default的encode方法判斷傳送的類型不是String或者byte[],就會拋異常:

class Default implements Encoder {


    @Override
    public void encode(Object object, Type bodyType, RequestTemplate template) {
      if (bodyType == String.class) {
        template.body(object.toString());
      } else if (bodyType == byte[].class) {
        template.body((byte[]) object, null);
      } else if (object != null) {
        throw new EncodeException(
            format("%s is not a type supported by this encoder.", object.getClass()));
      }
    }
  }

 

就這樣,我又得繼續尋找其他的方法,不然沒法遠程調用其他的服務了。這就很尷尬。

 

那接下來就是各種翻牆,各種谷歌,終於找到了合適的答案。

原文轉自(https://github.com/pcan/feign-client-test   可將示例代碼下載下來研究,這樣方便看調用的邏輯)

 

Feign Client Test

 

A Test project that uses Feign to upload Multipart files to a REST endpoint. Since Feign library does not support Multipart requests, I wrote a custom Encoder that enables this feature, using a HttpMessageConverter chain that mimics Spring's RestTemplate.

Multipart Request Types

A few request types are supported at the moment:

  • Simple upload requests: One MultipartFile alongwith some path/query parameters:
interface TestUpload { @RequestLine("POST /upload/{folder}") public UploadInfo upload(@Param("folder") String folder, @Param("file") MultipartFile file); }
  • Upload one file & object(s): One MultipartFile alongwith some path/query parameters and one or more JSON-encoded object(s):
interface TestUpload { @RequestLine("POST /upload/{folder}") public UploadInfo upload(@Param("folder") String folder, @Param("file") MultipartFile file, @Param("metadata") UploadMetadata metadata); }
  • Upload multiple files & objects: An array of MultipartFile alongwith some path/query parameters and one or more JSON-encoded object(s):
interface TestUpload { @RequestLine("POST /uploadArray/{folder}") public List<UploadInfo> uploadArray(@Param("folder") String folder, @Param("files") MultipartFile[] files, @Param("metadata") UploadMetadata metadata); }

 

根據上面的示例代碼的提示,我也就按照上面的修改我的代碼。因為原理方面沒有深入的研究,所以很多代碼直接復制過來修改一下。其中有一段:

Feign.Builder encoder = Feign.builder()
                .decoder(new JacksonDecoder())
                .encoder(new FeignSpringFormEncoder());

這里的encoder是示例代碼自己定義的(本人的代碼也用到了這個類),decoder用的是JacksonDecoder,那這塊我也直接復制了。然后修改好代碼為:

@Service
public class UploadService {


@Value("${commons.file.upload-url}")
private String HTTP_FILE_UPLOAD_URL;//此處配置上傳文件接口的域名(http(s)://XXXXX.XXXXX.XX)

public String uploadFile(MultipartFile file, String usage, boolean sync){
FileUploadResource fileUploadResource = Feign.builder()

  .decoder(new JacksonDecoder())
                .encoder(new FeignSpringFormEncoder())
.target(FileUploadResource.class, HTTP_FILE_UPLOAD_URL);
return fileUploadResource.fileUpload(file, usage, sync);
}
}

 

public interface FileUploadResource {


@RequestLine("POST /file")
String fileUpload(@Param("file") MultipartFile file, @Param("usage") String usage, @Param("sync") boolean sync);
}

 

其中調用上傳文件的代碼就改為上述的代碼進行運行。但是這樣還是拋了異常。跟蹤fileUploadResource.fileUpload(file, usage, sync)代碼,一步步發現遠程的調用和文件的上傳都是OK的,響應也是為200.但是最后的decoder時,拋異常:

unrecognized token 'http': was expecting ('true', 'false' or 'null')

只想說 What a fucking day!!!   這里也能出錯??心里很是郁悶。。。。沒辦法,這個方法還是很厲害的,因為不會影響其他遠程服務的調用,雖然只是這里報錯。那只有再次跟蹤源碼,發現在JacksonDecoder的decode方法:

@Override
  public Object decode(Response response, Type type) throws IOException {
    if (response.status() == 404) return Util.emptyValueOf(type);
    if (response.body() == null) return null;
    Reader reader = response.body().asReader();
    if (!reader.markSupported()) {
      reader = new BufferedReader(reader, 1);
    }
    try {
      // Read the first byte to see if we have any data
      reader.mark(1);
      if (reader.read() == -1) {
        return null; // Eagerly returning null avoids "No content to map due to end-of-input"
      }
      reader.reset();
      return mapper.readValue(reader, mapper.constructType(type));
    } catch (RuntimeJsonMappingException e) {
      if (e.getCause() != null && e.getCause() instanceof IOException) {
        throw IOException.class.cast(e.getCause());
      }
      throw e;
    }
  }

 

其中走到: return mapper.readValue(reader, mapper.constructType(type)); 然后就拋異常啦。郁悶啊。最后不知道一下子咋想的,就嘗試把這個decoder刪除,不設置decoder了。那終於萬幸啊。。。。全部調通了。。。。。。。所以修改完的UploadService代碼為:

@Service
public class UploadService {


@Value("${commons.file.upload-url}")
private String HTTP_FILE_UPLOAD_URL;//此處配置上傳文件接口的域名(http(s)://XXXXX.XXXXX.XX)

public String uploadFile(MultipartFile file, String usage, boolean sync){
FileUploadResource fileUploadResource = Feign.builder()
                .encoder(new FeignSpringFormEncoder())                 //這里沒有添加decoder了
.target(FileUploadResource.class, HTTP_FILE_UPLOAD_URL);
return fileUploadResource.fileUpload(file, usage, sync);
}
}

 

 

寫這篇博客是因為這個問題花費了我一天多的時間,所以我一定得記下來,不然下次遇到了,可能還是會花費一些時間才能搞定。不過上面提到的示例代碼還真的是牛。后面還得繼續研究一下。

 

希望這些記錄能對那些和我一樣遇到這樣問題的小伙伴有所幫助,盡快解決問題。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM