Retrofit基本使用和源碼解析

目錄介紹

  • 1.關(guān)于Retrofit基本介紹
  • 2.最簡單使用【配合Rx使用】
  • 3.注解的種類
    • 請求方法注解
    • 請求頭注解
    • 標記注解
    • 參數(shù)注解
    • 其它注解
  • 4.Retrofit相關(guān)請求參數(shù)
    • @Query()【備注:get請求/ 接上參數(shù) 】
    • @QueryMap()【備注:get請求/ 接上參數(shù) 】
    • @Path()【備注:get請求/ 替換url中某個字段】
    • @Body()【備注:post請求/ 指定一個對象作為HTTP請求體】
    • @Field()【備注:post請求/ 用于傳送表單數(shù)據(jù)】
    • @FieldMap()【備注:post請求/ 用于傳送表單數(shù)據(jù)】
    • @Header/@Headers()【備注: 添加請求頭部 】
    • @Part()作用于方法的參數(shù),用于定義Multipart請求的每和part
    • @PartMap()作用于方法的參數(shù)
    • 使用時注意事項
  • 5.Retrofit與RxJava結(jié)合
    • 使Rxjava與retrofit結(jié)合條件
    • 可以看到 Observable觀察者
    • 可以看到訂閱者
  • 6.OkHttpClient
    • 攔截器說明
    • 日志攔截器
    • 請求頭攔截器
    • 統(tǒng)一請求攔截器
    • 緩存攔截器
    • 自定義CookieJar
  • 7.踩坑經(jīng)驗
    • url被轉(zhuǎn)義
  • 8.Form表單提交與multipart/form-data
    • 8.1 form表單常用屬性
    • 8.2 瀏覽器提交表單時程储,會執(zhí)行如下步驟
    • 8.3 提交方式
    • 8.4 POST請求
    • 8.5 enctype指定的content-type
  • 9.content-type介紹
    • 9.1 application/x-www-form-urlencoded
    • 9.2 application/json
    • 9.3 text/xml
    • 9.4 multipart/form-data
  • 10.Retrofit源碼深入分析
    • 10.1 設(shè)計模式分析[建造者模式]
    • 10.2 如何理解動態(tài)代理模式
    • 10.3 如何攔截方法傀蚌,解析注解
    • 10.4 如何構(gòu)建Retrofit的Call
    • 10.5 如何執(zhí)行網(wǎng)絡(luò)異步請求enqueue方法
  • N.關(guān)于其他
    • 參考博客
    • 版本更新說明
    • 博客介紹

好消息

  • 博客筆記大匯總【16年3月到至今】衣形,包括Java基礎(chǔ)及深入知識點寇蚊,Android技術(shù)博客空入,Python學習筆記等等义黎,還包括平時開發(fā)中遇到的bug匯總诉瓦,當然也在工作之余收集了大量的面試題冷尉,長期更新維護并且修正漱挎,持續(xù)完善……開源的文件是markdown格式的!同時也開源了生活博客雀哨,從12年起磕谅,積累共計47篇[近20萬字],轉(zhuǎn)載請注明出處雾棺,謝謝膊夹!
  • 鏈接地址:https://github.com/yangchong211/YCBlogs
  • 如果覺得好,可以star一下捌浩,謝謝放刨!當然也歡迎提出建議,萬事起于忽微尸饺,量變引起質(zhì)變进统!

1.關(guān)于Retrofit基本介紹

  • Retrofit是Square 公司開發(fā)的一款正對Android 網(wǎng)絡(luò)請求的框架。底層基于OkHttp 實現(xiàn)浪听,OkHttp 已經(jīng)得到了google 官方的認可螟碎。
  • Retrofit是由Square公司出品的針對于Android和Java的類型安全的Http客戶端,如果看源碼會發(fā)現(xiàn)其實本質(zhì)上是OkHttp的封裝迹栓,使用面向接口的方式進行網(wǎng)絡(luò)請求掉分,利用動態(tài)生成的代理類封裝了網(wǎng)絡(luò)接口請求的底層,其將請求返回JavaBean迈螟,對網(wǎng)絡(luò)認證REST API進行了很友好的支持叉抡。使用Retrofit將會極大的提高我們應用的網(wǎng)絡(luò)體驗。
  • RxJava + Retrofit + okHttp組合答毫,流行的網(wǎng)絡(luò)請求框架
    • Retrofit 負責請求的數(shù)據(jù)和請求的結(jié)果褥民,使用接口的方式呈現(xiàn),OkHttp 負責請求的過程洗搂,RxJava 負責異步消返,各種線程之間的切換。
    • RxJava 在 GitHub 主頁上的自我介紹是 "a library for composing asynchronous and event-based programs using observable sequences for the Java VM"(一個在 Java VM 上使用可觀測的序列來組成異步的耘拇、基于事件的程序的庫)撵颊。這就是 RxJava ,概括得非常精準惫叛〕拢總之就是讓異步操作變得非常簡單。
  • 為什么要使用Retrofit嘉涌?
    • 優(yōu)點
      • 請求的方法參數(shù)注解可以定制
      • 支持同步妻熊、異步和RxJava
      • 超級解耦
      • 可以配置不同的反序列化工具來解析數(shù)據(jù)夸浅,如json、xml等
    • 其他說明
      • 在處理HTTP請求的時候扔役,因為不同場景或者邊界情況等比較難處理帆喇。你需要考慮網(wǎng)絡(luò)狀態(tài),需要在請求失敗后重試亿胸,需要處理HTTPS等問題坯钦,二這些事情讓你很苦惱,而Retrofit可以將你從這些頭疼的事情中解放出來侈玄。
        • 效率高婉刀,其次Retrofit強大且配置靈活,第三和OkHttp無縫銜接序仙,第四Jack Wharton主導的(你懂的)路星。

2.最簡單使用

  • Api接口
public interface DouBookApi {
    /**
    * 根據(jù)tag獲取圖書
    * @param tag  搜索關(guān)鍵字
    * @param count 一次請求的數(shù)目 最多100
    *              https://api.douban.com/v2/book/search?tag=文學&start=0&count=30
    */
    @GET("v2/book/search")
    Observable<DouBookBean> getBook(@Query("tag") String tag,
                                    @Query("start") int start,
                                    @Query("count") int count);
}
  • Model類
public class DouBookModel {

    private static DouBookModel bookModel;
    private DouBookApi mApiService;

    public DouBookModel(Context context) {
        mApiService = RetrofitWrapper
                .getInstance(ConstantALiYunApi.API_DOUBAN)   //baseUrl地址
                .create(DouBookApi.class);
    }

    public static DouBookModel getInstance(Context context){
        if(bookModel == null) {
            bookModel = new DouBookModel(context);
        }
        return bookModel;
    }

    public Observable<DouBookBean> getHotMovie(String tag, int start , int count) {
        Observable<DouBookBean> book = mApiService.getBook(tag, start, count);
        return book;
    }
}
  • 抽取類
public class RetrofitWrapper {

    private static RetrofitWrapper instance;
    private Retrofit mRetrofit;

    public RetrofitWrapper(String url) {
        OkHttpClient.Builder builder = new OkHttpClient.Builder();

        //打印日志
        HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
        logging.setLevel(HttpLoggingInterceptor.Level.BODY);
        builder.addInterceptor(logging).build();
        OkHttpClient client = builder.addInterceptor(new LogInterceptor("HTTP")).build();

        //解析json
        Gson gson = new GsonBuilder()
                .setLenient()
                .create();
        
        mRetrofit = new Retrofit
                .Builder()
                .baseUrl(url)
                .addConverterFactory(GsonConverterFactory.create(gson))
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .client(client)
                .build();
    }

    public  static RetrofitWrapper getInstance(String url){
        //synchronized 避免同時調(diào)用多個接口,導致線程并發(fā)
        synchronized (RetrofitWrapper.class){
            instance = new RetrofitWrapper(url);
        }
        return instance;
    }

    public <T> T create(final Class<T> service) {
        return mRetrofit.create(service);
    }
}
  • 使用
DouBookModel model = DouBookModel.getInstance(activity);
model.getHotMovie(mType,start,count)
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Subscriber<DouBookBean>() {
            @Override
            public void onCompleted() {

            }

            @Override
            public void onError(Throwable e) {

            }

            @Override
            public void onNext(DouBookBean bookBean) {

            }
        });

3.注解的種類

  • 請求方法注解
@GET        get請求
@POST       post請求
@PUT        put請求
@DELETE     delete請求
@PATCH      patch請求诱桂,該請求是對put請求的補充洋丐,用于更新局部資源
@HEAD       head請求
@OPTIONS    option請求
@HTTP       通用注解,可以替換以上所有的注解,其擁有三個屬性:method挥等,path友绝,hasBody
  • 請求頭注解
@Headers    用于添加固定請求頭,可以同時添加多個肝劲。通過該注解添加的請求頭不會相互覆蓋迁客,而是共同存在
@Header     作為方法的參數(shù)傳入,用于添加不固定值的Header辞槐,該注解會更新已有的請求頭
  • 標記注解
@FormUrlEncoded    
表示請求發(fā)送編碼表單數(shù)據(jù)掷漱,每個鍵值對需要使用@Field注解
用于修飾Fiedl注解 和FileldMap注解
使用該注解,表示請求正文將使用表單網(wǎng)址編碼榄檬。字段應該聲明為參數(shù)卜范,并用@Field 注解和 @FieldMap 注解鹿榜,使用@FormUrlEncoded 注解的請求將具有"application/x-www-form-urlencoded" MIME類型舱殿。字段名稱和值將先進行UTF-8進行編碼奥裸,再根據(jù)RFC-3986進行URI編碼。

@Multipart    
作用于方法     
表示請求發(fā)送multipart數(shù)據(jù)沪袭,使用該注解湾宙,表示請求體是多部分的,每個部分作為一個參數(shù),且用Part注解聲明侠鳄。

@Streaming         
作用于方法
未使用@Straming 注解嗡害,默認會把數(shù)據(jù)全部載入內(nèi)存畦攘,之后通過流獲取數(shù)據(jù)也是讀取內(nèi)存中數(shù)據(jù)知押,所以返回數(shù)據(jù)較大時鹃骂,需要使用該注解台盯。
處理返回Response的方法的響應體,用于下載大文件
提醒:如果是下載大文件必須加上@Streaming 否則會報OOM
@Streaming
@GET
Call<ResponseBody> downloadFileWithDynamicUrlAsync(@Url String fileUrl);
  • 參數(shù)注解
參數(shù)注解:@Query 静盅、@QueryMap市咽、@Body、@Field、@FieldMap躺酒、@Part劫灶、@PartMap
  • 其它注解
@Path、@Url

4.Retrofit相關(guān)請求參數(shù)

  • @Query()【備注:get請求/ 接上參數(shù) 】
@Query:作用于方法參數(shù)掖桦,用于添加查詢參數(shù)本昏,即請求參數(shù)
用于在url后拼接上參數(shù),例如:
@GET("book/search")
Call<Book> getSearchBook(@Query("q") String name);//name由調(diào)用者傳入

相當于
@GET("book/search?q=name")
Call<Book> getSearchBook();
用于Get中指定參數(shù)
  • @QueryMap()【備注:get請求/ 接上參數(shù) 】
@QueryMap:作用于方法的參數(shù)枪汪。以map的形式添加查詢參數(shù)雀久,即請求參數(shù)宿稀,參數(shù)的鍵和值都通過String.valueOf()轉(zhuǎn)換為String格式。默認map的值進行URL編碼赖捌,map中的每一項發(fā)鍵和值都不能為空祝沸,否則跑出IllegalArgumentException異常。
當然如果入?yún)⒈容^多越庇,就可以把它們都放在Map中罩锐,例如:
@GET("book/search")
Call<Book> getSearchBook(@QueryMap Map<String, String> options);
  • @Path()【備注:get請求/ 替換url中某個字段】
/**
 * http://api.zhuishushenqi.com/ranking/582ed5fc93b7e855163e707d
 * @return
 */
@GET("/ranking/{rankingId}")
Observable<SubHomeTopBean> getRanking(@Path("rankingId") String rankingId);


@GET("group/{id}/users")
Call<Book> groupList(@Path("id") int groupId);
* 像這種請求接口,在group和user之間有個不確定的id值需要傳入卤唉,就可以這種方法唯欣。我們把待定的值字段用{}括起來,當然 {}里的名字不一定就是id搬味,可以任取境氢,但需和@Path后括號里的名字一樣。如果在user后面還需要傳入?yún)?shù)的話碰纬,就可以用Query拼接上萍聊,比如:
@GET("group/{id}/users")
Call<Book> groupList(@Path("id") int groupId, @Query("sort") String sort);
* 當我們調(diào)用這個方法時,假設(shè)我們groupId傳入1悦析,sort傳入“2”寿桨,那么它拼接成的url就是group/1/users?sort=2,當然最后請求的話還會加上前面的baseUrl
  • @Body()【備注:post請求/ 指定一個對象作為HTTP請求體】
使用@Body 注解定義的參數(shù)不能為null 强戴。當你發(fā)送一個post或put請求亭螟,但是又不想作為請求參數(shù)或表單的方式發(fā)送請求時,使用該注解定義的參數(shù)可以直接傳入一個實體類,retrofit會通過convert把該實體序列化并將序列化的結(jié)果直接作為請求體發(fā)送出去。

可以指定一個對象作為HTTP請求體,比如:
@POST("users/new")
Call<User> createUser(@Body User user);
它會把我們傳入的User實體類轉(zhuǎn)換為用于傳輸?shù)腍TTP請求體当纱,進行網(wǎng)絡(luò)請求。
多用于post請求發(fā)送非表單數(shù)據(jù),比如想要以post方式傳遞json格式數(shù)據(jù)
  • @Field()【備注:post請求/ 用于傳送表單數(shù)據(jù)】
用于傳送表單數(shù)據(jù):
@FormUrlEncoded
@POST("user/edit")
Call<User> updateUser(@Field("first_name") String first, @Field("last_name") String last);
注意開頭必須多加上@FormUrlEncoded這句注釋扁掸,不然會報錯翘县。表單自然是有多組鍵值對組成,這里的first_name就是鍵谴分,而具體傳入的first就是值啦
多用于post請求中表單字段,Filed和FieldMap需要FormUrlEncoded結(jié)合使用
  • @FieldMap()【備注:post請求/ 用于傳送表單數(shù)據(jù)】
@FormUrlEncoded
@POST("user/login")
Call<User> login(@FieldMap Map<String,String> map);
  • @Header/@Headers()【備注: 添加請求頭部 】
用于動態(tài)添加請求頭部:
@GET("user")
Call<User> getUser(@Header("Authorization") String authorization)

表示將頭部Authorization屬性設(shè)置為你傳入的authorization锈麸;當然你還可以用@Headers表示,作用是一樣的比如:
@Headers("Cache-Control: max-age=640000")
@GET("user")
Call<User> getUser()

當然你可以多個設(shè)置:
@Headers({
    "Accept: application/vnd.github.v3.full+json",
    "User-Agent: Retrofit-Sample-App"
})
@GET("user")
Call<User> getUser()
  • @Part()作用于方法的參數(shù),用于定義Multipart請求的每和part
使用該注解定義的參數(shù)牺蹄,參數(shù)值可以為空忘伞,為空時,則忽略沙兰。使用該注解定義的參數(shù)類型有如下3中方式可選:
1 okhttp2.MulitpartBody.Part氓奈,內(nèi)容將被直接使用。省略part中的名稱僧凰,即@Part MultipartBody.Part part
2 如果類型是RequestBody,那么該值直接與其內(nèi)容類型一起使用熟丸。在注釋中提供part名稱(例如训措,@Part("foo") RequestBody foo)
3 其它對象類型將通過使用轉(zhuǎn)換器轉(zhuǎn)換為適當?shù)母袷健T谧⑨屩刑峁﹑art名稱(例如光羞,@Part("foo") Image photo)绩鸣。
@Multipart
@POST("/")
Call<ResponseBody> example(
       @Part("description") String description,
       @Part(value = "image", encoding = "8-bit") RequestBody image);
  • @PartMap()作用于方法的參數(shù)
以map的方式定義Multipart請求的每個part map中每一項的鍵和值都不能為空,否則拋出IllegalArgumentException異常纱兑。
使用@PartMap 注解定義的參數(shù)類型有一下兩種:
1 如果類型是RequestBody呀闻,那么該值將直接與其內(nèi)容類型與其使用。
2 其它對象類型將通過使用轉(zhuǎn)換器轉(zhuǎn)換為適當?shù)母袷健?

使用時注意事項

  • 1潜慎、Map用來組合復雜的參數(shù)捡多,并且對于FieldMap,HeaderMap铐炫,PartMap垒手,QueryMap這四種作用方法的注解,其參數(shù)類型必須為Map實例倒信,且key的類型必須為String類型科贬,否則拋出異常。
  • 2鳖悠、Query榜掌、QueryMap與Field、FieldMap功能一樣乘综,生成的數(shù)據(jù)形式一樣憎账;Query、QueryMap的數(shù)據(jù)體現(xiàn)在Url上卡辰;Field鼠哥、FieldMap的數(shù)據(jù)是請求體
  • 3熟菲、{占位符}和PATH盡量只用在URL的path部分,url的參數(shù)使用Query朴恳、QueryMap代替抄罕,保證接口的簡潔
  • 4、Query于颖、Field呆贿、Part支持數(shù)據(jù)和實現(xiàn)了iterable接口的類型,如List森渐、Set等做入,方便向后臺傳遞數(shù)組,代碼如下:
  • 5、以上部分注解真正的實現(xiàn)在ParameterHandler類中同衣,每個注解的真正實現(xiàn)都是ParameterHandler類中的一個final類型的內(nèi)部類竟块,每個內(nèi)部類都對各個注解的使用要求做了限制,比如參數(shù)是否可空耐齐、鍵和值是否可空等浪秘。
  • 6、@FormUrlEncoded 注解和@Multipart 注解不能同時使用埠况,否則會拋出methodError(“Only one encoding annotation is allowed.”)耸携,可在ServiceMethod類中parseMethodAnnotation()方法中找到不能同時使用的具體原因。
  • 7辕翰、@Path 與@Url 注解不能同時使用夺衍,否則會拋出parameterError(p, "@Path parameters may not be used with @Url."),可在ServcieMethod類中parseParameterAnnotation()方法中找到不能同時使用的具體代碼喜命。其實原因也是很好理解:Path注解用于替換url中的參數(shù)沟沙,這就要求在使用path注解時,必須已經(jīng)存在請求路徑壁榕。不然沒法替換路徑中指定的參數(shù)尝胆。而@Url 注解是在參數(shù)中指定了請求路徑的,這時候情定請求路徑已經(jīng)晚护桦,path注解找不到請求路徑含衔,更別提更換請求路徑了中的參數(shù)了。
  • 8二庵、使用@Body 注解的參數(shù)不能使用form 或multi-part編碼贪染,即如果為方法使用了FormUrlEncoded或Multipart注解,則方法的參數(shù)中不能使用@Body 注解催享,否則會拋出異常parameterError(p, “@Body parameters cannot be used with form or multi-part encoding.”)

5.Retrofit與RxJava結(jié)合

使Rxjava與retrofit結(jié)合條件

  • 在Retrofit對象建立的時候添加一句代碼addCallAdapterFactory(RxJavaCallAdapterFactory.create())
完整代碼
mRetrofit = new Retrofit
        .Builder()
        .baseUrl(url)
        .addConverterFactory(GsonConverterFactory.create(gson))
        .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
        .client(client)
        .build();
  • 可以看到 Observable觀察者
public Observable<DouBookBean> getHotMovie(String tag, int start , int count) {
    Observable<DouBookBean> book = mApiService.getBook(tag, start, count);
    return book;
}
  • 可以看到訂閱者
    • RxAndroid其實就是對RxJava的擴展杭隙。比如上面這個Android主線程在RxJava中就沒有,因此要使用的話就必須得引用RxAndroid
DouBookModel model = DouBookModel.getInstance(activity);
model.getHotMovie(mType,start,count)
        .subscribeOn(Schedulers.io())                    //請求數(shù)據(jù)的事件發(fā)生在io線程
        .observeOn(AndroidSchedulers.mainThread())        //請求完成后在主線程更顯UI
        .subscribe(new Observer<DouBookBean>() {        //訂閱
            @Override
            public void onCompleted() {
                //所有事件都完成因妙,可以做些操作痰憎。票髓。
            }

            @Override
            public void onError(Throwable e) {
                e.printStackTrace(); //請求過程中發(fā)生錯誤
            }

            @Override
            public void onNext(DouBookBean bookBean) {
                //這里的book就是我們請求接口返回的實體類
            }
        });

6.OkHttpClient

  • 攔截器說明
    • addNetworkInterceptor添加的是網(wǎng)絡(luò)攔截器Network,Interfacetor它會在request和response時分別被調(diào)用一次铣耘;
    • addInterceptor添加的是應用攔截器Application Interceptor他只會在response被調(diào)用一次洽沟。
  • 日志攔截器
    • 一種是使用HttpLoggingInterceptor,需要使用到依賴
    compile 'com.squareup.okhttp3:logging-interceptor:3.5.0'
    
    /**
     * 創(chuàng)建日志攔截器
     * @return
     */
    public static HttpLoggingInterceptor getHttpLoggingInterceptor() {
        HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
            @Override
            public void log(String message) {
                Log.e("OkHttp", "log = " + message);
            }
        });
        loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
        return loggingInterceptor;
    }
    
  • 另一種是創(chuàng)建自定義日志攔截器
  • 請求頭攔截器
/**
 * 請求頭攔截器
 * 使用addHeader()不會覆蓋之前設(shè)置的header,若使用header()則會覆蓋之前的header
 * @return
 */
public static Interceptor getRequestHeader() {
    Interceptor headerInterceptor = new Interceptor() {
        @Override
        public okhttp3.Response intercept(Chain chain) throws IOException {
            Request originalRequest = chain.request();
            Request.Builder builder = originalRequest.newBuilder();
            builder.addHeader("version", "1");
            builder.addHeader("time", System.currentTimeMillis() + "");
            Request.Builder requestBuilder = builder.method(originalRequest.method(), originalRequest.body());
            Request request = requestBuilder.build();
            return chain.proceed(request);
        }
    };
    return headerInterceptor;
}
使用addInterceptor()方法添加到OkHttpClient中
我的理解是,請求頭攔截器是為了讓服務端能更好的識別該請求,服務器那邊通過請求頭判斷該請求是否為有效請求等...
  • 統(tǒng)一請求攔截器
    • 使用addInterceptor()方法添加到OkHttpClient中蜗细,統(tǒng)一請求攔截器的功能跟請求頭攔截器相類似
/**
 * 統(tǒng)一請求攔截器
 * 統(tǒng)一的請求參數(shù)
 */
public static Interceptor commonParamsInterceptor() {
    Interceptor commonParams = new Interceptor() {
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request originRequest = chain.request();
            Request request;
            HttpUrl httpUrl = originRequest.url().newBuilder()
                    .addQueryParameter("paltform", "android")
                    .addQueryParameter("version", "1.0.0")
                    .build();
            request = originRequest.newBuilder()
                    .url(httpUrl)
                    .build();
            return chain.proceed(request);
        }
    };
    return commonParams;
}
  • 緩存攔截器
    • 使用okhttp緩存的話,先要創(chuàng)建Cache,然后在創(chuàng)建緩存攔截器
OkHttpClient.Builder builder = new OkHttpClient.Builder();
//添加緩存攔截器
//創(chuàng)建Cache
File httpCacheDirectory = new File("OkHttpCache");
Cache cache = new Cache(httpCacheDirectory, 10 * 1024 * 1024);
builder.cache(cache);
//設(shè)置緩存
builder.addNetworkInterceptor(InterceptorUtils.getCacheInterceptor());
builder.addInterceptor(InterceptorUtils.getCacheInterceptor());
  • 緩存攔截器裆操, 緩存時間自己根據(jù)情況設(shè)定
/**
 * 在無網(wǎng)絡(luò)的情況下讀取緩存,有網(wǎng)絡(luò)的情況下根據(jù)緩存的過期時間重新請求
 * @return
 */
public static Interceptor getCacheInterceptor() {
    Interceptor commonParams = new Interceptor() {
        @Override
        public okhttp3.Response intercept(Chain chain) throws IOException {
            Request request = chain.request();
            if (!NetworkUtils.isConnected()) {
                //無網(wǎng)絡(luò)下強制使用緩存炉媒,無論緩存是否過期,此時該請求實際上不會被發(fā)送出去踪区。
                request = request.newBuilder()
                        .cacheControl(CacheControl.FORCE_CACHE)
                        .build();
            }
            Response response = chain.proceed(request);
            if (NetworkUtils.isConnected()) {
                //有網(wǎng)絡(luò)情況下,根據(jù)請求接口的設(shè)置吊骤,配置緩存缎岗。
                // 這樣在下次請求時,根據(jù)緩存決定是否真正發(fā)出請求白粉。
                String cacheControl = request.cacheControl().toString();
                //當然如果你想在有網(wǎng)絡(luò)的情況下都直接走網(wǎng)絡(luò)传泊,那么只需要
                //將其超時時間這是為0即可:String cacheControl="Cache-Control:public,max-age=0"
                int maxAge = 60 * 60;
                // read from cache for 1 minute
                return response.newBuilder()
                        .header("Cache-Control", cacheControl)
                        .header("Cache-Control", "public, max-age=" + maxAge)
                        .removeHeader("Pragma") .build();
            } else { //無網(wǎng)絡(luò)
                int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
                return response.newBuilder()
                        .header("Cache-Control", "public,only-if-cached,max-stale=360000")
                        .header("Cache-Control", "public,only-if-cached,max-stale=" + maxStale)
                        .removeHeader("Pragma") .build();
            }
        }
    };
    return commonParams;
}
  • 自定義CookieJar
/**
 * 自定義CookieJar
 * @param builder
 */
public static void addCookie(OkHttpClient.Builder builder){
    builder.cookieJar(new CookieJar() {
        private final HashMap<HttpUrl, List<Cookie>> cookieStore = new HashMap<>();
        @Override
        public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
            cookieStore.put(url, cookies);
            //保存cookie //也可以使用SP保存
        }

        @Override
        public List<Cookie> loadForRequest(HttpUrl url) {
            List<Cookie> cookies = cookieStore.get(url);
            //取出cookie
            return cookies != null ? cookies : new ArrayList<Cookie>();
        }
    });
}

7.踩坑經(jīng)驗

  • url被轉(zhuǎn)義
http://api.mydemo.com/api%2Fnews%2FnewsList?
罪魁禍首@Url與@Path注解,我們開發(fā)過程中,肯定會需要動態(tài)的修改請求地址
兩種動態(tài)修改方式如下:
@POST()
Call<HttpResult<News>> post(@Url String url, @QueryMap Map<String, String> map);
@POST("api/{url}/newsList")
Call<HttpResult<News>> login(@Path("url") String url, @Body News post);
第一種是直接使用@Url,它相當于直接替換了@POST()里面的請求地址
第二種是使用@Path("url"),它只替換了@POST("api/{url}/newsList")中的{url}
如果你用下面這樣寫的話,就會出現(xiàn)url被轉(zhuǎn)義
@POST("{url}")
Call<HttpResult<News>> post(@Path("url") String url);
你如果執(zhí)意要用@Path,也不是不可以,需要這樣寫
@POST("{url}")
Call<HttpResult<News>> post(@Path(value = "url", encoded = true) String url);

8.Form表單提交與multipart/form-data

8.1 form表單常用屬性

  • action:url 地址,服務器接收表單數(shù)據(jù)的地址
  • method:提交服務器的http方法蜗元,一般為post和get
  • name:最好好吃name屬性的唯一性
  • enctype: 表單數(shù)據(jù)提交時使用的編碼類型或渤,默認使用"pplication/x-www-form-urlencoded"系冗,如果是使用POST請求奕扣,則請求頭中的content-type指定值就是該值。如果表單中有上傳文件掌敬,編碼類型需要使用"multipart/form-data"惯豆,類型,才能完成傳遞文件數(shù)據(jù)奔害。

8.2 瀏覽器提交表單時楷兽,會執(zhí)行如下步驟

  • 識別出表單中表單元素的有效項,作為提交項
  • 構(gòu)建一個表單數(shù)據(jù)集
  • 根據(jù)form表單中的enctype屬性的值作為content-type對數(shù)據(jù)進行編碼
  • 根據(jù)form表單中的action屬性和method屬性向指定的地址發(fā)送數(shù)據(jù)

8.3 提交方式

  • get:表單數(shù)據(jù)會被encodeURIComponent后以參數(shù)的形式:name1=value1&name2=value2 附帶在url?后面华临,再發(fā)送給服務器芯杀,并在url中顯示出來。
  • post:content-type 默認"application/x-www-form-urlencoded"對表單數(shù)據(jù)進行編碼雅潭,數(shù)據(jù)以鍵值對在http請求體重發(fā)送給服務器揭厚;如果enctype 屬性為"multipart/form-data",則以消息的形式發(fā)送給服務器扶供。

8.4 POST請求

  • HTTP/1.1 協(xié)議規(guī)定的HTTP請求方法有OPTIONS筛圆、GET、HEAD椿浓、POST太援、PUT闽晦、DELETE、TRACE提岔、CONNECT 這幾種仙蛉。其中POST一般用于向服務器提交數(shù)據(jù)。
  • 大家知道唧垦,HTTP協(xié)議是以ASCII 碼傳輸捅儒,建立在TCP/IP協(xié)議之上的應用層規(guī)范。規(guī)范把HTTP請求分為3大塊:狀態(tài)行振亮、請求頭巧还、消息體。類似于如下:
<method> <request-URL> <version>
<headers>
<entity-body>
  • 協(xié)議規(guī)定POST提交的數(shù)據(jù)必須放在消息主題(entity-body)中坊秸,但協(xié)議并沒有規(guī)定數(shù)據(jù)必須使用什么編碼方式麸祷。實際上,開發(fā)者可以自己決定消息體的格式褒搔,只要后面發(fā)送的HTTP請求滿足上面的格式就可以了阶牍。
  • 但是,數(shù)據(jù)發(fā)送出去后星瘾,還要服務器解析成功才有意義走孽。一般服務器都內(nèi)置了自動解析常見數(shù)據(jù)格式的功能。服務端通常是根據(jù)請求頭(headers)中的Content-Type字段來獲知請求中的消息主體是用何種方式編碼琳状,再對主體進行解析磕瓷。所以說到POST提交數(shù)據(jù)方法,包含了Content-Type和消息主題編碼方式兩部分念逞。

8.5 enctype指定的content-type

  • application/x-www-form-urlencoded
  • application/json
  • text/xml
  • multipart/form-data

9.content-type介紹

9.1 application/x-www-form-urlencoded

  • 這應該是最常見的POST提交數(shù)據(jù)的方式了困食。瀏覽器的原生<form>表單,如果不設(shè)置enctype屬性翎承,那么最終會以application/x-www-form-urlencoded方法提交數(shù)據(jù)硕盹。請求類似于如下內(nèi)容(省略了部分無關(guān)的內(nèi)容):
    • Content-Type 被指定為 application/x-www-form-urlencoded。
    • 提交的數(shù)據(jù)按照key-value的格式叨咖,也就是key1=value1,key2=value2這種方式進行編碼瘩例,key和val都進行URL轉(zhuǎn)碼。大部分服務器都對這種方式支持甸各。
    POST http://www.hao123.com/ HTTP/1.1
    Content-Type: application/x-www-form-urlencoded;charset=utf-8
    title=test&sub%5B%5D=1&sub%5B%5D=2&sub%5B%5D=3
    

9.2 application/json

  • application/json 這個Content-Type作為響應頭大家肯定不陌生垛贤。事實上現(xiàn)在已經(jīng)基本都是都是這種方式了,來通知服務器消息體是序列化后的JSON字符串痴晦。由于JSON規(guī)范的流行南吮,除了低版本的IE之外的現(xiàn)在主流瀏覽器都原生支持JSON。當然服務器也有處理JSON的函數(shù)誊酌。
  • JSON格式支持比鍵值對更復雜的結(jié)構(gòu)化數(shù)據(jù)部凑,這樣點也很有用露乏,在需要提交數(shù)據(jù)層次非常深的數(shù)據(jù)時,用JSON序列化之后提交涂邀,非常方便瘟仿。
POST http://www.hao123.com/ HTTP/1.1
Content-Type: application/json;charset=utf-8
{"title":"test","sub":[1,2,3]}
  • 這種方案,可以很方便的提交復雜的結(jié)構(gòu)化的數(shù)據(jù)比勉,特別適合RESTful的接口劳较。而且各大抓包工具如chrome自帶的開發(fā)者工具,F(xiàn)irebug浩聋、Fidder观蜗,都會以樹形結(jié)構(gòu)展示JSON數(shù)據(jù),非常友好衣洁。

9.3 text/xml

  • 它是一種使用HTTP作為傳輸協(xié)議墓捻,XML作為編碼方式的遠程調(diào)用規(guī)范。典型的XML-RPC是這樣的:
    • XML-RPC 協(xié)議很簡單坊夫、功能夠用砖第,各種語言的實現(xiàn)都有。它的使用也很廣泛环凿,但是我還是比較傾向于JSON梧兼,因為相比于JSON,XML太過于臃腫智听。
    POST http://www.example.com HTTP/1.1
    Content-Type: text/xml
    <?xml version="1.0"?>
    <methodCall>
        <methodName>examples.getStateName</methodName>
        <params>
            <param>
                <value><i4>41</i4></value>
            </param>
        </params>
    </methodCall>
    

9.4 multipart/form-data

  • 在最初的http協(xié)議中羽杰,沒有定義上傳文件的Method, 為了實現(xiàn)這個功能,http協(xié)議組改造了post請求瞭稼,添加一種post規(guī)范忽洛,設(shè)定這種規(guī)范的Content-Type為multipart/form-data;boundary={bound}腻惠,其中{bound}是定義分割符环肘,用于分割各項內(nèi)容(文件,key-value對)集灌,不然服務器無法正確識別各項內(nèi)容悔雹。post body里需要用到,盡量保證隨機唯一欣喧。
  • 這又是一個常見的POST數(shù)據(jù)提交的方式腌零。我們使用表單上傳文件時,必須讓form表單enctype等于multipart/form-data唆阿。
<form action="/upload" enctype="multipart/form-data" method="post">
    Username: <input type="text" name="username">
    Password: <input type="password" name="password">
    File: <input type="file" name="file">
    <input type="submit">
</form>
  • 案例如下所示
    • 這個例子稍微復雜點益涧。首先生成了一個boundary用于分割不同的字段,為了避免與正文內(nèi)容重復驯鳖,boundary很長很復雜闲询。然后Content-Type里指明了數(shù)據(jù)以multipart/form-data來編碼久免,本次請求的boundary是什么內(nèi)容。消息主體里按照字段個數(shù)又分為多個結(jié)構(gòu)類型的部分扭弧,每個部分都以---boundary開始阎姥,緊接著是內(nèi)容描述信息,然后是回車鸽捻,然后是字段的具體內(nèi)容(文本和二進制)呼巴。如果傳輸?shù)氖俏募€要包含文件名和文件類型信息御蒲。消息主體最后以----boundary----標志結(jié)束衣赶。
    header
    Content-Type: multipart/form-data; boundary={boundary}\r\n
    
    body
    普通 input 數(shù)據(jù)
    --{boundary}\r\n
    Content-Disposition: form-data; name="username"\r\n
    \r\n
    Tom\r\n
    
    文件上傳 input 數(shù)據(jù)
    --{boundary}\r\n
    Content-Disposition: form-data; name="file"; filename="myfile.txt"\r\n
    Content-Type: text/plain\r\n
    Content-Transfer-Encoding: binary\r\n
    \r\n
    hello word\r\n
    
    結(jié)束標志
    --{boundary}--\r\n
    
    數(shù)據(jù)示例
    POST /upload HTTP/1.1
    Host: 172.16.100.128:5000
    Content-Length: 394
    Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryLumpDpF3AwbRwRBn
    Referer: http://172.16.100.128:5000/
    
    ------WebKitFormBoundaryUNZIuug9PIVmZWuw
    Content-Disposition: form-data; name="username"
    
    Tom
    ------WebKitFormBoundaryUNZIuug9PIVmZWuw
    Content-Disposition: form-data; name="password"
    
    passwd
    ------WebKitFormBoundaryUNZIuug9PIVmZWuw
    Content-Disposition: form-data; name="file"; filename="myfile.txt"
    Content-Type: text/plain
    
    hello world
    ------WebKitFormBoundaryUNZIuug9PIVmZWuw--
    

關(guān)于其他

參考博客

版本更新說明

  • V1.0.1 更新2017年3月18日
  • V1.0.2 更新2017年5月21日
  • V1.0.3 更新2017年10月12日
  • V2.0.0 更新2018年2月23日
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市厚满,隨后出現(xiàn)的幾起案子屑埋,更是在濱河造成了極大的恐慌,老刑警劉巖痰滋,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件摘能,死亡現(xiàn)場離奇詭異,居然都是意外死亡敲街,警方通過查閱死者的電腦和手機团搞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來多艇,“玉大人逻恐,你說我怎么就攤上這事【颍” “怎么了复隆?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長姆涩。 經(jīng)常有香客問我挽拂,道長,這世上最難降的妖魔是什么骨饿? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任亏栈,我火速辦了婚禮,結(jié)果婚禮上宏赘,老公的妹妹穿的比我還像新娘绒北。我一直安慰自己,他們只是感情好察署,可當我...
    茶點故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布闷游。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪脐往。 梳的紋絲不亂的頭發(fā)上俱济,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天,我揣著相機與錄音钙勃,去河邊找鬼蛛碌。 笑死,一個胖子當著我的面吹牛辖源,可吹牛的內(nèi)容都是我干的蔚携。 我是一名探鬼主播,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼克饶,長吁一口氣:“原來是場噩夢啊……” “哼酝蜒!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起矾湃,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤亡脑,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后邀跃,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體霉咨,經(jīng)...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年拍屑,在試婚紗的時候發(fā)現(xiàn)自己被綠了途戒。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,926評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡僵驰,死狀恐怖喷斋,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蒜茴,我是刑警寧澤星爪,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站粉私,受9級特大地震影響顽腾,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜毡鉴,卻給世界環(huán)境...
    茶點故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一崔泵、第九天 我趴在偏房一處隱蔽的房頂上張望秒赤。 院中可真熱鬧猪瞬,春花似錦、人聲如沸入篮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽潮售。三九已至痊项,卻和暖如春锅风,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背鞍泉。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工皱埠, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人咖驮。 一個月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓边器,卻偏偏與公主長得像,于是被迫代替她去往敵國和親托修。 傳聞我的和親對象是個殘疾皇子忘巧,可洞房花燭夜當晚...
    茶點故事閱讀 44,871評論 2 354

推薦閱讀更多精彩內(nèi)容