OkHttp3簡單使用和封裝使用

OkHttp簡介

OkHttp是一個http協議網絡請求的框架,OkHttp是一個高效的HTTP客戶端,適用于Android和Java應用程序。從Android 4.4開始google已經開始將源碼中的HttpURLConnection替換為OkHttp,而在Android 6.0之后的SDK中google更是移除了對于HttpClient的支持,而現在流行的Retrofit同樣是使用OkHttp進行再次封裝而來的。

其它文章

Retrofit2.0+RxJava2.0封裝使用
Android開發 多語言、指紋登錄、手勢登錄
Android使用IconFont阿里矢量圖標
Android Studio 使用SVN 主干和分支合并代碼

本文章主要講的:

1.Okhttp3簡單使用
2.Okhttp3封裝使用

效果圖

效果圖.gif

項目地址:https://github.com/pengjunshan/UseOkhttp3

拿到代碼后移到自己項目中根據自己項目需求修改即可使用。

OkHttp3特性
  • 支持http2,使得對同一個主機發出的所有請求都可以共享相同的socket套接字連接;
  • 使用連接池來復用連接以減少延遲、提高效率;
  • 支持Gzip壓縮響應體,降低傳輸內容的大小;
  • 支持Http緩存,避免重復請求;
  • 請求失敗時會自動重試主機中的其他IP地址自動重定向;
  • 使用Okio來簡化數據的訪問與存儲,提高性能;
  • 使用了創建者設計模式;

Http簡介

HTTP是一個屬于應用層的面向對象的協議,由于其簡捷、快速的方式,適用于分布式超媒體信息系統。它于1990年提出,經過幾年的使用與發展,得到不斷地完善和擴展。目前在WWW中使用的是HTTP/1.1版本。2.0版本目前也有在使用,只是使用不廣泛。HTTP協議工作于客戶端-服務端架構為上,我們把Http協議中通信的兩方稱作Client和Server,Client端向Server端通過http協議發送一個Request請求,Server端收到Client端發來的Request請求后經過一系列的處理返回Client一個Response,過程如下圖。


Client客戶端 - Server服務端
HTTP請求報文格式
  • HTTP請求報文主要由請求行、請求頭部、請求正文3部分組成。

  • 請求行:由請求方法,URL,協議版本三部分構成。

    • URL是請求服務器地址。
    • 協議版本有HTTP1.0、HTTP1.1,目前HTTP2.0也有使用。
    • HTTP1.0定義了三種請求方法: GET, POST 和 HEAD方法。
    • HTTP1.1新增了五種請求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法。
    • 請求方法
  • 請求頭部為請求報文添加了一些附加信息,由“名/值”對組成,每行一對,名和值之間使用冒號分隔。
    Host ----接受請求的服務器地址,可以是IP:端口號,也可以是域名
    User-Agent ----發送請求的應用程序名稱
    Connection ---- 指定與連接相關的屬性,如Connection:Keep-Alive
    Accept-Charset ---- 通知服務端可以發送的編碼格式
    Accept-Encoding ---- 通知服務端可以發送的數據壓縮格式
    Accept-Language ---- 通知服務端可以發送的語言

  • 請求正文,可選部分,GET請求就沒有請求正文。

HTTP響應報文格式
  • HTTP響應報文主要由狀態碼、響應頭部、響應正文3部分組成。
    • 常用狀態碼
      200:響應成功
      302:重定向跳轉,跳轉地址通過響應頭中的Location屬性指定
      400:客戶端請求有語法錯誤,參數錯誤,不能被服務器識別
      403:服務器接收到請求,但是拒絕提供服務(認證失敗)
      404:請求資源不存在
      500:服務器內部錯誤

    • 響應頭部,與請求頭部類似,為響應報文添加了一些附加信息
      Server - 服務器應用程序軟件的名稱和版本
      Content-Type - 響應正文的類型(是圖片還是二進制字符串)
      Content-Length - 響應正文長度
      Content-Charset - 響應正文使用的編碼
      Content-Encoding - 響應正文使用的數據壓縮格式
      Content-Language - 響應正文使用的語言

    • 響應正文,是請求響應的最終結果,都在響應體里。可以是字符串可以是字符流。

配置
  • maven方式:
<dependency>
  <groupId>com.squareup.okhttp3</groupId>
  <artifactId>okhttp</artifactId>
  <version>3.12.0</version>
</dependency>
  • gradle方式:
   compile 'com.squareup.okhttp3:okhttp:3.12.0'
  • 自動下載okio資源


    Libraries資源
  • 聯網權限
  <uses-permission android:name="android.permission.INTERNET"></uses-permission>

1.簡單使用

HTTP工作中常用方式:
  • get請求
  • post請求
  • 文件上傳
  • 文件下載
  • 圖文混合上傳

Android3.0 以后已經不允許在主線程訪問網絡。需要注意的是這個onResponse回調方法不是在主線程回調,可以使用runOnUIThread(new Runnable(){})更新UI,或者使用Handler在主線程中更新UI。

GET請求

1.第一步創建OkHttpClient對象
2.如果需要拼接參數 (一般有參數的都會用Post請求,除非參數不重要)
3.第二步創建request對象
4.新建一個Call對象
5.同步請求網絡execute()
6.異步請求網絡enqueue(Callback)

      /**
     * 獲取輪播圖接口
     * GET請求
     */
   private void requestBannerApi(){
        //1.第一步創建OkHttpClient對象
        final OkHttpClient okHttpClient = new OkHttpClient();

        String url ="http://www.wanandroid.com/banner/json";

        //2. 如果需要拼接參數 (一般有參數的都會用Post請求,除非參數不重要)
//        Map<String, String> params = new HashMap<>();
//        params.put("movieid", "246363");
//        params.put("limit", "3");
//        params.put("offset", "5");
//        url = appendParams(url,params);

        //3.第二步創建request
        Request.Builder builder = new Request.Builder();
        final Request request = builder.url(url)
                .get()
                .build();

        //4.新建一個Call對象
        final Call call = okHttpClient.newCall(request);

        //5.同步請求網絡execute()
           new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Response response = call.execute();
                    if(response.isSuccessful()){
                        Log.e("Benner請求成功同步=",response.body().string());
                    }else{
                        throw new IOException("Unexpected code " + response);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
        }).start();


        //6.異步請求網絡enqueue(Callback)
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.e("TAG", "Benner請求失敗="+e.getMessage());
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                String json = response.body().string();
                Log.e("TAG", "Benner請求成功異步="+json);
            }
        });

    }

    /**
     * 拼接參數
     * @param url
     * @param params
     * @return
     */
    private String appendParams(String url, Map<String, String> params) {
        StringBuilder sb = new StringBuilder();
        sb.append(url + "?");
        if (params != null && !params.isEmpty()) {
            for (String key : params.keySet()) {
                sb.append(key).append("=").append(params.get(key)).append("&");
            }
        }
        sb = sb.deleteCharAt(sb.length() - 1);
        return sb.toString();
    }

注意execute()同步方式會阻塞調用線程,所以在Android中應放在子線程中執行,否則有可能引起ANR異常。一般都會使用enqueue()異步請求服務器。

POST請求(鍵值對 key-value)

1.拿到okhttpClient對象
2.創建 FormBody 添加需要的鍵值對
3.構造Request
4.創建一個Call對象
5.異步請求enqueue(Callback)

/**
    * 登錄接口
    * POST請求
    * @param account 用戶名
    * @param pwd 密碼
    */
   private void requestLoginApi(String account, String pwd) {
       // 1.拿到okhttpClient對象
       OkHttpClient okHttpClient = new OkHttpClient();

       //2.創建 FormBody 添加需要的鍵值對
       FormBody formBody = new FormBody.Builder()
               .add("username",account)
               .add("password",pwd)
               .build();
    
       // 3.構造Request
       Request.Builder builder = new Request.Builder();
       Request request = builder.url("http://www.wanandroid.com/user/login")
               .post(formBody)//鍵值對
               .build();

       //4.創建一個Call對象
       Call call = okHttpClient.newCall(request);

       //5.異步請求enqueue(Callback)
       call.enqueue(new Callback() {
           @Override
           public void onFailure(Call call, IOException e) {
               Log.e("TAG", "登錄失敗="+e.getMessage());
           }

           @Override
           public void onResponse(Call call, Response response) throws IOException {
               String json = response.body().string();
               UserInfo userInfo = new Gson().fromJson(json,UserInfo.class);
               if(userInfo!=null) {
                   if(userInfo.getErrorCode()!=0) {
                       Log.e("TAG", userInfo.getErrorMsg());
                   }else {
                       Log.e("TAG", "登錄成功="+json);
                   }
               }

           }
       });

   }

如果Post提交的數據是鍵值對就構造一個FormBody對象,可以添加N個鍵值對。

POST請求(json字符串)

1.拿到okhttpClient對象
2.設置提交類型MediaType+json字符串
3.構造Request
4.創建一個Call對象
5.異步請求enqueue(Callback)

   private void requestLoginApi(String account, String pwd) {
       // 1.拿到okhttpClient對象
       OkHttpClient okHttpClient = new OkHttpClient();
       //需要提交的json字符串
      String jsonStr = "hahaha";
      //2.創建 RequestBody 設置提交類型MediaType+json字符串
     RequestBody requestBody =  RequestBody.create(MediaType.parse("application/json"),jsonStr);

       // 3.構造Request
       Request.Builder builder = new Request.Builder();
       Request request = builder.url("http://www.wanandroid.com/user/login")
               .post(requestBody)//字符串
               .build();

       //4.創建一個Call對象
       Call call = okHttpClient.newCall(request);

       //5.異步請求enqueue(Callback)
       call.enqueue(new Callback() {
           @Override
           public void onFailure(Call call, IOException e) {
               Log.e("TAG", "登錄失敗="+e.getMessage());
           }

           @Override
           public void onResponse(Call call, Response response) throws IOException {
               String json = response.body().string();
               UserInfo userInfo = new Gson().fromJson(json,UserInfo.class);
               if(userInfo!=null) {
                   if(userInfo.getErrorCode()!=0) {
                       Log.e("TAG", userInfo.getErrorMsg());
                   }else {
                       Log.e("TAG", "登錄成功="+json);
                   }
               }

           }
       });

   }

如果提交json字符串需要構造一個RequestBody對象,用它來攜帶我們要提交的json字符串數據。在構造 RequestBody 需要指定MediaType,用于描述請求/響應 body 的內容類型。

RequstBody的幾種構造方式
POST上傳(文件)

1.創建OkHttpClient對象
2.獲取文件地址,設置上傳文件類型,構造RequestBody對象
3.構造Requst對象
4.構造Call對象進行 異步請求enqueue(Callback)

 /**
     * 提交txt文件
     * POST請求
     */
    private void requestPostFileTxt(){

        //1.創建OkHttpClient對象
        OkHttpClient okHttpClient = new OkHttpClient();

        //2.獲取文件地址,設置上傳文件類型,構造RequestBody對象
        File fileAdress = new File("/sdcard/wangshu.txt");
        MediaType mediaType = MediaType.parse("text/plain; charset=utf-8");
        RequestBody requestBody = RequestBody.create(mediaType,fileAdress);

        //3.構造Requst對象
        Request request = new Request.Builder()
                .url("http://www.baidu.com")
                .post(requestBody)
                .build();

        //4.構造Call對象進行 異步請求enqueue(Callback)
        okHttpClient.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.e("TAG", "post"+e.getMessage());
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                String json = response.body().string();
                Log.e("TAG", "Benner請求成功異步="+json);
            }
        });

    }

上傳文件本身也是一個post請求,向服務器發送文件時需要備注文件類型Content-Type,可以用MultipartBody上傳多個文件。如果沒有添加charset也沒關系,RequestBody 中已經幫我們寫好了。

charset

常用的Content-Type:
text/plain :純文本格式 .txt
text/xml : XML格式 .xml
image/gif :gif圖片格式 .gif
image/jpeg :jpg圖片格式 .jpg
image/png:png圖片格式 .png
audio/mp3 : 音頻mp3格式 .mp3
audio/rn-mpeg :音頻mpga格式 .mpga
video/mpeg4 : 視頻mp4格式 .mp4
video/x-mpg : 視頻mpa格式 .mpg
video/x-mpeg :視頻mpeg格式 .mpeg
video/mpg : 視頻mpg格式 .mpg
以application開頭的媒體格式類型:
application/xhtml+xml :XHTML格式
application/xml : XML數據格式
application/atom+xml :Atom XML聚合格式
application/json : JSON數據格式
application/pdf :pdf格式
application/msword : Word文檔格式
application/octet-stream : 二進制流數據(如常見的文件下載)

POST上傳圖片

1.創建OkHttpClient對象
2.設置文件類型
3.構造RequestBody 指定文件類型和文件
4.創建Request對象
5.異步請求newCall(Callback)

    /**
     * 上傳圖片
     * @param file
     */
    private void requestPostImg( File file) {

        //1.創建OkHttpClient對象
        OkHttpClient okHttpClient = new OkHttpClient();

        //2.設置文件類型
        MediaType mediaType = MediaType.parse("image/png");

        if (file != null && file.exists()) {

            //3.構造RequestBody 指定文件類型和文件
            RequestBody image = RequestBody.create(mediaType, file);
            RequestBody requestBody = new MultipartBody.Builder()
                    .setType(MultipartBody.FORM)
                    .addFormDataPart("img", file.getName(), image)
                    .build();

            //4.創建Request對象
            Request request = new Request.Builder()
                    .header("Authorization", "Client-ID " + "...")
                    .url("www.baidu.login")
                    .post(multipartBody)
                    .build();

            //5.異步請求newCall(Callback)
            okHttpClient.newCall(request).enqueue(new Callback() {
                @Override
                public void onFailure(Call call, IOException e) {
                    Log.e("TAG", "圖片上傳失敗="+e.getMessage());
                }

                @Override
                public void onResponse(Call call, Response response) throws IOException {
                    String result = response.body().string();
                    Log.e("TAG", "成功上傳圖片=" + result);
                }
            });

        }
    }

MultipartBody繼承RequestBody,具有自己的contentType+BufferedSink,是POST請求的最外層封裝,需要添加多個Part
Part對象組成:Headers+RequestBody。是MultipartBody的成員變量,需要寫入MultipartBody的BufferedSink中。

GET下載圖片

1.創建OkHttpClient對象
2.創建Request對象
3.異步請求newCall(Callback)
4.用文件流下載在本地文件夾下

 /**
   * 上傳圖片
   * 沒有測試服務器地址
   */
  public void PostImgRequet(View view) {
    //1.創建OkHttpClient對象
    OkHttpClient okHttpClient = new OkHttpClient();

    //2.設置文件類型
    MediaType mediaType = MediaType.parse("image/png");

    if (file != null && file.exists()) {

      //3.構造RequestBody 指定文件類型和文件
      RequestBody image = RequestBody.create(mediaType, file);
      RequestBody requestBody = new MultipartBody.Builder()
          .setType(MultipartBody.FORM)
          .addFormDataPart("img", file.getName(), image)
          .build();

      //4.創建Request對象
      Request request = new Request.Builder()
          .header("Authorization", "Client-ID " + "...")
          .url("www.baidu.login")
          .post(requestBody)
          .build();

      //5.異步請求newCall(Callback)
      okHttpClient.newCall(request).enqueue(new Callback() {
        @Override
        public void onFailure(Call call, IOException e) {
          mHandler.sendEmptyMessage(0);
          Log.e("TAG", "圖片上傳失敗="+e.getMessage());
        }

        @Override
        public void onResponse(Call call, Response response) throws IOException {
          String result = response.body().string();
          mHandler.sendEmptyMessage(1);
          Log.e("TAG", "成功上傳圖片=" + result);
        }
      });

    }
  }

  /**
   * 下載圖片
   */
  public void GetImgRequetSimpleness(View view) {
    //1.創建OkHttpClient對象
    OkHttpClient okHttpClient = new OkHttpClient();
    String url = "http://p0.meituan.net/165.220/movie/7f32684e28253f39fe2002868a1f3c95373851.jpg";
    //2.創建Request對象
    Request request  = new Request.Builder()
        .url(url)
        .build();
    //3.異步請求newCall(Callback)
    okHttpClient.newCall(request).enqueue(new Callback() {
      @Override
      public void onFailure(Call call, IOException e) {
        Log.e("TAG", "下載失敗");
        mHandler.sendEmptyMessage(0);
      }
      @Override
      public void onResponse(Call call, Response response) throws IOException {
        runOnUiThread(new Runnable() {
          @Override
          public void run() {
            Toast.makeText(SimplenessActivity.this, "下載圖片成功", Toast.LENGTH_SHORT).show();
          }
        });
        /**
         * 用java文件輸入流下載圖片
         */
               /* InputStream inputStream = response.body().byteStream();
                FileOutputStream fileOutputStream = null;
                try {
                    fileOutputStream = new FileOutputStream(new File("/sdcard/okhttp.jpg"));
                    byte[] buffer = new byte[2048];
                    int len = 0;
                    while ((len = inputStream.read(buffer)) != -1) {
                        fileOutputStream.write(buffer, 0, len);
                    }
                    fileOutputStream.flush();
                } catch (IOException e) {
                    Log.i("TAG", "IOException");
                    e.printStackTrace();
                }*/

        //方法一,獲取byte數組,然后轉換成圖片
        byte[] bytes = response.body().bytes();
        Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);

        //方法二,可以獲取字節流,然后轉換成圖片
//        InputStream inputStream = response.body().byteStream();
//        Bitmap bitmap = BitmapFactory.decodeStream(inputStream);

        /**
         * 保存圖片
         */
 /*       File file=new File("/sdcard/okhttp.jpg");
        file.createNewFile();
        //創建文件輸出流對象用來向文件中寫入數據
        FileOutputStream out=new FileOutputStream(file);
        //將bitmap存儲為jpg格式的圖片
        bitmap.compress(Bitmap.CompressFormat.JPEG,100,out);
        //刷新文件流
        out.flush();
        out.close();*/

        if(bitmap!=null) {
          Log.e("TAG", "圖片下載成功");
        }

      }
    });
  }

下載圖片可以用java文件輸入流下載圖片、BitmapFactory.decodeByteArray、 BitmapFactory.decodeStream,根據自己的需求來使用。

圖文混合上傳

1.創建OkHttpClient對象
2.構建多部件builder
3.創建 Map 添加需要的鍵值對
4.獲取要上傳的圖片集合
5.獲取參數并放到請求體中
6.添加圖片集合到請求體中
7.構造Request
8.異步請求enqueue(Callback)

/**
   * 圖文混合上傳
   * @param view
   */
  public void PostImgKeyValueRequet(View view) {

    //1.創建OkHttpClient對象
    OkHttpClient okHttpClient = new OkHttpClient();

    //2.構建多部件builder
    MultipartBody.Builder bodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM);

    //3.創建 Map 添加需要的鍵值對
    Map<String, String> params = new HashMap<>();
    params.put("username","15294792877");
    params.put("password","15294792877pp");

    //4.獲取要上傳的圖片集合
    List<File> fileList = new ArrayList<>();

    //5.獲取參數并放到請求體中
    try {
      if (params != null) {
        for (Map.Entry<String, String> entry : params.entrySet()) {
          //將請求參數逐一遍歷添加到我們的請求構建類中
          bodyBuilder.addFormDataPart(entry.getKey(), entry.getValue());
        }
      }
    } catch (Exception e) {
      e.printStackTrace();
    }


    //6.添加圖片集合到請求體中
    if (fileList != null) {
      for (File f : fileList) {
        bodyBuilder.addFormDataPart("files", f.getName(),
            RequestBody.create(MediaType.parse("image/png"), f));
      }
    }

    //7.構造Request
    Request request = new Request.Builder()
        .url("https://www.wanandroid.com/user/login")
        .post(bodyBuilder.build())
        .build();

    //8.異步請求enqueue(Callback)
    okHttpClient.newCall(request).enqueue(new Callback() {
      @Override
      public void onFailure(Call call, IOException e) {
        mHandler.sendEmptyMessage(0);
        Log.e("TAG", "失敗="+e.getMessage());
      }

      @Override
      public void onResponse(Call call, Response response) throws IOException {
        String json = response.body().string();
        Log.e("TAG", "成功="+json);
      }
    });

  }

要構建一個多部件MultipartBody.Builder,設置其類型為FORM("multipart/form-data")。然后把需要上傳的key-value鍵值對和圖片都通過addFormDataPart()方法添加進去。添加圖片時name要和后端接口指定name相同,還要添加RequestBody指定類型("image/png")。

封裝使用

如果不封裝使用起來還是很繁瑣的,比如:寫重復的代碼、增加類的代碼量、不易維護、回調函數不在主線程... 接下來我們就來封裝一個。(加泛型使用)

先看下封裝后請求代碼

一個get請求,一個post請求,和上面沒有封裝時相比是不是代碼很簡潔清晰。下面就簡單的講一下封裝的過程,想詳細的看封裝過程請下載demo查看,每個類都有注解。

/**
   * GET請求 
   * 返回類型要Json字符串
   */
  public void GetRequet(View view) {
    HttpRequest.getBannerApi(null, new ResponseCallback<String>() {
      @Override
      public void onSuccess(String s) {
        Toast.makeText(EncapsulationActivity.this, "請求成功" + s.toString(), Toast.LENGTH_SHORT)
            .show();
      }

      @Override
      public void onFailure(OkHttpException failuer) {
        Toast.makeText(EncapsulationActivity.this, "請求失敗=" + failuer.getEmsg(), Toast.LENGTH_SHORT)
            .show();
      }
    });
  }

  /**
   * POST請求 
   * 返回類型我要實體類
   */
  public void PostKeyValueRequet(View view) {
    RequestParams params = new RequestParams();
    params.put("username", "15294792877");
    params.put("password", "15294792877pp");
    HttpRequest.postLoginApi(params, new ResponseCallback<BaseBean<Info>>() {
      @Override
      public void onSuccess(BaseBean<Info> infoBaseBean) {
        Toast.makeText(EncapsulationActivity.this, "成功=" + infoBaseBean.toString(),
            Toast.LENGTH_SHORT).show();
      }

      @Override
      public void onFailure(OkHttpException failuer) {
        Toast.makeText(EncapsulationActivity.this, "失敗=" + failuer.getEmsg(), Toast.LENGTH_SHORT)
            .show();
      }
    });
  }

  /**
   * 下載圖片
   *  可以用GET方式||POST方式,一般是用POST方式 除非你們公司不注重隱式,
   *   本案例用的是GET方式,因為沒有找到免費的POST請求api。
   *
   * @param view
   */
  public void GetImgRequet(View view) {

    HttpRequest.getImgApi(null, String.valueOf(System.currentTimeMillis()) + ".png",
        new ResponseByteCallback() {
          @Override
          public void onSuccess(File file) {
            Toast.makeText(EncapsulationActivity.this, "圖片下載成功="+file.getAbsolutePath(), Toast.LENGTH_SHORT).show();
            Log.e("TAG", "圖片下載成功="+file.getAbsolutePath());
          }

          @Override
          public void onFailure(String failureMsg) {
            Toast.makeText(EncapsulationActivity.this, "圖片下載失敗="+failureMsg, Toast.LENGTH_SHORT).show();
            Log.e("TAG", "圖片下載失敗="+failureMsg);
          }
        });

  }

  /**
   * 圖文混合
   * @param view
   */
  public void PostImgKeyValueRequet(View view) {
    RequestParams params = new RequestParams();
    params.put("name", "aaaaaaa");
    //添加圖片
    List<File> fileList = new ArrayList<>();
//    HttpRequest.postMultipartApi(params, fileList, new ResponseCallback() {
//      @Override
//      public void onSuccess(Object responseObj) {
//
//      }
//
//      @Override
//      public void onFailure(OkHttpException failuer) {
//
//      }
//    });
  }
請求日志.png
OkHttpClient對象

初始化全局OkHttpClient對象,為我們的Client配置參數,使用靜態語句塊來配置,只執行一次,運行一開始就開辟了內存,內存放在全局。主要設置有緩存、超時時間、重定向、攔截器、Https支持,根據自己項目需求類配置就行了。

/**
 * @author:PengJunShan.
 * 時間:On 2019-05-05.
 * 描述:OkHttpClient對象
 */
public class CommonOkHttpClient {

    /**
     * 超時時間
     */
    private static final int TIME_OUT = 30;
    private static OkHttpClient mOkHttpClient;

    /**
     * 為我們的Client配置參數,使用靜態語句塊來配置
     * 只執行一次,運行一開始就開辟了內存,內存放在全局
     */
    static {
        //獲取緩存路徑
        File cacheDir = MyApplication.context.getExternalCacheDir();

        //設置緩存的大小
        int cacheSize = 10 * 1024 *1024 ;
        //創建我們Client對象的構建者
        OkHttpClient.Builder okHttpBuilder = new OkHttpClient.Builder();
        okHttpBuilder
                //為構建者設置超時時間
                .connectTimeout(TIME_OUT, TimeUnit.SECONDS)
                .readTimeout(TIME_OUT, TimeUnit.SECONDS)
                .writeTimeout(TIME_OUT, TimeUnit.SECONDS)
                ////websocket輪訓間隔(單位:秒)
                .pingInterval(20, TimeUnit.SECONDS)
                //設置緩存
                .cache(new Cache(cacheDir.getAbsoluteFile(), cacheSize))
                //允許重定向
                .followRedirects(true)
                //設置攔截器
                .addInterceptor(new RequetInterceptor())
                //添加https支持
                .hostnameVerifier(new HostnameVerifier() {
                    @Override
                    public boolean verify(String s, SSLSession sslSession) {
                        return true;
                    }
                })
                .sslSocketFactory(HttpsUtils.initSSLSocketFactory(), HttpsUtils.initTrustManager());

        mOkHttpClient = okHttpBuilder.build();
    }

    /**
     * 發送具體的HTTP以及Https請求
     */
    public static Call sendRequest(Request request, CommonJsonCallback commonCallback) {
        Call call = mOkHttpClient.newCall(request);
        call.enqueue(commonCallback);
        return call;
    }

    /**
     * GET請求
     */
    public static Call get(Request request, ResposeDataHandle handle) {
        Call call = mOkHttpClient.newCall(request);
        call.enqueue(new CommonJsonCallback(handle));
        return call;
    }

    /**
     * POST請求
     */
    public static Call post(Request request, ResposeDataHandle handle) {
        Call call = mOkHttpClient.newCall(request);
        call.enqueue(new CommonJsonCallback(handle));
        return call;
    }

 /**
   * POST請求圖片
   */
  public static Call downLadImg(Request request, final String imgPath,
      final ResponseByteCallback callback) {
    Call call = mOkHttpClient.newCall(request);
    call.enqueue(new Callback() {
      @Override
      public void onFailure(Call call, IOException e) {
        Log.e("TAG", "下載圖片失敗=" + e.getMessage());
        new Handler().post(new Runnable() {
          @Override
          public void run() {
            callback.onFailure(e.getMessage());
          }
        });

      }

      @Override
      public void onResponse(Call call, Response response) throws IOException {
        Log.e("TAG", "下載圖片成功=" + response);
        File file = null;
        try {
          InputStream is = response.body().byteStream();
          int len = 0;
          // 文件夾路徑
          String pathUrl =
              Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator
                  + "/sgcc/";
          File filepath = new File(pathUrl);
          if (!filepath.exists()) {
            filepath.mkdirs();// 創建文件夾
          }
          file = new File(pathUrl, imgPath);

          FileOutputStream fos = new FileOutputStream(file);

          byte[] buf = new byte[2048];
          while ((len = is.read(buf)) != -1) {
            fos.write(buf, 0, len);
          }
          fos.flush();
          fos.close();
          is.close();
          File finalFile = file;
          new Handler(Looper.getMainLooper()).post(new Runnable() {
            @Override
            public void run() {
              callback.onSuccess(finalFile);
            }
          });
        } catch (final Exception e) {
          new Handler(Looper.getMainLooper()).post(new Runnable() {
            @Override
            public void run() {
              callback.onFailure(e.getMessage());
            }
          });
         
        }

      }
    });
    return call;
  }

}

公共入參(CommonRequest)

我們每次請求都會創建Request對象,寫著重復的代碼,那我們就寫一個類專門處理入參然后返回Request對象。

/**
 * 創建: PengJunShan
 * 描述: 公共入參
 */

public class CommonRequest {

  /**
   * 創建Get請求的Request
   */
  public static Request createGetRequest(String url, RequestParams params) {
    StringBuilder urlBuilder = new StringBuilder(url).append("?");

    if (params != null) {
      for (Map.Entry<String, String> entry : params.urlParams.entrySet()) {
        urlBuilder
            .append(entry.getKey())
            .append("=")
            .append(entry.getValue())
            .append("&");
      }
    }

    return new Request.Builder().url(urlBuilder.substring(0, urlBuilder.length() - 1))
        .get().build();
  }

  /**
   * 創建Post請求的Request
   *
   * @return 返回一個創建好的Request對象
   */
  public static Request createPostRequest(String url, RequestParams params) {
    FormBody.Builder mFromBodyBuilder = new FormBody.Builder();

    //將請求參數逐一遍歷添加到我們的請求構建類中
    for (Map.Entry<String, String> entry : params.urlParams.entrySet()) {
      mFromBodyBuilder.add(entry.getKey(), entry.getValue());
    }

    //通過請求構建類的build方法獲取到真正的請求體對象
    FormBody mFormBody = mFromBodyBuilder.build();
    Request request = new Request.Builder()
        .url(url)
        .post(mFormBody)
        .build();

    return request;
  }


  /**
   * 混合form和圖片
   * @return 返回一個創建好的Request對象
   */
  public static Request createMultipartRequest(String url, RequestParams params, List<File> files) {

    //構建多部件builder
    MultipartBody.Builder bodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM);
    //獲取參數并放到請求體中
    try {
      if (params != null) {
        JSONObject jsonObject = new JSONObject();
        for (Map.Entry<String, String> entry : params.urlParams.entrySet()) {
          //將請求參數逐一遍歷添加到我們的請求構建類中
          bodyBuilder.addFormDataPart(entry.getKey(), entry.getValue());
          jsonObject.put(entry.getKey(), entry.getValue());
        }
        Log.e("TAG", "入參:   " + jsonObject.toString());
      }
    } catch (JSONException e) {
      e.printStackTrace();
    }

    //添加圖片集合放到請求體中
    if (files != null) {
      for (File f : files) {
        bodyBuilder.addFormDataPart("files", f.getName(),
            RequestBody.create(MediaType.parse("image/png"), f));
      }
    }

    Request request = new Request.Builder()
        .url(url)
        .post(bodyBuilder.build())
        .build();

    return request;
  }

}

請求模式(RequestMode)

實際工作中常用的請求模式有:get(無參)、post(key-value)、圖文混合、圖片下載。

/**
 * 創建: PengJunShan
 * 描述:請求模式
 */

public class RequestMode {

  /**
   * GET請求
   * @param url URL請求地址
   * @param params 入參
   * @param callback 回調接口
   * @param clazz 需要解析的實體類
   */
  public static void getRequest(String url, RequestParams params,
      ResponseCallback callback, Class<?> clazz) {
    CommonOkHttpClient.get(CommonRequest.createGetRequest(url, params),
        new ResposeDataHandle(callback, clazz));
  }

  /**
   * POST請求
   * @param url URL請求地址
   * @param params 入參
   * @param callback 回調接口
   * @param clazz 需要解析的實體類
   */
  public static void postRequest(String url, RequestParams params,
      ResponseCallback callback, Class<?> clazz) {
    CommonOkHttpClient.post(CommonRequest.createPostRequest(url, params),
        new ResposeDataHandle(callback, clazz));
  }

  /**
   * 下載圖片 Get方式
   */
  public static void getLoadImg(String url,RequestParams params,String imgPath, ResponseByteCallback callback){
    CommonOkHttpClient.downLadImg(CommonRequest.createGetRequest(url, params),imgPath,callback);
  }

  /**
   * 下載圖片 Post方式
   */
  public static void postLoadImg(String url,RequestParams params,String imgPath, ResponseByteCallback callback){
    CommonOkHttpClient.downLadImg(CommonRequest.createPostRequest(url, params),imgPath,callback);
  }

  /**
   * 表單和媒體 圖文混合
   */
  public static void postMultipart(String url, RequestParams params,
      List<File> files, ResponseCallback callback, Class<?> clazz) {
    CommonOkHttpClient.post(CommonRequest.createMultipartRequest(url, params, files),
        new ResposeDataHandle(callback, clazz));
  }

}
HttpRequest

HttpRequest存放所有的請求接口,我們在activity中請求接口最先就是調用的這個類中的方法。

/**
 * 作者:PengJunShan.
 * 時間:On 2019-05-05.
 * 描述:所有的請求接口
 */
public class HttpRequest {


  /**
   * @param params 入參
   * @param callback 回調接口
   */
  public static void getBannerApi(RequestParams params, ResponseCallback<String> callback) {
    RequestMode.getRequest("https://www.wanandroid.com/banner/json", params, callback);
  }

  /**
   * @param params 入參
   * @param callback 回調接口
   */
  public static void postLoginApi(RequestParams params, ResponseCallback<BaseBean<Info>> callback) {
    RequestMode.postRequest("https://www.wanandroid.com/user/login", params, callback);
  }

  /**
   * 下載圖片 Get方式
   * @param params 入參
   * @param imgPath 存儲地址
   * @param callback 回調接口
   */
  public static void getImgApi(RequestParams params,String imgPath, ResponseByteCallback callback) {
    RequestMode.getLoadImg("http://p0.meituan.net/165.220/movie/7f32684e28253f39fe2002868a1f3c95373851.jpg",params,imgPath,callback);
  }

  /**
   * 下載圖片 Post方式
   * @param params 入參
   * @param imgPath 存儲地址
   * @param callback 回調接口
   */
  public static void postImgApi(RequestParams params,String imgPath, ResponseByteCallback callback) {
    RequestMode.postLoadImg("url地址",params,imgPath,callback);
  }

  /**
   * 圖文混合上傳服務器
   * @param params
   * @param files
   * @param callback
   */
  public static void postMultipartApi(RequestParams params, List<File> files, ResponseCallback callback) {
    RequestMode.postMultipart("url地址", params, files, callback, null);
  }

}
回調ResponseCallback<T>

每次請求api都創建這個抽象類并實現其抽象方法,通過回傳把數據回調。

/**
 * 創建: PengJunShan 描述:回調 使用泛型
 */

public abstract class ResponseCallback<T> {

  Type mType;

  public ResponseCallback() {

    //Type是 Java 編程語言中所有類型的公共高級接口。它們包括原始類型、參數化類型、數組類型、類型變量和基本類型。
    Type superclass = getClass().getGenericSuperclass();

    if (superclass instanceof Class) {
//      throw new RuntimeException("請傳入實體類");
      mType = null;
    } else {
      //ParameterizedType參數化類型,即泛型
      ParameterizedType parameterized = (ParameterizedType) superclass;

      //getActualTypeArguments獲取參數化類型的數組,泛型可能有多個
      //將Java 中的Type實現,轉化為自己內部的數據實現,得到gson解析需要的泛型
      mType = $Gson$Types.canonicalize(parameterized.getActualTypeArguments()[0]);
    }

  }

  //請求成功回調事件處理
  public abstract void onSuccess(T t);

  //請求失敗回調事件處理
  public abstract void onFailure(OkHttpException failuer);

}
處理JSON數據(CommonJsonCallback)

當我們請求到Json數據后不是直接返給最前端的接口,而是先進行解析處理。讓這個類繼承Callback接口,實現onFailure()、onResponse()方法。如果走了onFailure()失敗中,通過Exception類型判斷失敗原因。如果走了onResponse()中,首先是獲取errorMsg值來判斷是否成功然后解析數據。大家都知道CallBck的回調是在子線程中不能操作UI,那該怎么辦呢?我們在創建Handler時通過Looper.getMainLooper(),獲得主線程的Looper,將其綁定到此Handler對象上,這種情況下,Runnable對象是運行在主線程中可以更新UI操作。

/**
 * 創建: PengJunShan
 * 描述:專門處理JSON數據的回調響應
 */

public class CommonJsonCallback<T> implements Callback {

  /**
   * errorCode是根據接口返回的標識 實際根據自己接口返回為準
   */
  protected final String RESULT_CODE = "errorCode";
  protected final int RESULT_CODE_VALUE = 0;

  /**
   * errorMsg字段提示信息,實際根據自己接口返回為準
   */
  protected final String ERROR_MSG = "errorMsg";

  protected final String NETWORK_MSG = "請求失敗";
  protected final String JSON_MSG = "解析失敗";

  /**
   * 自定義異常類型
   */
  protected final int NETWORK_ERROR = -1; //網絡失敗
  protected final int JSON_ERROR = -2; //解析失敗
  protected final int OTHER_ERROR = -3; //未知錯誤
  protected final int TIMEOUT_ERROR = -4; //請求超時

  private Handler mDeliveryHandler; //進行消息的轉發
  private ResponseCallback<T> mListener;

  public CommonJsonCallback(ResposeDataHandle handle) {
    this.mListener = handle.mListener;
    this.mDeliveryHandler = new Handler(Looper.getMainLooper());
  }

  /**
   * 請求失敗的處理
   */
  @Override
  public void onFailure(@NonNull Call call, @NonNull final IOException e) {
    Log.e("TAG", "請求失敗=" + e.getMessage());
    mDeliveryHandler.post(new Runnable() {
      @Override
      public void run() {
        if (!Utils.isConnected(MyApplication.context)) {
          mListener.onFailure(new OkHttpException(NETWORK_ERROR, "請檢查網絡"));
        } else if (e instanceof SocketTimeoutException) {
          //判斷超時異常
          mListener.onFailure(new OkHttpException(TIMEOUT_ERROR, "請求超時"));
        } else if (e instanceof ConnectException) {
          //判斷超時異常
          mListener.onFailure(new OkHttpException(OTHER_ERROR, "請求服務器失敗"));
        } else {
          mListener.onFailure(new OkHttpException(NETWORK_ERROR, e.getMessage()));
        }

      }
    });
  }

  /**
   * 請求成功的處理 回調在主線程
   */
  @Override
  public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
    final String result = response.body().string();
    mDeliveryHandler.post(new Runnable() {
      @Override
      public void run() {
        handleResponse(result);
      }
    });
  }

  /**
   * 處理Http成功的響應
   */
  private void handleResponse(Object responseObj) {
    if (responseObj == null && responseObj.toString().trim().equals("")) {
      mListener.onFailure(new OkHttpException(NETWORK_ERROR, NETWORK_MSG));
      return;
    }

    try {
      JSONObject result = new JSONObject(responseObj.toString());
      if (result.has(RESULT_CODE)) {
        //從JSON對象中取出我們的響應碼,如果為0,則是正確的響應 (實際情況按你們接口文檔)
        if (result.getInt(RESULT_CODE) == RESULT_CODE_VALUE) {

          /**
           * 判斷是否需要解析成實體類還是json字符串
           * class com.google.gson.internal.$Gson$Types$ParameterizedTypeImpl
           */
          Gson gson = new GsonBuilder().serializeNulls().create();
          T obj = null;
          if (!mListener.mType.getClass().equals("java.lang.Class")) {
            obj = gson.fromJson((String) responseObj, mListener.mType);
          } else {
            obj = (T) responseObj;
          }

          if (obj != null) {
            mListener.onSuccess(obj);
          } else {
            mListener.onFailure(new OkHttpException(JSON_ERROR, JSON_MSG));
          }
        } else { //將服務端返回的異常回調到應用層去處理
          mListener.onFailure(new OkHttpException(OTHER_ERROR, result.get(ERROR_MSG) + ""));
          Log.e("TAG", "onResponse處理失敗");
        }
      }
    } catch (Exception e) {
      e.printStackTrace();
      mListener.onFailure(new OkHttpException(OTHER_ERROR, e.getMessage()));
      Log.e("TAG", "onResponse處理失敗" + e.getMessage());
    }
  }

}
日志攔截器(RequetInterceptor)

攔截器的作用還是很大的,一般我們工作中請求頭部都會傳入token、用戶id標識認證參數。連接器中可以攔截到Request對象,然后添加頭部公共參數。通過獲取FormBody可以打印出入參參數,通過獲取Response可以打印出出參參數。不能直接使用response.body().string()的方式輸出日志,因為response.body().string()之后,response中的流會被關閉,我們需要創建出一個新的response給應用層處理。

/**
 * @author:PengJunShan.
 * 時間:On 2019-05-05.
 * 描述:日志攔截器
 */
public class RequetInterceptor implements Interceptor {

  /**
   * 這個chain里面包含了request和response,所以你要什么都可以從這里拿
   */
  @Override
  public Response intercept(Chain chain) throws IOException {

    /**
     * 可以添加公共頭部參數如token
     */
    Request request = chain.request()
        .newBuilder()
//        .header("TOKEN", token)
//        .header("ID", id)
        .build();

    /**
     * 開始時間
     */
    long startTime = System.currentTimeMillis();
    Log.e("TAG","\n"+"requestUrl=" + request.url());
    String method = request.method();

    if ("POST".equals(method)) {
      try {
        JSONObject jsonObject = new JSONObject();
        if (request.body() instanceof FormBody) {
          FormBody body = (FormBody) request.body();
          for (int i = 0; i < body.size(); i++) {
            jsonObject.put(body.encodedName(i), body.encodedValue(i));
          }
          Log.e("TAG","入參JSON= " + jsonObject.toString());
        }
      } catch (JSONException e) {
        e.printStackTrace();
      }
    }

    Response response = chain.proceed(request);
    /**
     * 這里不能直接使用response.body().string()的方式輸出日志
     * 因為response.body().string()之后,response中的流會被關閉,程序會報錯,我們需要創建出一個新的response給應用層處理
     */
    ResponseBody responseBody = response.peekBody(1024 * 1024);
    Log.e("TAG","出參JSON=" + responseBody.string());
    long endTime = System.currentTimeMillis();
    long duration = endTime - startTime;
    Log.e("TAG","----------" + "耗時:" + duration + "毫秒----------");
    return response;
  }

}
結束

網上有很多別人封裝好的庫,但是符不符合自己的項目使用就是另外一回事了。還不如自己花點時間封裝一個既簡單又符合自己項目使用的代碼,文章中貼出來的代碼是核心代碼并不是所有的代碼,下載代碼根據自己項目需求修改一下就可以用的。下篇我要寫一個以retrofit2+rxjava2進行網絡請求封裝。

項目地址:https://github.com/pengjunshan/UseOkhttp3

拿到代碼后移到自己項目中根據自己項目需求修改即可使用。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,698評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,202評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,742評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,580評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,297評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,688評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,693評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,875評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,438評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,183評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,384評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,931評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,612評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,022評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,297評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,093評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,330評論 2 377