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()));
      }
    }
  }

 

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

 

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

原文轉自(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);
}
}

 

 

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

 

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章