36. OkHttp之-攔截器-RetryAndFollowUpInterceptor

分發器的邏輯執行完成就會進入攔截器了,OkHttp使用了攔截器模式來處理一個請求從發起到響應的過程。

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

    @Override
    public Response execute() throws IOException {
        ...
        try {
            ...
            // 發起請求
            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()的區別僅在於添加的順序)
            //但是不同的順序也會產生不同的效果,具體可參考下
            //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);
    }

可以看到OkHttp內部默認存在五大攔截器,而今天這篇要講的就是retryAndFollowUpInterceptor,這個攔截器在RealCall被new出來時已經創建了,從他的名字就可以看出來,他負責的是失敗重試和重定向的邏輯處理。

失敗重試

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

    @Override
    public Response intercept(Chain chain) throws IOException {
        ...
        while (true) {
            ...
            try {
                //請求出現了異常,那麼releaseConnection依舊爲true。
                response = realChain.proceed(request, streamAllocation, null, null);
                releaseConnection = false;
            } catch (RouteException e) {
                //路由異常,連接未成功,請求還沒發出去
                if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
                    throw e.getLastConnectException();
                }
                releaseConnection = false;
                continue;
            } catch (IOException e) {
                //請求發出去了,但是和服務器通信失敗了。(socket流正在讀寫數據的時候斷開連接)
                // 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是路由異常,連接未成功,請求還沒發出去,所以recover方法中第三個參數直接傳的false,表示請求還沒有開始;而IOException是請求發出去了,但是和服務器通信失敗了,所以所以recover方法中第三個參數值取決於

boolean requestSendStarted = !(e instanceof ConnectionShutdownException);

HTTP2纔會拋出ConnectionShutdownException。所以對於HTTP1 requestSendStarted一定是true。

從上面的代碼可以看出,realChain.proceed是後續的責任鏈執行的邏輯,如果這些執行發生了異常,在RetryAndFollowUpInterceptor會被捕獲,然後通過recover方法判斷當前異常是否滿足重試的條件(並不是所有失敗都會被重試),如果滿足,則continue,再進行一次,這個操作是在while循環中進行的,也就是隻要滿足重試的條件,可以進行無數次的重試,但事實上,由於重試的條件比較苛刻,一般也不會被多次重試。那麼這個重試的條件究竟有哪些呢?

重試條件

進入recover方法

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

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

        //RouteException不用判斷這個條件,
        //當是IOException時,由於requestSendStarted只在http2的io異常中可能爲false,所以主要是第二個條件,body是UnrepeatableRequestBody則不必重試
        if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody)
            return false;

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

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

進入isRecoverable方法

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

        // 請求超時導致的中斷,可以重試
        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;
            }
        }
        //證書校驗失敗 不匹配
        if (e instanceof SSLPeerUnverifiedException) {
            // e.g. a certificate pinning error.
            return false;
        }
        return true;
    }

總結一下:
1、協議異常,如果是那麼直接判定不能重試;(你的請求或者服務器的響應本身就存在問題,沒有按照http協議來 定義數據,再重試也沒用)
2、超時異常,可能由於網絡波動造成了Socket連接的超時,可以使用不同路線重試。
3、SSL證書異常/SSL驗證失敗異常,前者是證書驗證失敗,後者可能就是壓根就沒證書

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

重定向

OkHttp支持重定向請求,見followUpRequest方法,主要是對響應頭的一些判斷

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

        final String method = userResponse.request().method();
        switch (responseCode) {
            //407 客戶端使用了HTTP代理服務器,在請求頭中添加 “Proxy-Authorization”,讓代理服務器授權 @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 需要身份驗證 有些服務器接口需要驗證使用者身份 在請求頭中添加 “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:
                // 如果設置了不允許重定向,那就返回null
                if (!client.followRedirects()) return null;
                // 從響應頭取出location
                String location = userResponse.header("Location");
                if (location == null) return null;
                // 根據location 配置新的請求
                HttpUrl url = userResponse.request().url().resolve(location);

                // 如果爲null,說明協議有問題,取不出來HttpUrl,那就返回null,不進行重定向
                if (url == null) return null;

                // 如果重定向在http到https之間切換,需要檢查用戶是不是允許(默認允許)
                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 的請求(不包含請求體的請求),把請求頭中關於請求體的數據刪掉
                    if (!maintainBody) {
                        requestBuilder.removeHeader("Transfer-Encoding");
                        requestBuilder.removeHeader("Content-Length");
                        requestBuilder.removeHeader("Content-Type");
                    }
                }

                // 在跨主機重定向時,刪除身份驗證請求頭
                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;
                }
                // 如果是本身這次的響應就是重新請求的產物同時上一次之所以重請求還是因爲408,那我們這次不再重請求 了
                if (userResponse.priorResponse() != null
                        && userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {
                    // We attempted to retry and got another timeout. Give up.
                    return null;
                }
                // 如果服務器告訴我們了 Retry-After 多久後重試,那框架不管了。
                if (retryAfter(userResponse, 0) > 0) {
                    return null;
                }

                return userResponse.request();
            // 503 服務不可用 和408差不多,但是隻在服務器告訴你 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構建的時候可以設置,對應HTTP_PROXY_AUTH響應頭

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

@b:在OkHttpClient Builder構建的時候可以設置,對應HTTP_UNAUTHORIZED響應頭

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

整個是否需要重定向的判斷內容很多,關鍵在於理解他們的意思。如果此方法返回空,那就表 示不需要再重定向了,直接返回響應;但是如果返回非空,那就要重新請求返回的 Request ,但是需要注意的是, 我們的 followup 在攔截器中定義的最大次數爲20次。

總結

RetryAndFollowUpInterceptor攔截器是整個責任鏈中的第一個,這意味着它會是首次接觸到 Request 與最後接收到 Response 的角色,在這個 攔截器中主要功能就是判斷是否需要重試與重定向。
重試的前提是出現了 RouteException 或者 IOException 。一但在後續的攔截器執行過程中出現這兩個異常,就會 通過 recover 方法進行判斷是否進行連接重試。
重定向發生在重試的判定之後,如果不滿足重試的條件,還需要進一步調用 followUpRequest 根據 Response 的響 應碼(當然,如果直接請求失敗, Response 都不存在就會拋出異常)。 followup 最大發生20次。

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