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

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

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

  • 相关阅读:
    随笔练习
    获得屏幕相关的辅助类
    C# 下sqlite简单使用
    XP系统下 VS2010 选中行崩溃
    Custome Buble Data Point
    RIA
    Chart Style
    d3js
    TreeView
    [Java入门笔记] Java语言简介
  • 原文地址:https://www.cnblogs.com/tony-lu229/p/7156332.html
Copyright © 2011-2022 走看看