這兩天老大突然交給一個任務,就是當用戶關注我們的微信號時,我們應該將其微信頭像下載下來,然後上傳到公司內部的服務器上。如果直接保存微信頭像的鏈接,當用戶更換微信頭像時,我們的產品在獲取用戶頭像很可能會出現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);
}
}
寫這篇博客是因爲這個問題花費了我一天多的時間,所以我一定得記下來,不然下次遇到了,可能還是會花費一些時間才能搞定。不過上面提到的示例代碼還真的是牛。後面還得繼續研究一下。
希望這些記錄能對那些和我一樣遇到這樣問題的小夥伴有所幫助,儘快解決問題。