Retrofit解析及文件上传下载(前后台详细实现)

在开始之前,本片文章使用得是Rxjava和retrofit结合,介绍的文件的上传和下载的实现,如果还不太了解和使用Rxjava和Retrofit的基本使用的同学,可以先了解完这部分内容以后,再阅读本篇文章。

retrofit的注解字段的介绍和使用:

1.@GET请求的参数解析:标记是GET请求。
    /**
     * 首页Banner
     * http://www.wanandroid.com/banner/json
     * @return BannerResponse
     */
    @GET("/banner/json")
    Observable<DataResponse<List<Banner>>> getHomeBanners();
1.1@query 封装GET请求参数的字段
    /**
     * 知识体系下的文章
     * http://www.wanandroid.com/article/list/json?cid=168
     *
     * @param page page
     * @param cid  cid
     */
    @GET("/article/list/json")
    Observable<DataResponse<Article>> getKnowledgeSystemArticles( @Query("cid") int cid);

1.2 @queryMap 和 @ Query的使用一样,只是当参数不固定或者参数比较多的时候调用

   @GET("/friend/json")
    Observable<DataResponse<Article>> getFile(@QueryMap Map<String,String> params);

调用处的代码:

Map<String,String> options = new HashMap<String,String>();
options.put("name",trainName);
options.put("key",KEY);
Call<Movie> movie = service.getFile(options);

1.3 @Path:url中的占位符,相当于动态的改变url, 当然别掉了{}将动态的配置参数包起来.


    /**
     * 搜索
     * http://www.wanandroid.com/article/query/0/json
     *
     * @param page page
     * @param k    POST search key
     */
    @POST("/article/query/{page}/json")
    @FormUrlEncoded
    Observable<DataResponse<Article>> getSearchArticles(@Path("page") int page, @Field("k") String k);

2.@POST请求的参数解析

2.1@FormUrlEncoded

在post请求中配置该参数,说明该请求将表单的形式传递参数,它不能用于get请求。
@FormUrlEncoded将会自动将请求参数的类型调整为application/x-www-form-urlencoded,假如content传递的参数为Good Luck,那么最后得到的请求体就是

content=Good+Luck
FormUrlEncoded不能用于Get请求

2.2 @Field标记POST请求中,键值对参数的key,例:username,和password,当调用的loing()方法时,自动封装到请求参数中

@Field注解将每一个请求参数都存放至请求体中,还可以添加encoded参数,该参数为boolean型,具体的用法为

@Field(value = "book", encoded = true) String book

encoded参数为true的话,key-value-pair将会被编码,即将中文和特殊字符进行编码转换

    /**
     * 登录
     *
     * @param username username
     * @param password password
     * @return Deferred<User>
     */
    @POST("/user/login")
    @FormUrlEncoded
    Observable<DataResponse<User>> login(@Field("username") String username, @Field("password") String password);

2.3 @FiledMap:这个跟Field差不多,将所有的参数用Map的方式进行传递

@FormUrlEncoded
@POST("voice")
Call<Vioce> sendVoiceMessage(@FieldMap Map<String,String> options);

调用处的代码:

Map<String,String> options = new HashMap<String,String>();
options.put("valicode","123456");
options.put("to","18772351259");
options.put("key",voice_KEY);
Call<Vioce> voice = service.sendVoiceMessage(options);

2.4 @Body :将所有的参数封住到一个自定义的对象,如果参数过多,统一封装到类中应该会更好,便于维护代码。

@FormUrlEncoded
@POST("voice")
Call<Vioce> sendVoiceMessage(@Body AddParams params);

public class AddParams{
    String valicode;
    String to;
    String key;

    public void setKey(String key) {
        this.key = key;
    }

    public void setTo(String to) {
        this.to = to;
    }

    public void setValicode(String valicode) {
        this.valicode = valicode;
    }

3、(单、多)文件上传实现及参数解析

3.1 @Part:配合@Multipart用于文件上传

在介绍文件上传之前,为了一些比较喜欢骚动的同学,我也贴出java后台的接受代码,方便我们更好的理解文件上传的详细过程。也方便后面的介绍。但是这个Servlet需要借助2个jar包。


lib.png
public class UploadFileServlet extends HttpServlet {

    
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        response.getWriter().write("this is the post request! ");
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)  
            throws ServletException, IOException {  
      response.setContentType("text/html");  
      PrintWriter out = response.getWriter();  

      // 创建文件项目工厂对象  
      DiskFileItemFactory factory = new DiskFileItemFactory();  

      // 设置文件上传路径  
      String upload = "f:";  
      // 获取系统默认的临时文件保存路径,该路径为Tomcat根目录下的temp文件夹  
      String temp = System.getProperty("java.io.tmpdir");  
      // 设置缓冲区大小为 5M  
      factory.setSizeThreshold(1024 * 1024 * 5);  
      // 设置临时文件夹为temp  
      factory.setRepository(new File(temp));  
      // 用工厂实例化上传组件,ServletFileUpload 用来解析文件上传请求  
      ServletFileUpload servletFileUpload = new ServletFileUpload(factory);  

      // 解析结果放在List中  
      try {  
          List<FileItem> list = servletFileUpload.parseRequest(request);  

          for (FileItem item : list) {  
              String name = item.getFieldName();  
              InputStream is = item.getInputStream();  

              System.out.println("the current name is " + name);  

              if (name.contains("photo") ||name.contains("file")) {  
                  try {  
                      inputStream2File(is,  
                            upload + "\\" + System.currentTimeMillis()  
                                    + item.getName());  
                  } catch (Exception e) {  
                      e.printStackTrace();  
                  }  
              } else {  
                  String key = item.getName();  
                  String value = item.getString(); 
//                  System.out.println(value );
//                  System.out.println(key + "---" + value);  
              }  
          }  

          out.write("success");  
      } catch (FileUploadException e) {  
          e.printStackTrace();  
          out.write("failure");  
      }  

      out.flush();  
      out.close();  

  }  

  // 流转化成字符串  
  public static String inputStream2String(InputStream is) throws IOException {  
      ByteArrayOutputStream baos = new ByteArrayOutputStream();  
      int i = -1;  
      while ((i = is.read()) != -1) {  
          baos.write(i);  
      }  
      return baos.toString();  
  }  

  // 流转化成文件  
  public static void inputStream2File(InputStream is, String savePath)  
          throws Exception {  
      System.out.println("the file path is  :" + savePath);  
      File file = new File(savePath);  
      InputStream inputSteam = is;  
      BufferedInputStream fis = new BufferedInputStream(inputSteam);  
      FileOutputStream fos = new FileOutputStream(file);  
      int f;  
      while ((f = fis.read()) != -1) {  
          fos.write(f);  
      }  
      fos.flush();  
      fos.close();  
      fis.close();  
      inputSteam.close();  
  }  
}

    @Multipart
    @POST("UploadServlet")
    Call<ResponseBody> upLoadPrefectFile( @Part("description") RequestBody description,@Part MultipartBody.Part file);

在Activity中的代码:

        final RequestBody requestBody = createPartFromString("this is des!");
        retrofit2.Call call = RetrofitHelper.getUpLoadFileAPI().upLoadPrefectFile(requestBody, prepareFilePart(new File("/sdcard/1.zip"), "file"));
        call.enqueue(new retrofit2.Callback() {
            @Override
            public void onResponse(retrofit2.Call call, retrofit2.Response response) {

                String s = response.body().toString();
                String s1 = response.message().toString();
                Log.d("TAG", "onResponse: " + s1);
                Toast.makeText(TestUploadFileActivity.this, "上传成功!" + s, Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onFailure(retrofit2.Call call, Throwable t) {

                String s = t.getMessage().toString();
                Log.d(TAG, s);
                Toast.makeText(TestUploadFileActivity.this, "上传失败!" + s, Toast.LENGTH_SHORT).show();
            }
        });

在上面的代码中出现了,我们也贴出该方法的代码:

   @NonNull
    private MultipartBody.Part prepareFilePart(File file, String partName) {

        // 为file建立RequestBody实例
        RequestBody requestFile =
                RequestBody.create(MediaType.parse(MULTIPART_FORM_DATA), file);

        // MultipartBody.Part借助文件名完成最终的上传
        return MultipartBody.Part.createFormData(partName, file.getName(), requestFile);
    }

这里解释一下 MultipartBody.Part.createFormData()方法的使用和参数说明。第一个参数partName为与后台协调的进行文件检索的名称,也可以这样理解,如果是txt文件,我们标记为txt,如果是照片,标记为jpg,其他文件标记为file,方便后台管理和存储文件。
我们在看看partName在后台是如何进行区分和使用的?


image.png

根据检索出不同的文件类型,进行不同的操作。

3.1.1了解 multipart/form-data

我们这里这是简单的介绍一下:
RequestBody requestFile =
RequestBody.create(MediaType.parse("multipart/form-data"), file);

这里在看看我们构建RequestBody的代码:这里参数至于为什么使用MediaType.parse("multipart/form-data"),需要我们了解http的传输协议:

在最初的http协议中,没有定义上传文件的Method,为了实现这个功能,http协议组改造了post请求,添加了一种post规范,设定这种规范的Content-Type为multipart/form-data;boundary=bound,其中{bound}是定义的分隔符,用于分割各项内容(文件,key-value对),不然服务器无法正确识别各项内容。post body里需要用到,尽量保证随机唯一。

3.1.2 post格式如下:
–${bound} 
Content-Disposition: form-data; name=”Filename”

HTTP.pdf 
–${bound} 
Content-Disposition: form-data; name=”file000”; filename=”HTTP协议详解.pdf” 
Content-Type: application/octet-stream

%PDF-1.5 
file content 
%%EOF

–${bound} 
Content-Disposition: form-data; name=”Upload”

Submit Query 
–${bound}–
${bound}是Content-Type里boundary的值
3.1.3 Retrofit2 对multipart/form-data的封装

Retrofit其实是个网络代理框架,负责封装请求,然后把请求分发给http协议具体实现者-httpclient。retrofit默认的httpclient是okhttp。

既然Retrofit不实现http,为啥还用它呢。因为他方便!!
Retrofit会根据注解封装网络请求,待httpclient请求完成后,把原始response内容通过转化器(converter)转化成我们需要的对象(object)。

具体怎么使用 retrofit2,请参考: Retrofit2官网

那么Retrofit和okhttp怎么封装这些multipart/form-data上传数据呢

在retrofit中:
@retrofit2.http.Multipart: 标记一个请求是multipart/form-data类型,需要和@retrofit2.http.POST一同使用,并且方法参数必须是@retrofit2.http.Part注解。
@retrofit2.http.Part: 代表Multipart里的一项数据,即用${bound}分隔的内容块。
在okhttp3中:
okhttp3.MultipartBody: multipart/form-data的抽象封装,继承okhttp3.RequestBody
okhttp3.MultipartBody.Part: multipart/form-data里的一项数据。

以上内容摘自 一叶扁舟的博客

3.2 多文件上传:(2种方式:)

3.2.1 第一种是方式:文件个数固定
    @Multipart
    @POST("UploadServlet")
    Call<ResponseBody> uploadMultipleFiles(
            @Part("description") RequestBody description,
            @Part MultipartBody.Part file1,
            @Part MultipartBody.Part file2);
3.2.2 第二种是方式:文件个数不固定
    @Multipart
    @POST("UploadServlet")
    Call<ResponseBody> uploadMapFile(@PartMap Map<String, RequestBody> params);

这里我们只展示文件个数不固定的上传方法的使用:

        File file=new File("/sdcard/img.jpg");
        File file1=new File("/sdcard/ic.jpg");
        File file2=new File("/sdcard/1.txt");

        RequestBody requestBody = RequestBody.create(MediaType.parse("multipart/form-data"), file);
        RequestBody requestBody1 = RequestBody.create(MediaType.parse("multipart/form-data"), file1);
        RequestBody requestBody2 = RequestBody.create(MediaType.parse("multipart/form-data"), file2);

        Map<String, RequestBody> params=new HashMap<>() ;
        params.put("file\"; filename=\""+ file.getName(), requestBody);
        params.put("file\"; filename=\""+ file1.getName(), requestBody1);
        params.put("file\"; filename=\""+ file2.getName(), requestBody2);

        retrofit2.Call call = RetrofitHelper.getUpLoadFileAPI().uploadMapFile(params);
        call.enqueue(new retrofit2.Callback() {
            @Override
            public void onResponse(retrofit2.Call call, retrofit2.Response response) {
                Toast.makeText(TestUploadFileActivity.this, "上传成功!", Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onFailure(retrofit2.Call call, Throwable t) {
                Log.d("Tag",t.getMessage().toString());
                Toast.makeText(TestUploadFileActivity.this, "上传失败!", Toast.LENGTH_SHORT).show();
            }
        });

4、文件的下载

我们都实现了文件的上传了,还不能坚持一下,把文件下载搞定?那必须的必啊!下载文件其实就一个普通的GET 请求,只不过我们处理好IO操作,将response.body()进行保存为自己想要的位置或者处理。

    @GET("u=107188706,3427188039&fm=27&gp=0.jpg")
    Call<ResponseBody> downloadFile();

Activity中的使用:

                Call<ResponseBody> call = RetrofitHelper.getDownloadApi().downloadFile();
                call.enqueue(new Callback<ResponseBody>() {
                    @Override
                    public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
                        boolean writtenToDisk = writeResponseBodyToDisk(response.body());

                        btnDownload.setText(writtenToDisk ? "success" : "false");
                    }

                    @Override
                    public void onFailure(Call<ResponseBody> call, Throwable t) {
                        Toast.makeText(TestUploadFileActivity.this, "fail", Toast.LENGTH_SHORT).show();
                    }
                });

writeResponseBodyToDisk()方法的具体实现:

 private boolean writeResponseBodyToDisk(ResponseBody body) {
        try {
            // todo change the file location/name according to your needs
            File futureStudioIconFile = new File(Environment.getExternalStorageDirectory() + File.separator + "taylor.png");

            Log.d(TAG, "writeResponseBodyToDisk: " + Environment.getExternalStorageDirectory().getAbsolutePath());

            InputStream inputStream = null;
            OutputStream outputStream = null;

            try {
                byte[] fileReader = new byte[4096];

                long fileSize = body.contentLength();
                long fileSizeDownloaded = 0;

                inputStream = body.byteStream();
                outputStream = new FileOutputStream(futureStudioIconFile);

                while (true) {
                    int read = inputStream.read(fileReader);

                    if (read == -1) {
                        break;
                    }

                    outputStream.write(fileReader, 0, read);

                    fileSizeDownloaded += read;

                    Log.d(TAG, "file download: " + fileSizeDownloaded + " of " + fileSize);
                }

                outputStream.flush();

                return true;
            } catch (IOException e) {
                return false;
            } finally {
                if (inputStream != null) {
                    inputStream.close();
                }

                if (outputStream != null) {
                    outputStream.close();
                }
            }
        } catch (IOException e) {
            return false;
        }
    }

5、其他细节问题总结

5.1 @Header:添加http header
@GET("user")
Call<User> getUser(@Header("Authorization") String authorization);

等同于:

@Headers("Authorization: authorization")//这里authorization就是上面方法里传进来变量的值
@GET("widget/list")
Call<User> getUser()
5.2为某个请求设置完整的URL

​ 假如说你的某一个请求不是以baseUrl开头该怎么办呢?别着急,办法很简单,看下面这个例子你就懂了


public interface BlueService {  
    @GET
    public Call<ResponseBody> profilePicture(@Url String url);
}

Retrofit retrofit = Retrofit.Builder()  
    .baseUrl("https://your.api.url/");
    .build();

BlueService service = retrofit.create(BlueService.class);  
service.profilePicture("https://s3.amazon.com/profile-picture/path");

还有一些添加Header的具体的用法:
Header

Retrofit下载上传文件自定义进度实现

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249

推荐阅读更多精彩内容