zoukankan      html  css  js  c++  java
  • Android Retrofit 2.0文件上传

    Android Retrofit 实现(图文上传)文字(参数)和多张图片一起上传

    使用Retrofit进行文件上传,肯定离不开Part & PartMap。

    public interface FileUploadService {  
        @Multipart
        @POST("upload")
        Call<ResponseBody> upload(@Part("description") RequestBody description,
                                  @Part MultipartBody.Part file);
    }

    上面是定义的接口方法,需要注意的是,这个方法不再有 @FormUrlEncoded 这个注解,而换成了 @Multipart,后面只需要在参数中增加 Part 就可以了。也许你会问,这里的 Part 和 Field 究竟有什么区别,其实从功能上讲,无非就是客户端向服务端发起请求携带参数的方式不同,并且前者可以携带的参数类型更加丰富,包括数据流。也正是因为这一点,我们可以通过这种方式来上传文件,下面我们就给出这个接口的使用方法:

    //先创建 service
    FileUploadService service = retrofit.create(FileUploadService.class);
    
    //构建要上传的文件
    File file = new File(filename);
    RequestBody requestFile =
            RequestBody.create(MediaType.parse("application/otcet-stream"), file);
    
    MultipartBody.Part body =
            MultipartBody.Part.createFormData("aFile", file.getName(), requestFile);
    
    String descriptionString = "This is a description";
    RequestBody description =
            RequestBody.create(
                    MediaType.parse("multipart/form-data"), descriptionString);
    
    Call<ResponseBody> call = service.upload(description, body);
    call.enqueue(new Callback<ResponseBody>() {
      @Override
      public void onResponse(Call<ResponseBody> call,
                             Response<ResponseBody> response) {
        System.out.println("success");
      }
    
      @Override
      public void onFailure(Call<ResponseBody> call, Throwable t) {
        t.printStackTrace();
      }
    });

    在实验时,我上传了一个只包含一行文字的文件: 
    Visit me: http://www.println.net

    那么我们去服务端看下我们的请求是什么样的: 
    HEADERS

    Accept-Encoding: gzip 
    Content-Length: 470 
    Content-Type: multipart/form-data; boundary=9b670d44-63dc-4a8a-833d-66e45e0156ca 
    User-Agent: okhttp/3.2.0 
    X-Request-Id: 9d70e8cc-958b-4f42-b979-4c1fcd474352 
    Via: 1.1 vegur 
    Host: requestb.in 
    Total-Route-Time: 0 
    Connection: close 
    Connect-Time: 0

    FORM/POST PARAMETERS

    description: This is a description

    RAW BODY

    –9b670d44-63dc-4a8a-833d-66e45e0156ca 
    Content-Disposition: form-data; name=”description” 
    Content-Transfer-Encoding: binary 
    Content-Type: multipart/form-data; charset=utf-8 
    Content-Length: 21
    
    This is a description 
    –9b670d44-63dc-4a8a-833d-66e45e0156ca 
    Content-Disposition: form-data; name=”aFile”; filename=”uploadedfile.txt” 
    Content-Type: application/otcet-stream 
    Content-Length: 32
    
    Visit me: http://www.println.net 
    –9b670d44-63dc-4a8a-833d-66e45e0156ca–

    我们看到,我们上传的文件的内容出现在请求当中了。如果需要上传多个文件,就声明多个 Part 参数,或者试试 PartMap。

    使用RequestBodyConverter简化请求

    上面为大家展示了如何用 Retrofit 上传文件,这个上传的过程还是有那么点儿不够简练,我们只是要提供一个文件用于上传,可我们前后构造了三个对象:

    这里写图片描述

    天哪,肯定是哪里出了问题。实际上,Retrofit 允许我们自己定义入参和返回的类型,不过,如果这些类型比较特别,我们还需要准备相应的 Converter,也正是因为 Converter 的存在, Retrofit 在入参和返回类型上表现得非常灵活。 
    下面我们把刚才的 Service 代码稍作修改:

    public interface FileUploadService {  
        @Multipart
        @POST("upload")
        Call<ResponseBody> upload(@Part("description") RequestBody description,
            //注意这里的参数 "aFile" 之前是在创建 MultipartBody.Part 的时候传入的
            @Part("aFile") File file);
    }

    现在我们把入参类型改成了我们熟悉的 File,如果你就这么拿去发请求,服务端收到的结果会让你哭了的。。。 
    RAW BODY

    –7d24e78e-4354-4ed4-9db4-57d799b6efb7 
    Content-Disposition: form-data; name=”description” 
    Content-Transfer-Encoding: binary 
    Content-Type: multipart/form-data; charset=utf-8 
    Content-Length: 21
    
    This is a description 
    –7d24e78e-4354-4ed4-9db4-57d799b6efb7 
    Content-Disposition: form-data; name=”aFile” 
    Content-Transfer-Encoding: binary 
    Content-Type: application/json; charset=UTF-8 
    Content-Length: 35
    
    // 注意这里!!之前是文件的内容,现在变成了文件的路径 
    {“path”:”samples/uploadedfile.txt”} 
    –7d24e78e-4354-4ed4-9db4-57d799b6efb7–

    文件内容成功上传了,当然其中还存在一些问题,这个目前直接使用 Retrofit 的 Converter 还做不到,原因主要在于我们没有办法通过 Converter 直接将 File 转换为 MultiPartBody.Part,如果想要做到这一点,我们可以对 Retrofit 的源码稍作修改,这个我们下面再谈。

    继续简化文件上传的接口

    上面我们曾试图简化文件上传接口的使用,尽管我们已经给出了相应的 File -> RequestBody 的 Converter,不过基于 Retrofit本身的限制,我们还是不能像直接构造 MultiPartBody.Part 那样来获得更多的灵活性。这时候该怎么办?当然是 Hack~~ 
    首先明确我们的需求:

    • 文件的 Content-Type 需要更多的灵活性,不应该写死在 Converter 当中,可以的话,最好可以根据文件的扩展名来映射出来对应的 Content-Type, 比如 image.png -> image/png;
    • 在请求的数据中,能够正常携带 filename 这个字段。

    为此,我增加了一套完整的参数解析方案:

      • 增加任意类型转换的 Converter,这一步主要是满足后续我们直接将入参类型转换为 MultiPartBody.Part 类型:
    public interface Converter<F, T> {
      ...
    
      abstract class Factory {
        ...
        //返回一个满足条件的不限制类型的 Converter
        public Converter<?, ?> arbitraryConverter(Type originalType,
              Type convertedType, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit){
          return null;
        }
      }
    }

    需要注意的是,Retrofit 类当中也需要增加相应的方法:

    public <F, T> Converter<F, T> arbitraryConverter(Type orignalType,
                                                     Type convertedType, Annotation[] parameterAnnotations, Annotation[] methodAnnotations) {
      return nextArbitraryConverter(null, orignalType, convertedType, parameterAnnotations, methodAnnotations);
    }
    
    public <F, T> Converter<F, T> nextArbitraryConverter(Converter.Factory skipPast,
                                  Type type, Type convertedType,  Annotation[] parameterAnnotations, Annotation[] methodAnnotations) {
      checkNotNull(type, "type == null");
      checkNotNull(parameterAnnotations, "parameterAnnotations == null");
      checkNotNull(methodAnnotations, "methodAnnotations == null");
    
      int start = converterFactories.indexOf(skipPast) + 1;
      for (int i = start, count = converterFactories.size(); i < count; i++) {
        Converter.Factory factory = converterFactories.get(i);
        Converter<?, ?> converter =
                factory.arbitraryConverter(type, convertedType, parameterAnnotations, methodAnnotations, this);
        if (converter != null) {
          //noinspection unchecked
          return (Converter<F, T>) converter;
        }
      }
      return null;
    }
    • 再给出 arbitraryConverter 的具体实现:
    public class TypedFileMultiPartBodyConverterFactory extends Converter.Factory {
        @Override
        public Converter<TypedFile, MultipartBody.Part> arbitraryConverter(Type originalType, Type convertedType, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
            if (originalType == TypedFile.class && convertedType == MultipartBody.Part.class) {
                return new FileRequestBodyConverter();
            }
            return null;
        }
    }
    public class TypedFileMultiPartBodyConverter implements Converter<TypedFile, MultipartBody.Part> {
    
        @Override
        public MultipartBody.Part convert(TypedFile typedFile) throws IOException {
            RequestBody requestFile =
                    RequestBody.create(typedFile.getMediaType(), typedFile.getFile());
            return MultipartBody.Part.createFormData(typedFile.getName(), typedFile.getFile().getName(), requestFile);
        }
    }
    public class TypedFile {
        private MediaType mediaType;
        private String name;
        private File file;
    
        public TypedFile(String name, String filepath){
            this(name, new File(filepath));
        }
    
        public TypedFile(String name, File file) {
            this(MediaType.parse(MediaTypes.getMediaType(file)), name, file);
        }
    
        public TypedFile(MediaType mediaType, String name, String filepath) {
            this(mediaType, name, new File(filepath));
        }
    
        public TypedFile(MediaType mediaType, String name, File file) {
            this.mediaType = mediaType;
            this.name = name;
            this.file = file;
        }
    
        public String getName() {
            return name;
        }
    
        public MediaType getMediaType() {
            return mediaType;
        }
    
        public File getFile() {
            return file;
        }
    }
    • 在声明接口时,@Part 不要传入参数,这样 Retrofit 在 ServiceMethod.Builder.parseParameterAnnotation 方法中解析 Part时,就会认为我们传入的参数为 MultiPartBody.Part 类型(实际上我们将在后面自己转换)。那么解析的时候,我们拿到前面定义好的 Converter,构造一个 ParameterHandler:
    ...
    } else if (MultipartBody.Part.class.isAssignableFrom(rawParameterType)) {
        return ParameterHandler.RawPart.INSTANCE;
    } else {
        Converter<?, ?> converter =
                retrofit.arbitraryConverter(type, MultipartBody.Part.class, annotations, methodAnnotations);
        if(converter == null) {
            throw parameterError(p,
                    "@Part annotation must supply a name or use MultipartBody.Part parameter type.");
        }
        return new ParameterHandler.TypedFileHandler((Converter<TypedFile, MultipartBody.Part>) converter);
    }
    ...
    static final class TypedFileHandler extends ParameterHandler<TypedFile>{
    
      private final Converter<TypedFile, MultipartBody.Part> converter;
    
      TypedFileHandler(Converter<TypedFile, MultipartBody.Part> converter) {
        this.converter = converter;
      }
    
      @Override
      void apply(RequestBuilder builder, TypedFile value) throws IOException {
        if(value != null){
          builder.addPart(converter.convert(value));
        }
      }
    }
    • 这时候再看我们的接口声明:
    public interface FileUploadService {
      @Multipart
      @POST("upload")
      Call<ResponseBody> upload(@Part("description") RequestBody description,
                                @Part TypedFile typedFile);
    }

    使用方法:

    etrofit retrofit = new Retrofit.Builder()
        .baseUrl("http://www.println.net/")
        .addConverterFactory(new TypedFileMultiPartBodyConverterFactory())
        .addConverterFactory(GsonConverterFactory.create())
        .build();
    
    FileUploadService service = retrofit.create(FileUploadService.class);
    TypedFile typedFile = new TypedFile("aFile", filename);
    String descriptionString = "This is a description";
    RequestBody description =
            RequestBody.create(
                    MediaType.parse("multipart/form-data"), descriptionString);
    
    Call<ResponseBody> call = service.upload(description, typedFile);
    call.enqueue(...);

    至此,我们已经通过自己的双手,让 Retrofit 点亮了自定义上传文件的技能,风骚等级更上一层楼!

    摘自:http://bugly.qq.com/bbs/forum.php?mod=viewthread&tid=1117

    http://www.chenkaihua.com/2016/04/02/retrofit2-upload-multipart-files.html?utm_source=tuicool&utm_medium=referral

    http://www.jianshu.com/users/fc232a6c7db3/latest_articles

    http://blog.csdn.net/u010945031/article/details/49475897

  • 相关阅读:
    os模块中关于文件/目录常用的函数使用方法
    字符串的方法及注释
    XAMPP禁止目录浏览的方法
    linux下lampp(xampp)安装memcached扩展
    linux下xampp集成包安装配置方法
    java判断一个字符串是否为数字型
    锁表原因及解决思路
    搞清楚MySQL事务隔离级别
    Java接口的幂等性设计
    java幂等性的解决方案
  • 原文地址:https://www.cnblogs.com/zhujiabin/p/7601658.html
Copyright © 2011-2022 走看看