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下載上傳文件自定義進度實現

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章