OkHttp 源码简要分析三 (拦截器及其细节)

上一章提到了 OkHttp 的 RealCall 类,获取报文是通过 getResponseWithInterceptorChain() 方法,这里面用到了责任链模式,看看代码

  Response getResponseWithInterceptorChain() throws IOException {
    List<Interceptor> interceptors = new ArrayList<>();
    // 0 OkHttpClient 创建时,我们自定义的拦截器
    interceptors.addAll(client.interceptors());
    // 1 失败重试以及重定向
    interceptors.add(retryAndFollowUpInterceptor);
    // 2 把用户构造的请求转换为发送到服务器的请求、把服务器返回的响应转换为请求的响应的
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    // 3 读取缓存及更新缓存
    interceptors.add(new CacheInterceptor(client.internalCache()));
    // 4 与服务器建立连接
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      // 5 不是webSocket,网络拦截器
      interceptors.addAll(client.networkInterceptors());
    }
    // 6 向服务器发送请求数据,消息头就是在这一步设置进去的;从服务器读取响应数据
    interceptors.add(new CallServerInterceptor(forWebSocket));
    // 责任链模式
    Interceptor.Chain chain = new RealInterceptorChain(
        interceptors, null, null, null, 0, originalRequest);
    return chain.proceed(originalRequest);
  }

责任链模式是一种设计模式,对此不太了解的可以搜一下相关资料,它会形成一个串,从上到下,然后再把结果依次从下往上返回,我们可以任意添加拦截器按照规范去做任意操作。

0 是我们自定义的拦截器,这个最后再讲;

1 是失败重试以及重定向,我们取消请求时也是把取消的属性存储在这个类里,同时对外暴露方法;这个拦截器中,创建了 StreamAllocation 对象,createAddress() 这个方法创建了 Address 对象,它是 StreamAllocation 其中的一个属性,这里是初始化一个连接对象,然后开启while循环,在循环中,把它传给下一个拦截器中;接收到 Response 后,会调用followUpRequest() 方法检查是否需要重定向,不需要的话跳出while循环;如果需要,会有个次数检查,不能超过20次,把它传给下一个拦截器中,继续第一次的步骤;

2 BridgeInterceptor 这个有点意思,Request 中有我们添加的消息头 Headers,在 BridgeInterceptor 中,会对 Request 中消息头做些修改,比如 Post 请求中,判断 body.contentLength() 的长度,添加 Content-Length 或 Transfer-Encoding 中的一个,添加一个就会把另外一个移除;添加 Content-Type、Connection 等属性,如果我们没有设置 Accept-Encoding 属性,这里会自动添加为 gzip 压缩格式;然后就是把修改头部信息后的 Request 传给下一个连接器,接收回传的 Response,在获取 Response 后会判断报文如果是以 gzip压缩过的,则会先进行解压缩,移除响应中的 Content-Encoding 和 Content-Length,通过 Okio 来解压报文,创建新的 ResponseBody;

3 是缓存拦截,这个里面判断挺多的,就是为了节省流量,可以考虑使用本地缓存,减少请求次数同时也减轻了服务端的压力,这个需要在创建 OkHttpClient 时设置 Cache 值。

4  ConnectInterceptor 中的逻辑很简单,这里获取了 RetryAndFollowUpInterceptor 中创建的 StreamAllocation 对象,然后创建了 HttpStream 对象,使用http1.1,对应的是 Http1xStream 这个实现类,然后把这些对象传递给了下一个拦截器;

5 这个也是外面传进来的自定义的拦截器,这个叫网络拦截器,比如常见的是 FaceBook 开源的 StethoInterceptor 这个拦截器。

6 CallServerInterceptor 这个是真正的向服务器请求数据的类,这个里面牵涉消息头和消息体的写入问题httpStream .writeRequestHeaders(request) 这行代码就是把 Request 的请求方式及url和 HTTP/1.1 拼接在一起形成名为 requestLine 的字符串,然后把requestLine添加到 BufferedSink 对象中,紧接着就是把消息头里面的信息也添加到 BufferedSink 中,我们添加在 Request 中的消息头信息就这么添加到Okio中,为下一步做准备;然后就是判断是否是post请求及是否存在RequestBody,如果有,则写入请求体,把其中的内容写入一个新的 Sink 中进行请求; httpStream.readResponseHeaders() 是用来读取响应信息的,去读取返回的信息,一旦有数据返回,马上把它封装到 Response.Builder 中,接着设置一些参数后调用 build() 方法创建 Response,如果不是WebSocket请求或响应码不为101,则调用httpStream.openResponseBody(response) 创建一个 RealResponseBody 对象,封装了响应头和报文,然后创建新的 Response 对象返回给上一层。


以上是拦截器模式,现在重点说说拦截器 0 ,也就是我们自己定义的拦截器。我们可以写一个类,实现 Interceptor 接口,然后通过创建 OkHttpClient 时,使用 addInterceptor() 方法把它添加进去,我们需要自定义哪些拦截器呢?比如说想打印一下网络请求的耗时、想打印一些请求头、响应头的日志或者说想添加网络请求的公共参数等,都可以用到。现在先说打印网络耗时,如下

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

        long t1 = SystemClock.elapsedRealtime();
        Response response = chain.proceed(request);
        long t2 = SystemClock.elapsedRealtime();
        Log.e("LoggingInterceptor", "Received response for " + "   " +
                response.request().url() + "   " + (t2 - t1));

        return response;

    }
}

intercept(Chain chain) 方法中的参数 Chain 是 RealInterceptorChain 类型,就是 getResponseWithInterceptorChain() 方法中创建的 RealInterceptorChain 这种类型,由于本次只有这一个自定义的拦截器,所以在这里, chain.request() 获取的 Request 是 getResponseWithInterceptorChain() 方法中的 originalRequest 对象,也就是一开始最原始的 Request,如果我们在这里对 request 修改了,把它传递给下一个拦截器,则下一个拦截器里接收的就不是最原始的request数据了,而是上一个传递进来的数据。一开始就记录个时间,这里用的是开机到现在的时间,然后调用 chain.proceed(request) 开始责任链模式的传递,这里是个耗时过程,等获取到数据后,再记录个时间,然后打印它的时间差,这就是网络请求的耗时。

扫描二维码关注公众号,回复: 9779717 查看本文章

打印网络请求的消息头或请求参数等信息或者返回报文等信息,怎么打印呢?在前面拦截器继承上再扩展,举个栗子

public class LoggingInterceptor implements Interceptor {

    @Override
    public Response intercept(Chain chain) throws IOException {
        long t1 = SystemClock.elapsedRealtime();

        Request request = chain.request();
        Headers headers = request.headers();
        Log.e("LoggingInterceptor", "method:  " + request.method() + "    " + "headers:  " + headers.toString());
        RequestBody requestBody = request.body();
        if(requestBody != null){
            MediaType mediaType = requestBody.contentType();
            long length = requestBody.contentLength();
            Log.e("LoggingInterceptor", "mediaType:  " + mediaType.toString() + "      " + "length:  " + length);

            Buffer buffer = new Buffer();
            requestBody.writeTo(buffer);
            String params = buffer.readUtf8();
            Log.e("LoggingInterceptor", "params:  " + params); // 注意,这里是编码后的字符串
        }
        
        Response response = chain.proceed(request);
        long t2 = SystemClock.elapsedRealtime();
        Log.e("LoggingInterceptor", "Received response for " + "   " +
                response.request().url() + "   " + (t2 - t1));

        return response;

    }
}

Request 这个类对外暴露了公共方法,我们可以根据它来获取想要的值,如栗子中那样,获取信息头及消息体,这里面需要注意一点,如果是 Post 请求的话,一般都会需要添加一些参数,我们这里使用了 Okio 的库来读取 RequestBody 中的参数,需要注意的是,读出来的 String params = buffer.readUtf8() 是编码的值,如果我们要看原版的需要使用 URLDecoder.decode(params) 来解码,为什么会这样呢? 我们一般通过 FormBody 来作为 Post 请求体,添加参数会通过 HttpUrl.canonicalize() 方法给 key 和 value 进行编码,如果参数的 key 和 value 都是字母的话没影响,但如果是汉字则惨了,如果没有相应的解码,则我们打印的值自己也看不明白。网上有个开源的日志拦截器 HttpLoggingInterceptor,可以了解一下。

Request 这个类使用了 Builder 模式,它可以很方便扩展字段或者创建新对象,或者把一个对象的属性值复制给一个新的对象。因此像添加公共参数这种需求,一般可以使用拦截器来实现,我们需要给 Request 添加新的公共参数,举个栗子

public class CommonParamsInterceptor implements Interceptor {


    public CommonParamsInterceptor() {
    }

    @Override
    public Response intercept(Chain chain) throws IOException {

        Request request = chain.request();
        Request.Builder requestBuilder = request.newBuilder();

        // 公共参数
        Map<String, String> paramsMap = new HashMap<>();
        paramsMap.put("token", "默苍离");//添加键值对
        paramsMap.put("section", "玄狐");//添加键值对

        if (request.method().equals("POST") && request.body().contentType().subtype().equals("x-www-form-urlencoded")) {
            FormBody.Builder formBodyBuilder = new FormBody.Builder();
            if (paramsMap.size() > 0) {
                Iterator iterator = paramsMap.entrySet().iterator();
                while (iterator.hasNext()) {
                    Map.Entry entry = (Map.Entry) iterator.next();
                    formBodyBuilder.add((String) entry.getKey(), (String) entry.getValue());
                }
            }
            String postBodyString = bodyToString(request.body());
            RequestBody formBody = formBodyBuilder.build();
            String commonParam = bodyToString(formBody);
            if(!TextUtils.isEmpty(commonParam)){
                postBodyString +=  "&" + commonParam;
            }
            requestBuilder.post(RequestBody.create(MediaType.parse("application/x-www-form-urlencoded;charset=UTF-8"), postBodyString));
        } else {
            injectParamsIntoUrl(request, requestBuilder, paramsMap);
        }
        request = requestBuilder.build();
        return chain.proceed(request);
    }

    /**
     * 把参数拼接在 url 后面。
     */
    private void injectParamsIntoUrl(Request request, Request.Builder requestBuilder, Map<String, String> paramsMap) {
        HttpUrl.Builder httpUrlBuilder = request.url().newBuilder();
        if (paramsMap.size() > 0) {
            Iterator iterator = paramsMap.entrySet().iterator();
            while (iterator.hasNext()) {
                Map.Entry entry = (Map.Entry) iterator.next();
                httpUrlBuilder.addQueryParameter((String) entry.getKey(), (String) entry.getValue());
            }
        }
        requestBuilder.url(httpUrlBuilder.build());

    }

    /**
     *  RequestBody中参数其转化为 String
     */
    private static String bodyToString(final RequestBody request) {
        try {
            final Buffer buffer = new Buffer();
            request.writeTo(buffer);
            return URLDecoder.decode(buffer.readUtf8());//name和value默认是会被编码的,此处需要反编码
        } catch (IOException e) {
            e.printStackTrace();
            return "";
        }
    }

}


注意拦截器中,paramsMap 是定义的公共参数的集合,可以由外部传进来,这里为了简便直接写在方法里。get 方式的参数是拼接在url后面,post 方式是添加到消息体中,所以这里要判断方式进行进行拼接;如果是 POST 并且是表单格式,则创建一个 FormBody 添加参数,然后调用 bodyToString() 方式,这里是通过 Okio 把RequestBody中参数转化为 String 类型,把原 request 中的 RequestBody 参数也转换为 String 类型,然后通过  "&" 拼接处完整的参数 String,然后通过 RequestBody.create() 方式创建新的 RequestBody,并把它添加到 Request 中,调用 bodyToString() 方式要注意解码。 如果是 GET 方式通过 injectParamsIntoUrl() 方法,把通过 HttpUrl 把参数添加进去,builder.addQueryParameter() 是用来添加参数的,最后创建 HttpUrl 后,把它添加到  Request 中,公共参数就这么添加好了。如果想添加消息头的方法类似,消息头就不需要区分请求方式了,都一样。


最终觉得 OkHttp 和 HttpUrlConnection 是一级的,都是用 socket 实现了网络连接;HttpUrlConnection 直接使用了 IO 流,OkHttp 用的是 Okio,是对 IO 流的封装,效率更高。
 

发布了176 篇原创文章 · 获赞 11 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/Deaht_Huimie/article/details/102863767