OkHttp 4源码(2)— 拦截器机制分析

本文基于OkHttp 4.3.1源码分析
OkHttp - 官方地址
OkHttp - GitHub代码地址

概述

OkHttp整体流程(本文覆盖红色部分)

Interceptor流程

责任链模式

概念
责任链模式(Chain of Responsibility Pattern),包含命令对象和一系列实现了相同接口的处理对象,这些处理对象相互连接成为一条责任链。每一个处理对象能决定它能处理哪些命令对象,对于它不能处理的命令对象,将会传递给该链中的下一个处理对象。负责链模式就是对这种顺序处理事件的行为的抽象,通过接口来定义处理事件的方法,顺序分发/处理事件。

主要解决
职责链上的处理者负责处理请求,客户只需要将请求发送到职责链上即可,无须关心请求的处理细节和请求的传递,所以职责链将请求的发送者和请求的处理者解耦了。

优点

  1. 降低耦合度。它将请求的发送者和接收者解耦。
  2. 简化了对象。使得对象不需要知道链的结构。
  3. 增强给对象指派职责的灵活性。通过改变链内的成员或者调动它们的次序,允许动态地新增或者删除责任。
  4. 增加新的请求处理类很方便。

特点

  1. 每个责任人实现相同的接口,处理一个事件对象
  2. 让事件对象责任人之间顺序传递
  3. 事件的处理结果的返回是逆序的
  4. 责任链中的每个责任人都可以有权不继续传递事件,以自身为终点处理事件返回结果

OkHttp中应用
在 OkHttp 中,命令对象就是 Request 对象,处理对象就是每一个 Interceptor 对象。每个 interceptor 对 request 进行一些步骤的处理,而将其余的工作交给下一个 interceptor。另外,责任链中的处理对象如果可以全权处理命令对象,则不需要交给下一个处理对象。OkHttp 中的 CacheInterceptor 也是具有全权处理的能力。如果请求的结果已经缓存,则不需要再交给 ConnectInterceptor 等进行连接服务器、发送请求的处理,直接返回已缓存的 response 即可。

源码分析

Interceptor

拦截器,即处理对象类,Chain和Interceptor共同实现顺序调用,最终反响依次返回Response
拦截器一般可以概括三个步骤

  1. 请求前处理逻辑
  2. 执行 chain.proceed,顺序调用
  3. 获取响应结果后逻辑处理

默认拦截器依次包括

  1. RetryAndFollowUpInterceptor (重试机制)
  2. BridgeInterceptor (Http连接转换桥)
  3. CacheInterceptor (缓存策略)
  4. ConnectInterceptor (连接)
  5. CallServerInterceptor (响应读取)
// 拦截器,处理对象
interface Interceptor {
  // 拦截方法,接受拦截器链(包括命令对象Request及Call、和一系列辅助对象),返回处理结果
  fun intercept(chain: Chain): Response

  // 链接口
  interface Chain {
    // 核心方法, 顺序执行 处理方法
    fun proceed(request: Request): Response
    ... 
  }
}

RealInterceptorChain 拦截器链

完成责任链模式核心类,拦截器顺序执行、反向返回及中途中断核心实现
前一篇文章知道,命令对象RealCall,执行RealInterceptorChain的proceed方法,该方法最后会按照拦截器的加入顺序,循环构建Chain对象,然后执行拦截器的拦截方法,执导最后一个拦截器或者是中途拦截器直接返回了。

class RealInterceptorChain(
  private val interceptors: List<Interceptor>, // 拦截器,处理对象
  private val transmitter: Transmitter, // 发射机
  private val exchange: Exchange?, // 交换器 请求和响应 
  private val index: Int,
  private val request: Request, // 请求
  private val call: Call,  // 一次请求和返回封装
  private val connectTimeout: Int,
  private val readTimeout: Int,
  private val writeTimeout: Int
) : Interceptor.Chain {
  ...

  override fun proceed(request: Request): Response {
    return proceed(request, transmitter, exchange)
  }

  // 顺序调用方法 
  fun proceed(request: Request, transmitter: Transmitter, exchange: Exchange?): Response {
    ...
    calls++
    ...
    // 构造一个 RealInterceptorChain
    val next = RealInterceptorChain(interceptors, transmitter, exchange,
        index + 1, request, call, connectTimeout, readTimeout, writeTimeout)
    // 根据index 依次取拦截器
    val interceptor = interceptors[index]

    // 所有拦截器顺序执行,拦截器方法中执行 chain.proceed完成reponse返回,将命令对象交给下一个拦截器进行处理,如果不执行chain.proceed则在该拦截器进行终结。
    val response = interceptor.intercept(next) ?: throw NullPointerException(
        "interceptor $interceptor returned null")
    ...
    return response
  }

}

RetryAndFollowUpInterceptor

重试拦截器

RetryAndFollowUpInterceptor.intercept

  • 请求连接准备工作
  • 执行chain.proceed顺序执行,获取response
  • 若出现异常,则判断是否需要进行重试机制
  • 根据服务端返回的状态码,进行重定向请求生成
  • 进行重试,直到不需要重试或者成功,最多20次
  @Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {
    var request = chain.request()
    val realChain = chain as RealInterceptorChain
    val transmitter = realChain.transmitter()
    var followUpCount = 0
    var priorResponse: Response? = null
    while (true) { // 循环
      // 发射机 执行连接准备工作
      transmitter.prepareToConnect(request)
    
      if (transmitter.isCanceled) { // 处理取消的请求
        throw IOException("Canceled")
      }

      var response: Response
      var success = false
      try {
        // 执行 proceed顺序执行流程,最终获取response
        response = realChain.proceed(request, transmitter, null)
        success = true
      } catch (e: RouteException) {
        // 若不满足重试条件,则抛出异常
        if (!recover(e.lastConnectException, transmitter, false, request)) {
          throw e.firstConnectException
        }
        continue    // 满足,则重试
      } catch (e: IOException) {
        // 若不满足重试条件,则抛出异常
        val requestSendStarted = e !is ConnectionShutdownException
        if (!recover(e, transmitter, requestSendStarted, request)) throw e
        continue    // 满足,则重试
      } finally {
        // 异常抛出,则success=false,执行释放资源
        if (!success) {
          transmitter.exchangeDoneDueToException()
        }
      }

      if (priorResponse != null) { // 赋值上一次的response
        response = response.newBuilder()
            .priorResponse(priorResponse.newBuilder()
                .body(null)
                .build())
            .build()
      }

      val exchange = response.exchange
      val route = exchange?.connection()?.route()
      // 获取进一步重试的请求
      val followUp = followUpRequest(response, route)

      if (followUp == null) {
        if (exchange != null && exchange.isDuplex) {
          transmitter.timeoutEarlyExit() // 无重试,停止timeout并退出
        }
        return response
      }

      val followUpBody = followUp.body
      // 如果期望执行一次,则结束
      if (followUpBody != null && followUpBody.isOneShot()) { 
        return response
      }

      response.body?.closeQuietly()
      if (transmitter.hasExchange()) {
        exchange?.detachWithViolence()
      }
      // 重试不超过20此
      if (++followUpCount > MAX_FOLLOW_UPS) {
        throw ProtocolException("Too many follow-up requests: $followUpCount")
      }
      // 修改下次的请求
      request = followUp
      // 记录上一次的response
      priorResponse = response
    }
  }
  

RetryAndFollowUpInterceptor.followUpRequest

主要针对重定向的几个状态码进行特殊里,从Header中取Location字段,构造重定向request

private fun followUpRequest(userResponse: Response, route: Route?): Request? {
    val responseCode = userResponse.code

    val method = userResponse.request.method
    when (responseCode) {
      HTTP_PROXY_AUTH -> { // 407
        val selectedProxy = route!!.proxy
        if (selectedProxy.type() != Proxy.Type.HTTP) {
          throw ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy")
        }
        // 代理身份认证
        return client.proxyAuthenticator.authenticate(route, userResponse)
      }
      // 身份认证
      HTTP_UNAUTHORIZED -> return client.authenticator.authenticate(route, userResponse)
      // 针对Get、Head请求 执行构建重定向请求
      HTTP_PERM_REDIRECT, HTTP_TEMP_REDIRECT -> { // 307 、308
        if (method != "GET" && method != "HEAD") {
          return null
        }
        return buildRedirectRequest(userResponse, method)
      }
      // 针对300、301、302、303 执行构建重定向请求
      HTTP_MULT_CHOICE, HTTP_MOVED_PERM, HTTP_MOVED_TEMP, HTTP_SEE_OTHER -> {
        return buildRedirectRequest(userResponse, method)
      }
        
      HTTP_CLIENT_TIMEOUT -> {
        // 408 ,说明需要重新发送一次相同的请求
        ... // 判断是否有必要重新发送
        return userResponse.request
      }

      HTTP_UNAVAILABLE -> { // 503
        return null
      }
      else -> return null
    }
  }

private fun buildRedirectRequest(userResponse: Response, method: String): Request? {
    // 设置不需要执行重定向
    if (!client.followRedirects) return null
    // location header 是否支持
    val location = userResponse.header("Location") ?: return null
    // Don't follow redirects to unsupported protocols.
    val url = userResponse.request.url.resolve(location) ?: return null

    // 如果不允许SSL和Non-SSL之间的重定向,则返回null
    val sameScheme = url.scheme == userResponse.request.url.scheme
    if (!sameScheme && !client.followSslRedirects) return null

    // Most redirects don't include a request body.
    val requestBuilder = userResponse.request.newBuilder()
    if (HttpMethod.permitsRequestBody(method)) {
      val maintainBody = HttpMethod.redirectsWithBody(method)
      if (HttpMethod.redirectsToGet(method)) {
        requestBuilder.method("GET", null)
      } else {
        val requestBody = if (maintainBody) userResponse.request.body else null
        requestBuilder.method(method, requestBody)
      }
      if (!maintainBody) {
        requestBuilder.removeHeader("Transfer-Encoding")
        requestBuilder.removeHeader("Content-Length")
        requestBuilder.removeHeader("Content-Type")
      }
    }

    // When redirecting across hosts, drop all authentication headers. This
    // is potentially annoying to the application layer since they have no
    // way to retain them.
    if (!userResponse.request.url.canReuseConnectionFor(url)) {
      requestBuilder.removeHeader("Authorization")
    }
    // 构建 新的请求
    return requestBuilder.url(url).build()
  }

BridgeInterceptor

Bridges from application code to network code.

连接应用层和网络层的桥梁

  • 主要针对请求Header进行处理,如gzip、cookie及其他Header字段等
  • 响应则进行gzip解压
 
 class BridgeInterceptor(private val cookieJar: CookieJar) : Interceptor {

  @Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {
    val userRequest = chain.request()
    val requestBuilder = userRequest.newBuilder()
    
    // 将userRequest中的属性设置到request中
    val body = userRequest.body
    if (body != null) {
      val contentType = body.contentType()
      if (contentType != null) {
        requestBuilder.header("Content-Type", contentType.toString())
      }

      val contentLength = body.contentLength()
      if (contentLength != -1L) {
        requestBuilder.header("Content-Length", contentLength.toString())
        requestBuilder.removeHeader("Transfer-Encoding")
      } else {
        requestBuilder.header("Transfer-Encoding", "chunked")
        requestBuilder.removeHeader("Content-Length")
      }
    }

    if (userRequest.header("Host") == null) {
      requestBuilder.header("Host", userRequest.url.toHostHeader())
    }

    if (userRequest.header("Connection") == null) {
      requestBuilder.header("Connection", "Keep-Alive")
    }

    // 如果没有设置Accept-Encoding,则自动设置为gzip
    var transparentGzip = false
    if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
      transparentGzip = true
      requestBuilder.header("Accept-Encoding", "gzip")
    }
    // 将userRequest中的cookie解析出来并设置到Request中的Cookie字段中
    val cookies = cookieJar.loadForRequest(userRequest.url)
    if (cookies.isNotEmpty()) {
      requestBuilder.header("Cookie", cookieHeader(cookies))
    }
    // 设置 User-Agent
    if (userRequest.header("User-Agent") == null) {
      requestBuilder.header("User-Agent", userAgent)
    }
    // 顺序执行处理方法
    val networkResponse = chain.proceed(requestBuilder.build())

    // 处理响应的headers,并缓存cookie
    cookieJar.receiveHeaders(userRequest.url, networkResponse.headers)

    val responseBuilder = networkResponse.newBuilder()
        .request(userRequest)
    // 设置了Gzip,且响应结果支持gzip,则进行Gzip解析
    if (transparentGzip &&
        "gzip".equals(networkResponse.header("Content-Encoding"), ignoreCase = true) &&
        networkResponse.promisesBody()) {
        
      val responseBody = networkResponse.body
      if (responseBody != null) {
        val gzipSource = GzipSource(responseBody.source())
        val strippedHeaders = networkResponse.headers.newBuilder()
            .removeAll("Content-Encoding")
            .removeAll("Content-Length")
            .build()
        responseBuilder.headers(strippedHeaders)
        val contentType = networkResponse.header("Content-Type")
        responseBuilder.body(RealResponseBody(contentType, -1L, gzipSource.buffer()))
      }
    }

    return responseBuilder.build()
  }

  /** Returns a 'Cookie' HTTP request header with all cookies, like `a=b; c=d`. */
  private fun cookieHeader(cookies: List<Cookie>): String = buildString {
    cookies.forEachIndexed { index, cookie ->
      if (index > 0) append("; ")
      append(cookie.name).append('=').append(cookie.value)
    }
  }
}

下一篇 OkHttp 4源码(3)— 缓存机制分析

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容