前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >OkHttp 3.x 源码解析(一)之Interceptor 拦截器

OkHttp 3.x 源码解析(一)之Interceptor 拦截器

作者头像
开发者技术前线
发布2020-11-23 15:24:34
1.4K0
发布2020-11-23 15:24:34
举报

阅读过很多写过Okhttp原理的文章,笔者看完觉得还是很失望,因此大白君打算写一个okhttp系列,接下来下半年还会写一个对RxJava2的系列,看过我去年写Retrofit和Java系列的,都知道我是用心去写的,并不是凭空的去翻译API, 大都是自己亲自使用过后总结出的经验,再来一步步的构思去写,好的博客,笔者认为首先要构思清晰,由浅入深,再总结回顾,最后恍然大悟!这样才能让读者身临其境,喜欢的朋友可以继续关注Tamic,若是出,只出高质量技术文章

拦截器

Java里的拦截器是动态拦截Action调用的对象。它提供了一种机制可以使开发者可以定义在一个action执行的前后执行的代码,也可以在一个Action执行前阻止其执行,同时也提供了一种可以提取action中可重用部分的方式。 在AOP(Aspect-Oriented Programming)中拦截器用于在某个方法或字段被访问之前,进行拦截然后在之前或之后加入某些操作。

过滤器

过滤器可以简单理解为“取你所想取”,忽视掉那些你不想要的东西;拦截器可以简单理解为“拒你所想拒”,关心你想要拒绝掉哪些东西,比如一个BBS论坛上拦截掉敏感词汇。

  • 1.拦截器是基于Java反射机制的,而过滤器是基于函数回调的。但很意外的是OKhttp的拦截器也是基于接口回调的。
  • 2.过滤器依赖于servlet容器,而拦截器不依赖于servlet容器。
  • 3.拦截器只对action起作用,而过滤器几乎可以对所有请求起作用。
  • 4.拦截器可以访问action上下文、值栈里的对象,而过滤器不能。
  • 5.在action的生命周期里,拦截器可以多起调用,而过滤器只能在容器初始化时调用一次。

Android里面过滤器大家用的已经无法再陌生了,Filter就是一个很好的列子,在清单文件注册Filter,就可以过滤掉启动某个组件的Action。

为何这里我额外的描述了过滤器的概念,后续文章中我会对okhttp的网络返回结果加一个自定义过滤器,用来做错误处理!以前文章我是通过rxjava的转换器实现的。

Okhttp拦截器

Okhttp拦截器因此应运而生,处理一次网络调用的Action拦截,做某些修改操作。下图是拦截器在okhttp中的介绍,图就直接安利了,来自github。

使用

okhttp拦截器用法很简单,构建OkHttpClient的时候通过.addInterceptor()就可以将拦截器加入到一次会话中。

OkHttpClient client = 
 new OkHttpClient.Builder()      
    .addInterceptor(new LoggingInterceptor())      
     .build();

自定义拦截器

拦截器是Okhttp一种强大的机制,可以监视,重写和重试每一次网络请求。下面是一个简单的拦截器,用于输出传出的请求和响应日志。

class LoggingInterceptor implements Interceptor{   
  @Override 
  public Responseintercept(Interceptor.Chain chain) throws IOException {   
   Request request = chain.request();    
   long t1 = System.nanoTime();         
    Response response = chain.proceed(request);    
    long t2 = System.nanoTime(); 
      response.request().url(), (t2 - t1) / 1e6d, response.headers())); 
      return response;   
    } 
  }

拦截器实现关键部分是调用chain.proceed(request)。这个方法是所有HTTP工作发生的地方,以满足请求和响应的需求。

拦截器可以有多个,假设同时拥有一个压缩拦截器和一个校验拦截器:需要确定数据是否已压缩,然后对数据进行校验,或者校验和压缩两者一起使用。OkHttp的拦截器用集合用来跟踪调度拦截器,拦截器是按集合索引按顺序调用。

应用拦截器

拦截器可以注册为应用程序或网络拦截器。使用LoggingInterceptor 来显示不同的地方。

注册一个应用程序通过调用拦截器的

addInterceptor()OkHttpClient.Builder

OkHttpClient client = 
   new OkHttpClient.Builder()          
   .addInterceptor(new LoggingInterceptor())        
   .build(); Request request = new Request.Builder()      
   .url("http://www.publicobject.com/helloworld.txt")      
   .header("User-Agent", "OkHttp Example")       
   .build(); 
 Response response =   
      client.newCall(request).execute(); 
 response.body().close();

URLhttp://www.publicobject.com/helloworld.txt

重定向到https://publicobject.com/helloworld.txt

OkHttp自动会跟踪这个重定向。应用拦截器被调用一次,网络返回响应chain.proceed()具有重定向返回如下:

INFO: Sending request http://www.publicobject.com/helloworld.txt on null User-Agent: OkHttp Example INFO: Received response for https://publicobject.com/helloworld.txt in 1179.7ms Server: nginx/1.4.6 (Ubuntu) Content-Type: text/plain Content-Length: 1759 Connection: keep-alive

可以看到,被重定向是因为response.request().url()不同于request.url()。两个日志记录了两个不同的URL。

网络拦截器

和注册应用拦截器一样,注册网络拦截器和他是非常相似的。调用addNetworkInterceptor(),不是调用addInterceptor()这个方法;

OkHttpClient client = 
     new OkHttpClient.Builder()             
.addNetworkInterceptor(new LoggingInterceptor())             .build();      Request request = new Request.Builder()           .url("http://www.publicobject.com/helloworld.txt")           .header("User-Agent", "OkHttp Example")           .build();      Response response = client.newCall(request).execute();               response.body().close(); 123456789101112123456789101112

跑起这个代码时,拦截器会运行两次。一次为初始请求http://www.publicobject.com/helloworld.txt,另一个为重定向https://publicobject.com/helloworld.txt。

INFO: Sending request http://www.publicobject.com/helloworld.txt on Connection{www.publicobject.com:80, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=none protocol=http/1.1} User-Agent: OkHttp Example Host: www.publicobject.com Connection: Keep-Alive Accept-Encoding: gzip

INFO: Received response for http://www.publicobject.com/helloworld.txt in 115.6ms Server: nginx/1.4.6 (Ubuntu) Content-Type: text/html Content-Length: 193 Connection: keep-alive Location: https://publicobject.com/helloworld.txt INFO: Sending request https://publicobject.com/helloworld.txt on Connection{publicobject.com:443, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA protocol=http/1.1} User-Agent: OkHttp Example Host: publicobject.com Connection: Keep-Alive Accept-Encoding: gzip INFO: Received response for https://publicobject.com/helloworld.txt in 80.9ms Server: nginx/1.4.6 (Ubuntu) Content-Type: text/plain Content-Length: 1759 Connection: keep-alive

网络请求还包含了更多的数据,例如Accept-Encoding: gzip由OkHttp添加的请求头来支持数据响应进行压缩。网络拦截器Chain具有非空值Connection(下文会讲到),可用于询问用于连接到Web服务器的IP地址和TLS配置。

如何选择拦截器?

在应用拦截器和网络拦截器之间如何让进行选择?先看看两个拦截器有不同的优点。

应用拦截器

  • 不需要关心中间响应,如重定向和重试等。
  • 只调用一次,即使从缓存中拿数据提供HTTP响应。
  • 遵守应用程序的原始意图。不关心OkHttp注入的其他请等If-None-Match。
  • 允许中断结束本次请求,并且不会调用Chain.proceed()。
  • 允许重试并进行多次呼叫Chain.proceed()。

网络拦截器

  • 能够对重定向和重试等中间响应环节进行操作。
  • 不会调用缓存的数据来结束网络。也就是说即使有缓存有会去调用Http的请求。
  • 用来监视整个请求和返回的数据。
  • 可以访问Connection请求。

重写请求

拦截器可以添加,删除或替换请求头。还可以修改请求的正文。例如,如果连接到已知支持Web服务器,则可以使用应用程序拦截器添加请求体压缩。

final class GzipRequestInterceptor implements Interceptor {    
   @Override 
   public Response intercept(Interceptor.Chain chain) throws IOException {    
     Request originalRequest = chain.request();    
     if (originalRequest.body() == null || originalRequest.header       
     ("Content-Encoding") != null) {      
       return chain.proceed(originalRequest);   
       }   
       Request compressedRequest = originalRequest.newBuilder()       
       .header("Content-Encoding", "gzip")       
       .method(originalRequest.method(), 
       gzip(originalRequest.body()))       
       .build();    
       return chain.proceed(compressedRequest);  
       }  
       private RequestBody gzip(final RequestBody body) {     
       return new RequestBody() {     
       @Override 
       public MediaType contentType() {        
       return body.contentType();     
       }      
       @Override public long contentLength() {         
       return -1;          
       // We don't know the compressed length in advance!      
       }     
       @Override 
       public void writeTo(BufferedSink sink) throws IOException {        
          BufferedSink gzipSink = Okio.buffer(new GzipSink(sink));        
          body.writeTo(gzipSink);        
          gzipSink.close();       
       }     
     };   
    } 
  }

重写响应

对称地拦截器可以重写响应头并转换响应Body。这通常比重写请求头更加有杀伤管力,因为可以篡改,违反了web服务器的本身返回数据的本意!

在特殊的情况 需要修复容错服务端的返回的数据,重写返回的Head是解决问题的有效方式。例如,可以修复服务器配置了错误的Cache-Control响应头来配置更好的响应缓存:

private static final Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = new Interceptor() {    
   @Override 
    public Response intercept(Interceptor.Chain chain) throws IOException {   
       Response originalResponse =        chain.proceed(chain.request());    
       return originalResponse.newBuilder()   
       .header("Cache-Control", "max-age=60")   
       .build();   
     } 
 };

一般来讲,这种方法在补充Web服务器上的不足,修复客户端程序数据的场景下效果更好!

工作原理

1, Interceptor代码本质:

拦截器源码:包含基础的RequestResponse 获取接口,并且内部包含了Connection接口。 代码如下:

public interface Interceptor {   
     Response intercept(Chain chain) throws IOException;   
     interface Chain {  
        Request request();    
        Response proceed(Request request) throws IOException;    
        /**     
        * Returns the connection the request will be executed on. This is  only available in the chains     
        * of network interceptors; for application interceptors this is  always null.    
        */   
        @Nullable Connection connection();  
       } 
   }

2 .Connection是神马东西?

Connection是一次面向连接过程,这里包含基础的协议ProtocolSocketRouteHandshake.

`public interface Connection {   
     Route route();   
     Socket socket();  
     @Nullable 
     Handshake handshake();  
     Protocol protocol(); 
 }

Handshake是ohkttp自己的握手机制,里面包括了SSL验证过程,这里不做源码分析,这里提供了基础Https的认证的基础根方法,本文不做探讨。

Interceptor怎么被调用

发起请求

OkHttpClient mOkHttpClient = new OkHttpClient();     
Request request = new Request.Builder()             .url("https://github.com/hongyangAndroid")             .build(); 
//new call
 Call call = mOkHttpClient.newCall(request);

进行newCaLL

RealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {     
   final EventListener.Factory eventListenerFactory = client.eventListenerFactory();     
     this.client = client;    
     this.originalRequest = originalRequest;     
     this.forWebSocket = forWebSocket;     
     //这里就是处理拦截器的地方!     
     this.retryAndFollowUpInterceptor = new 
     RetryAndFollowUpInterceptor(client, forWebSocket);    
     // TODO(jwilson): 
     this is unsafe publication and not threadsafe.    
     this.eventListener = 
         eventListenerFactory.create(this);  
     }

处理请求拦截

@Override 
public Response intercept(Chain chain) throws IOException {
    Request request = chain.request(); 
    streamAllocation = new StreamAllocation(     
    client.connectionPool(), 
    createAddress(request.url()), callStackTrace); int followUpCount = 0; 
    Response priorResponse = null; while (true) {   if (canceled) {     
    streamAllocation.release();     
    throw new IOException("Canceled");   }   
    Response response = null;   
    boolean releaseConnection = true;   
    try {     //也就是这里进行上层注入的拦截拦截     
        response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null);     
        releaseConnection = false;   
        } catch (RouteException e) {     
           ......     
          continue;   
          } catch (IOException e) {     
          // An attempt to communicate with a server failed. The request may have been sent.     
          boolean requestSendStarted = !(e instanceof ConnectionShutdownException);     
          if (!recover(e, requestSendStarted, request)) throw e;     
            releaseConnection = false;     
            continue;   
          } finally {    
           // We're throwing an unchecked exception. Release any resources.    
              if (releaseConnection) {       
               streamAllocation.streamFailed(null);       
               streamAllocation.release();     
              }   
           }   
        .......
   }

Response proceed()方法很简单,内部使用集合进行遍历,一个反射进行真实数据处理! 其通过内部的

Response response = interceptor.intercept(next); 其实就回调到了你实现的intercept(Chain chain)的接口,一次闭环结束!

处理返回拦截

使用者都知道我们每次进行一次请求调用call.execute() ,真正的response也在这里开始,拦截器也从这方法为导火索。

Override  public Response execute() throws IOException {   synchronized (this) {   
   if (executed) throw new IllegalStateException("Already Executed");  
  executed = true;   
   } captureCallStackTrace();
  try {  
    client.dispatcher().executed(this);   
    //处理拦截了   
    Response result = getResponseWithInterceptorChain();   
    if (result == null) throw new IOException("Canceled");       
        return result; 
    } finally {   
     client.dispatcher().finished(this); 
}

如果到这里你还未能猜出内部机制,这里也不用在介绍,通过处理请求拦截的介绍,你一也应该明白内部进行拦截器集合循环遍历,进行的具体处理。

到此明白了Interceptor的工作原理我们就可以愉快的使用他来完成一些功能了。

这里我做了一个图, 更能理解整个过程,只理解拦截机制,Okhttp源码流程带后续继续分析。

具体功能实践

增加同步cookie

请看:http://www.jianshu.com/p/1a5f14b63f47

修改请求

Okhttp对每个Request统一动态添加header和参数

实现缓存

请看 Rxjava +Retrofit 你需要掌握的几个技巧,Retrofit缓存

注意

OkHttp的拦截器需要OkHttp 2.2或以上版本使用。值得注意的是,拦截器不支持OkUrlFactory,或者依赖Okhttp的其他库,包括Retrofit≤1.8和 Picasso≤2.4。

如果看的不过瘾,下期到Tamic原创,分析下okhttp路由原理!

参考资料

Okhttp官方GitHub Wiki以及APi文档 Tamic / http://blog.csdn.net/sk719887916/article/details/74308343

iOS特殊赞助通道,随意支持作者!

☞ 持续关注,[撸框架系列]

后续不定期将推出撸RxJava, 撸RxBus,撸RxCache,RxLifecyle。

精彩推荐

自己动手轻松撸一个OkHttp框架

你不知道的Retrofit缓存库RxCache

技术 - 资讯 - 感悟

END

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2017-07-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 开发者技术前线 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 拦截器
  • 过滤器
  • Okhttp拦截器
  • 如何选择拦截器?
  • 网络拦截器
    • 重写请求
      • 重写响应
        • 工作原理
          • 具体功能实践
          • 增加同步cookie
          • 修改请求
          • 实现缓存
            • 注意
            相关产品与服务
            容器服务
            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档