36. OkHttp之-攔截器-RetryAndFollowUpInterceptor

分發(fā)器的邏輯執(zhí)行完成就會進(jìn)入攔截器了天揖,OkHttp使用了攔截器模式來處理一個請求從發(fā)起到響應(yīng)的過程烦粒。

代碼還是從我們上一篇提到的getResponseWithInterceptorChain開始

    @Override
    public Response execute() throws IOException {
        ...
        try {
            ...
            // 發(fā)起請求
            Response result = getResponseWithInterceptorChain();
            ...
            return result;
        } catch (IOException e) {
            
        } finally {
            
        }
    }
    Response getResponseWithInterceptorChain() throws IOException {
        // Build a full stack of interceptors.
        List<Interceptor> interceptors = new ArrayList<>();
        //自定義攔截器加入到集合
        interceptors.addAll(client.interceptors()); 
        interceptors.add(retryAndFollowUpInterceptor);
        interceptors.add(new BridgeInterceptor(client.cookieJar()));
        interceptors.add(new CacheInterceptor(client.internalCache()));
        interceptors.add(new ConnectInterceptor(client));
        if (!forWebSocket) {
            //自定義攔截器加入到集合,(和上邊client.interceptors()的區(qū)別僅在于添加的順序)
            //但是不同的順序也會產(chǎn)生不同的效果承匣,具體可參考下
            //https://segmentfault.com/a/1190000013164260
            interceptors.addAll(client.networkInterceptors());
        }
        interceptors.add(new CallServerInterceptor(forWebSocket));

        Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
                originalRequest, this, eventListener, client.connectTimeoutMillis(),
                client.readTimeoutMillis(), client.writeTimeoutMillis());

        return chain.proceed(originalRequest);
    }
addInterceptor 與 addNetworkInterceptor 的區(qū)別婿着?

1.添加順序不同
應(yīng)用攔截器是最先執(zhí)行的攔截器溶耘,也就是用戶自己設(shè)置request屬性后的原始請求通惫,第一個被添加的,而網(wǎng)絡(luò)攔截器位于ConnectInterceptor和CallServerInterceptor之間驰吓,此時網(wǎng)絡(luò)鏈路已經(jīng)準(zhǔn)備好涧尿,只等待發(fā)送請求數(shù)據(jù)
2.可能執(zhí)行的次數(shù)不同
網(wǎng)絡(luò)攔截器(addNetworkInterceptor)可能執(zhí)行多次(如果發(fā)生了錯誤重試或者網(wǎng)絡(luò)重定向),也可能一次都沒有執(zhí)行(如果在CacheInterceptor中命中了緩存)檬贰;而addInterceptor只會執(zhí)行一次姑廉,因?yàn)樗窃谡埱蟀l(fā)起之前最先執(zhí)行的(在RetryAndFollowUpInterceptor之前)
3.應(yīng)用場景不同
應(yīng)用攔截器因?yàn)橹徽{(diào)用一次,可用于統(tǒng)計(jì)客戶端發(fā)起的次數(shù)翁涤,而網(wǎng)絡(luò)攔截器一次調(diào)用代表了一定會發(fā)起一次網(wǎng)絡(luò)通信桥言,因此通常可用于統(tǒng)計(jì)網(wǎng)絡(luò)鏈路上傳輸?shù)臄?shù)據(jù)葵礼。

可以看到OkHttp內(nèi)部默認(rèn)存在五大攔截器号阿,而今天這篇要講的就是retryAndFollowUpInterceptor,這個攔截器在RealCall被new出來時已經(jīng)創(chuàng)建了鸳粉,從他的名字就可以看出來扔涧,他負(fù)責(zé)的是失敗重試和重定向的邏輯處理。

失敗重試

從這個攔截器的intercept方法中可以看出,雖然這個攔截器是第一個被執(zhí)行的枯夜,但是其實(shí)他真正的重試和重定向操作是在請求被響應(yīng)之后才做的處理.

    @Override
    public Response intercept(Chain chain) throws IOException {
        ...
        while (true) {
            ...
            try {
                //請求出現(xiàn)了異常弯汰,那么releaseConnection依舊為true。
                response = realChain.proceed(request, streamAllocation, null, null);
                releaseConnection = false;
            } catch (RouteException e) {
                //路由異常卤档,連接未成功蝙泼,請求還沒發(fā)出去
                if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
                    throw e.getLastConnectException();
                }
                releaseConnection = false;
                continue;
            } catch (IOException e) {
                //請求發(fā)出去了,但是和服務(wù)器通信失敗了劝枣。(socket流正在讀寫數(shù)據(jù)的時候斷開連接)
                // HTTP2才會拋出ConnectionShutdownException。所以對于HTTP1 requestSendStarted一定是true
                boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
                if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
                releaseConnection = false;
                continue;
            } finally {
                ...
            }
            ...
        }
    }

可以看到被處理的exception只有RouteException和IOException织鲸,RouteException是路由異常舔腾,連接未成功,請求還沒發(fā)出去搂擦,所以recover方法中第三個參數(shù)直接傳的false稳诚,表示請求還沒有開始;而IOException是請求發(fā)出去了瀑踢,但是和服務(wù)器通信失敗了扳还,所以所以recover方法中第三個參數(shù)值取決于

boolean requestSendStarted = !(e instanceof ConnectionShutdownException);

HTTP2才會拋出ConnectionShutdownException。所以對于HTTP1 requestSendStarted一定是true橱夭。

從上面的代碼可以看出氨距,realChain.proceed是后續(xù)的責(zé)任鏈執(zhí)行的邏輯,如果這些執(zhí)行發(fā)生了異常棘劣,在RetryAndFollowUpInterceptor會被捕獲俏让,然后通過recover方法判斷當(dāng)前異常是否滿足重試的條件(并不是所有失敗都會被重試),如果滿足茬暇,則continue首昔,再進(jìn)行一次,這個操作是在while循環(huán)中進(jìn)行的糙俗,也就是只要滿足重試的條件勒奇,可以進(jìn)行無數(shù)次的重試,但事實(shí)上巧骚,由于重試的條件比較苛刻赊颠,一般也不會被多次重試。那么這個重試的條件究竟有哪些呢网缝?

重試條件

進(jìn)入recover方法

    private boolean recover(IOException e, StreamAllocation streamAllocation,
                            boolean requestSendStarted, Request userRequest) {
        streamAllocation.streamFailed(e);

        //調(diào)用方在OkhttpClient初始化時設(shè)置了不允許重試(默認(rèn)允許)
        if (!client.retryOnConnectionFailure()) return false;

        //RouteException不用判斷這個條件巨税,
        //當(dāng)是IOException時,由于requestSendStarted只在http2的io異常中可能為false粉臊,所以主要是第二個條件草添,body是UnrepeatableRequestBody則不必重試
        if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody)
            return false;

        //對異常類型進(jìn)行判斷
        if (!isRecoverable(e, requestSendStarted)) return false;

        //不存在更多的路由也沒辦法重試
        if (!streamAllocation.hasMoreRoutes()) return false;
        //以上條件都允許了,才能重試
        return true;
    }

進(jìn)入isRecoverable方法

    private boolean isRecoverable(IOException e, boolean requestSendStarted) {
        // 協(xié)議異常扼仲,那么重試幾次都是一樣的
        if (e instanceof ProtocolException) {
            return false;
        }

        // 請求超時導(dǎo)致的中斷远寸,可以重試
        if (e instanceof InterruptedIOException) {
            return e instanceof SocketTimeoutException && !requestSendStarted;
        }
        //證書不正確  可能證書格式損壞 有問題
        if (e instanceof SSLHandshakeException) {
            // If the problem was a CertificateException from the X509TrustManager,
            // do not retry.
            if (e.getCause() instanceof CertificateException) {
                return false;
            }
        }
        //證書校驗(yàn)失敗 不匹配
        if (e instanceof SSLPeerUnverifiedException) {
            // e.g. a certificate pinning error.
            return false;
        }
        return true;
    }

總結(jié)一下:
1抄淑、協(xié)議異常,如果是那么直接判定不能重試;(你的請求或者服務(wù)器的響應(yīng)本身就存在問題驰后,沒有按照http協(xié)議來 定義數(shù)據(jù)肆资,再重試也沒用)
2、超時異常灶芝,可能由于網(wǎng)絡(luò)波動造成了Socket連接的超時郑原,可以使用不同路線重試。
3夜涕、SSL證書異常/SSL驗(yàn)證失敗異常犯犁,前者是證書驗(yàn)證失敗,后者可能就是壓根就沒證書

所以說要滿足重試的條件還是比較苛刻的女器。

重定向

OkHttp支持重定向請求酸役,見followUpRequest方法,主要是對響應(yīng)頭的一些判斷

    private Request followUpRequest(Response userResponse, Route route) throws IOException {
        //重定向的判斷必須在服務(wù)器有返回的情況下驾胆,否則拋出異常
        if (userResponse == null) throw new IllegalStateException();
        int responseCode = userResponse.code();

        final String method = userResponse.request().method();
        switch (responseCode) {
            //407 客戶端使用了HTTP代理服務(wù)器涣澡,在請求頭中添加 “Proxy-Authorization”,讓代理服務(wù)器授權(quán) @a
            case HTTP_PROXY_AUTH:
                Proxy selectedProxy = route != null
                        ? route.proxy()
                        : client.proxy();
                if (selectedProxy.type() != Proxy.Type.HTTP) {
                    throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not " +
                            "using proxy");
                }
                return client.proxyAuthenticator().authenticate(route, userResponse);
             //401 需要身份驗(yàn)證 有些服務(wù)器接口需要驗(yàn)證使用者身份 在請求頭中添加 “Authorization” @b
            case HTTP_UNAUTHORIZED:
                return client.authenticator().authenticate(route, userResponse);
            // 308 永久重定向
            // 307 臨時重定向
            case HTTP_PERM_REDIRECT:
            case HTTP_TEMP_REDIRECT:
                // 如果請求方式不是GET或者HEAD丧诺,框架不會自動重定向請求
                if (!method.equals("GET") && !method.equals("HEAD")) {
                    return null;
                }
            // 300 301 302 303
            case HTTP_MULT_CHOICE:
            case HTTP_MOVED_PERM:
            case HTTP_MOVED_TEMP:
            case HTTP_SEE_OTHER:
                // 如果設(shè)置了不允許重定向入桂,那就返回null
                if (!client.followRedirects()) return null;
                // 從響應(yīng)頭取出location
                String location = userResponse.header("Location");
                if (location == null) return null;
                // 根據(jù)location 配置新的請求
                HttpUrl url = userResponse.request().url().resolve(location);

                // 如果為null,說明協(xié)議有問題锅必,取不出來HttpUrl事格,那就返回null,不進(jìn)行重定向
                if (url == null) return null;

                // 如果重定向在http到https之間切換搞隐,需要檢查用戶是不是允許(默認(rèn)允許)
                boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
                if (!sameScheme && !client.followSslRedirects()) return null;

                // Most redirects don't include a request body.
                Request.Builder requestBuilder = userResponse.request().newBuilder();

                //重定向請求中 只要不是 PROPFIND 請求驹愚,無論是POST還是其他的方       
                //法都要改為GET請求方式, * 即只有 PROPFIND 請求才能有請求體
                //請求不是get與head HttpMethod.permitsRequestBody ===> return !(method.equals("GET") || method.equals("HEAD"));
                
                //HttpMethod.permitsRequestBody ===> return method.equals("PROPFIND"); 
                //HttpMethod.permitsRequestBody ===> return !method.equals("PROPFIND");
                if (HttpMethod.permitsRequestBody(method)) {
                    final boolean maintainBody = HttpMethod.redirectsWithBody(method);
                    // 除了 PROPFIND 請求之外都改成GET請求
                    //HttpMethod.redirectsToGet ===> return !method.equals("PROPFIND");
                    if (HttpMethod.redirectsToGet(method)) {
                        requestBuilder.method("GET", null);
                    } else {
                        RequestBody requestBody = maintainBody ? userResponse.request().body() :
                                null;
                        requestBuilder.method(method, requestBody);
                    }
                    // 不是 PROPFIND 的請求(不包含請求體的請求)劣纲,把請求頭中關(guān)于請求體的數(shù)據(jù)刪掉
                    if (!maintainBody) {
                        requestBuilder.removeHeader("Transfer-Encoding");
                        requestBuilder.removeHeader("Content-Length");
                        requestBuilder.removeHeader("Content-Type");
                    }
                }

                // 在跨主機(jī)重定向時逢捺,刪除身份驗(yàn)證請求頭
                if (!sameConnection(userResponse, url)) {
                    requestBuilder.removeHeader("Authorization");
                }

                return requestBuilder.url(url).build();
            // 408 客戶端請求超時
            case HTTP_CLIENT_TIMEOUT:
                // 408 算是連接失敗了,所以判斷用戶是不是允許重試
                if (!client.retryOnConnectionFailure()) {
                    // The application layer has directed us not to retry the request.
                    return null;
                }

                if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
                    return null;
                }
                // 如果是本身這次的響應(yīng)就是重新請求的產(chǎn)物同時上一次之所以重請求還是因?yàn)?08癞季,那我們這次不再重請求 了
                if (userResponse.priorResponse() != null
                        && userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {
                    // We attempted to retry and got another timeout. Give up.
                    return null;
                }
                // 如果服務(wù)器告訴我們了 Retry-After 多久后重試劫瞳,那框架不管了。
                if (retryAfter(userResponse, 0) > 0) {
                    return null;
                }

                return userResponse.request();
            // 503 服務(wù)不可用 和408差不多绷柒,但是只在服務(wù)器告訴你 Retry-After:0(意思就是立即重試) 才重請求
            case HTTP_UNAVAILABLE:
                if (userResponse.priorResponse() != null
                        && userResponse.priorResponse().code() == HTTP_UNAVAILABLE) {
                    // We attempted to retry and got another timeout. Give up.
                    return null;
                }

                if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
                    // specifically received an instruction to retry without delay
                    return userResponse.request();
                }

                return null;

            default:
                return null;
        }
    }

@a:在OkHttpClient Builder構(gòu)建的時候可以設(shè)置志于,對應(yīng)HTTP_PROXY_AUTH響應(yīng)頭

    public Builder proxyAuthenticator(Authenticator proxyAuthenticator) {
            if (proxyAuthenticator == null)
                throw new NullPointerException("proxyAuthenticator == null");
            this.proxyAuthenticator = proxyAuthenticator;
            return this;
        }

@b:在OkHttpClient Builder構(gòu)建的時候可以設(shè)置,對應(yīng)HTTP_UNAUTHORIZED響應(yīng)頭

    public Builder authenticator(Authenticator authenticator) {
            if (authenticator == null) throw new NullPointerException("authenticator == null");
            this.authenticator = authenticator;
            return this;
        }

整個是否需要重定向的判斷內(nèi)容很多废睦,關(guān)鍵在于理解他們的意思伺绽。如果此方法返回空,那就表 示不需要再重定向了,直接返回響應(yīng);但是如果返回非空奈应,那就要重新請求返回的 Request 澜掩,但是需要注意的是, 我們的 followup 在攔截器中定義的最大次數(shù)為20次杖挣。

總結(jié)

RetryAndFollowUpInterceptor攔截器是整個責(zé)任鏈中的第一個肩榕,這意味著它會是首次接觸到 Request 與最后接收到 Response 的角色,在這個 攔截器中主要功能就是判斷是否需要重試與重定向惩妇。
重試的前提是出現(xiàn)了 RouteException 或者 IOException 株汉。一但在后續(xù)的攔截器執(zhí)行過程中出現(xiàn)這兩個異常,就會 通過 recover 方法進(jìn)行判斷是否進(jìn)行連接重試屿附。
重定向發(fā)生在重試的判定之后郎逃,如果不滿足重試的條件,還需要進(jìn)一步調(diào)用 followUpRequest 根據(jù) Response 的響 應(yīng)碼(當(dāng)然挺份,如果直接請求失敗, Response 都不存在就會拋出異常)贮懈。 followup 最大發(fā)生20次匀泊。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市朵你,隨后出現(xiàn)的幾起案子各聘,更是在濱河造成了極大的恐慌,老刑警劉巖抡医,帶你破解...
    沈念sama閱讀 219,366評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件躲因,死亡現(xiàn)場離奇詭異,居然都是意外死亡忌傻,警方通過查閱死者的電腦和手機(jī)大脉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來水孩,“玉大人镰矿,你說我怎么就攤上這事》郑” “怎么了秤标?”我有些...
    開封第一講書人閱讀 165,689評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長宙刘。 經(jīng)常有香客問我苍姜,道長,這世上最難降的妖魔是什么悬包? 我笑而不...
    開封第一講書人閱讀 58,925評論 1 295
  • 正文 為了忘掉前任衙猪,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘屈嗤。我一直安慰自己潘拨,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評論 6 392
  • 文/花漫 我一把揭開白布饶号。 她就那樣靜靜地躺著铁追,像睡著了一般。 火紅的嫁衣襯著肌膚如雪茫船。 梳的紋絲不亂的頭發(fā)上琅束,一...
    開封第一講書人閱讀 51,727評論 1 305
  • 那天,我揣著相機(jī)與錄音算谈,去河邊找鬼涩禀。 笑死,一個胖子當(dāng)著我的面吹牛然眼,可吹牛的內(nèi)容都是我干的艾船。 我是一名探鬼主播,決...
    沈念sama閱讀 40,447評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼高每,長吁一口氣:“原來是場噩夢啊……” “哼屿岂!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起鲸匿,我...
    開封第一講書人閱讀 39,349評論 0 276
  • 序言:老撾萬榮一對情侶失蹤爷怀,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后带欢,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體运授,經(jīng)...
    沈念sama閱讀 45,820評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評論 3 337
  • 正文 我和宋清朗相戀三年乔煞,在試婚紗的時候發(fā)現(xiàn)自己被綠了吁朦。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,127評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡瘤缩,死狀恐怖喇完,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情剥啤,我是刑警寧澤锦溪,帶...
    沈念sama閱讀 35,812評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站府怯,受9級特大地震影響刻诊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜牺丙,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評論 3 331
  • 文/蒙蒙 一则涯、第九天 我趴在偏房一處隱蔽的房頂上張望复局。 院中可真熱鬧,春花似錦粟判、人聲如沸亿昏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽角钩。三九已至,卻和暖如春呻澜,著一層夾襖步出監(jiān)牢的瞬間递礼,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評論 1 272
  • 我被黑心中介騙來泰國打工羹幸, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留脊髓,地道東北人。 一個月前我還...
    沈念sama閱讀 48,388評論 3 373
  • 正文 我出身青樓栅受,卻偏偏與公主長得像将硝,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子屏镊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評論 2 355

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