zoukankan      html  css  js  c++  java
  • 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);
    }
    }

    写这篇博客是因为这个问题花费了我一天多的时间,所以我一定得记下来,不然下次遇到了,可能还是会花费一些时间才能搞定。不过上面提到的示例代码还真的是牛。后面还得继续研究一下。

    希望这些记录能对那些和我一样遇到这样问题的小伙伴有所帮助,尽快解决问题。

  • 相关阅读:
    Codechef EDGEST 树套树 树状数组 线段树 LCA 卡常
    BZOJ4319 cerc2008 Suffix reconstruction 字符串 SA
    Codechef STMINCUT S-T Mincut (CodeChef May Challenge 2018) kruskal
    Codeforces 316G3 Good Substrings 字符串 SAM
    Codechef CHSIGN Change the Signs(May Challenge 2018) 动态规划
    BZOJ1396 识别子串 字符串 SAM 线段树
    CodeForces 516C Drazil and Park 线段树
    CodeForces 516B Drazil and Tiles 其他
    CodeForces 516A Drazil and Factorial 动态规划
    SPOJ LCS2
  • 原文地址:https://www.cnblogs.com/tony-lu229/p/7156332.html
Copyright © 2011-2022 走看看