笔记--OkHttp拦截器

今天捋清楚了OkHttp的拦截器机制,并且理解之后试着模仿着写了一套拦截器的架构。把笔记记下来,方便以后查看。

OkHttp拦截器

用OkHttp做网络请求的时候,可以很方便的根据自身需要构造很多拦截器,以实现不同的功能,对请求体和返回信息进行处理。甚至可能这个机制太好用了,连OkHttp本身最后对服务器的请求也是通过定义一个拦截器来实现的,这个拦截器叫CallServerInterceptor,是拦截器链条里面的最后一个拦截器。
我们来看一个基本的网络请求,它的流程很简单:请求,等待响应,拿到响应信息,结束。
大致如下图:


基本网络请求流程

但是很多时候我们有需求在这个流程中做很多自己需要的操作,比如打印日志啦,添加统一的请求消息头啦,根据返回信息的共同特征做特定的统一操作啦(比如token过期跳登录页面),还有序列化对象啦等等等等很多各式各样的需求。这个时候拦截器就派上用场了,它的功能极其强大,基本可以满足你在从请求到返回响应过程中想做的一切骚操作。
我们看看加入拦截器之后流程的变化:


添加一个拦截器

我们捋一捋拦截器做的这几件事

[1] 首先,拦截器接收一个request,拿到这个request之后,可以决定要不要做加工处理,然后把加工好的request交给下一个拦截器,这是第一件事。
[2] 自己处理完了之后,要驱动下一个拦截器执行它的操作,不能一层一层处理到自己这里就停下来了,导致链条运行中断,这一步必须做。这是第二件事。
[3] 处理完request,还要看看下一层返回来的response自己要不要处理一下,加工好了之后再把它丢给上一层。这是第三件事。
[4] 第四件事其实包含在1和3里面,就是根据上一层传下来的request和下一层返回的response信息决定要不要做额外的操作,这个其实是附带的操作,和整体拦截器的运行流程相关性不大。

根据上面几条,依次来看,我们来看看实现一个拦截器要实现的基本方法:

class InterceptorA implements Interceptor{
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request request = chain.request();
            //处理requeset...
            Response response = chain.proceed(request);
            //处理response...
            return response;
        }
    }

实现拦截器需要实现一个方法,就是intercept方法,但是传入的参数却不是Request,而是一个Chain对象,这个对象,是一个灵魂中枢。
通过上面的描述,我们可以知道,拦截器机制其实是通过责任链模式来实现的,所以这个灵魂中枢对象起名为Chain(链条)。
Chain里面保存了传入的Request对象,可以随时取出来进行加工处理。
Chain里面保存了整个的拦截器列表,可以取出下一个拦截器,执行intercept方法,也因此,Chain可以从下一个拦截器的intercept方法中获取下一层返回回来的Response,然后对Response进行加工处理,再返回给上一层。
这就是上面的拦截器要做的三件事,都依赖Chain这个对象。
然后我们来看看Chain的实现,看看它都做了那些事...
我们知道,责任链模式中每一个链块执行完自己的操作之后都要继续驱动下一个链块继续执行操作,这就要求每一个链块需要保存有下一个链块的执行方法入口,有的人通过一个公共的管理方法来驱动执行,也有的直接保存下一个链块的对象,拦截器这里的处理方式是,让每一个链块都持有了整个拦截器列表的实体,执行完这一个之后,从列表中取出下一个,继续执行。

/**
 * A concrete interceptor chain that carries the entire interceptor chain: all application
 * interceptors, the OkHttp core, all network interceptors, and finally the network caller.
 */
public final class RealInterceptorChain implements Interceptor.Chain {
  private final List<Interceptor> interceptors;//重点
  private final StreamAllocation streamAllocation;
  private final HttpCodec httpCodec;
  private final RealConnection connection;
  private final int index;//重点
  private final Request request;
  private final Call call;
  private final EventListener eventListener;
  private final int connectTimeout;
  private final int readTimeout;
  private final int writeTimeout;
  private int calls;
  ...
  ...
}

上面的 List<Interceptor> interceptors是整个所有的拦截器列表,index是当前的执行位置,根据index,可以从list中取出下一个有效的拦截器,继续执行。
它的构造方法里有11个参数:

public RealInterceptorChain(List<Interceptor> interceptors, StreamAllocation streamAllocation,
      HttpCodec httpCodec, RealConnection connection, int index, Request request, Call call,
      EventListener eventListener, int connectTimeout, int readTimeout, int writeTimeout) {
    this.interceptors = interceptors;
    this.connection = connection;
    this.streamAllocation = streamAllocation;
    this.httpCodec = httpCodec;
    this.index = index;
    this.request = request;
    this.call = call;
    this.eventListener = eventListener;
    this.connectTimeout = connectTimeout;
    this.readTimeout = readTimeout;
    this.writeTimeout = writeTimeout;
  }

其中和拦截器运行机制相关的关键参数有三个:

List<Interceptor> interceptors;//完整的拦截器列表
int index;//当前的执行位置
Request request;//上一层传入的Request

这就是它驱动下一层拦截器继续执行所依赖的基本信息。
那么具体怎么取出下一个拦截器继续执行下一层的呢?
我们都知道,在实现Interceptor接口的时候,去实现它的intercept(Chain chain)方法,有一行代码是必须要执行的,那就是chain.proceed(request),哪怕你啥也不做,你也得写成下面这个样子:

class InterceptorB implements Interceptor{
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request request = chain.request();
            return chain.proceed(request);
        }
    }

为啥必须执行这行代码呢,因为就是它负责去驱动下一层拦截器执行操作,不写这行代码,拦截器链条走到这一层,就断了,整个链条的处理就会停在这里。
我们来看一下proceed方法的具体实现:

@Override 
public Response proceed(Request request) throws IOException {
    return proceed(request, streamAllocation, httpCodec, connection);
 }
 public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
      RealConnection connection) throws IOException {
    if (index >= interceptors.size()) throw new AssertionError();

    calls++;

    // If we already have a stream, confirm that the incoming request will use it.
    if (this.httpCodec != null && !this.connection.supportsUrl(request.url())) {
      throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
          + " must retain the same host and port");
    }

    // If we already have a stream, confirm that this is the only call to chain.proceed().
    if (this.httpCodec != null && calls > 1) {
      throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
          + " must call proceed() exactly once");
    }

    // Call the next interceptor in the chain.
    RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
        connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
        writeTimeout);
    Interceptor interceptor = interceptors.get(index);
    Response response = interceptor.intercept(next);

    // Confirm that the next interceptor made its required call to chain.proceed().
    if (httpCodec != null && index + 1 < interceptors.size() && next.calls != 1) {
      throw new IllegalStateException("network interceptor " + interceptor
          + " must call proceed() exactly once");
    }

    // Confirm that the intercepted response isn't null.
    if (response == null) {
      throw new NullPointerException("interceptor " + interceptor + " returned null");
    }

    if (response.body() == null) {
      throw new IllegalStateException(
          "interceptor " + interceptor + " returned a response with no body");
    }

    return response;
  }

取出里面关键的代码出来看:

public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
      RealConnection connection) throws IOException {
    ...
    ...
    // Call the next interceptor in the chain.
    RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
        connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
        writeTimeout);
    Interceptor interceptor = interceptors.get(index);
    Response response = interceptor.intercept(next);
    ...
    ...
}

上面的注释也写的很清晰了:调用链条中的下一个拦截器。第一行代码,构造下一个拦截器的Chain,传入本层的request,index+1,以及拦截器列表。然后从拦截器列表中取出下一个拦截器,传入构造好的Chain对象,执行intercept(chain)方法。下一层的方法处理完,返回response,本层通过proceed()方法取得response,再决定要不要继续做处理,再继续返回给上一层。
到这一步,基本就把拦截器的工作流程梳理清楚了,再回头来看实现拦截器的方法里做的事情,就清晰很多了:

class InterceptorA implements Interceptor{
        @Override
        public Response intercept(Chain chain) throws IOException {
            //上一层构造好的chain对象,传入本层,里面包含了整个的拦截器列表和上一层传入的request对象
            Request request = chain.request();//chain里面可以随时取出request对象进行加工处理
            //处理requeset...

            //porceed方法负责将本层加工处理好的request对象构造出下一层的chain对象
            //并驱动执行下一层拦截器的intercept(Chain chain) 方法,获取下一层返回的response
            Response response = chain.proceed(request);
            
            //获取到下一层返回回来的response之后,根据自身需要来决定是否要处理response或者根据
            //response的信息决定要不要做一些额外的事情
            //处理response...

            return response;//返回response给上一层
        }
    }

这就是单个拦截器所做的所有事情。
然后,当加入多个拦截器之后,处理的大致流程就变得如下图所示:


多层拦截器

以上,拦截器整个的运行机制基本就梳理完了。

...@Y(^ _ ^)Y@ ...

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