OkHttp3源码解析(二)——拦截器链和缓存策略

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 从源码分析OkHttp3的原理

OKHttp3源码解析系列

上一篇文章中我们介绍了OkHttp3的同步和异步请求流程,我们分析到不论是同步还是异步请求,都是通过RealCall内部的getResponseWithInterceptorChain方法来执行具体的网络连接的,下面我们来分析看看OkHttp3具体是怎么进行网络请求的。

OkHttp3的拦截器链

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) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));

    //这里参数0,表示的是interceptors列表中的索引。
    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    return chain.proceed(originalRequest);
}

getResponseWithInterceptorChain方法中添加了一系列的拦截器Interceptor,包括用户的拦截器等。然后调用了RealInterceptorChain的proceed方法。每个拦截器都有特定的作用,通过责任链模式,每个拦截器完成自己的任务后,不断调用下个拦截器,最后完成网络请求。

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

    calls++;

    ...

    // Call the next interceptor in the chain.
    //这里index+1,所以就会调用interceptors中的下一个拦截器
    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);

    ...

    return response;
}

这里可以看到在RealInterceptorChain的proceed方法中,会逐个调用interceptors中的拦截器,调用顺序就是前面添加的顺序。所以RealInterceptorChain可以看作是整个拦截器链的控制器,通过它可以调用链上的下一个拦截器,而每一个拦截器就相当于链上的一环。

(1)用户拦截器:通过Builder的addInterceptor方法添加的拦截器。

(2)RetryAndFollowUpInterceptor:负责失败自动重连和必要的重定向。

(3)BridgeInterceptor:负责将用户的Request转换成一个实际的网络请求Request,再调用下一个拦截器获取Response,然后将Response转换成用户的Response。

(4)CacheInterceptor:负责控制缓存,缓存的逻辑就在这里面。

(5)ConnectInterceptor:负责进行连接主机,在这里会完成socket连接,并将连接返回。

(6)CallServerInterceptor:和服务器通信,完成Http请求。

所以我们可以总结出网络请求的调用流程:

(1)RealInterceptorChain拦截器链的proceed方法-->用户拦截器-->RetryAndFollowUpInterceptor-->BridgeInterceptor-->CacheInterceptor-->ConnectInterceptor-->CallServerInterceptor

(2)当然在CacheInterceptor中,如果发现当前的请求有缓存的话就会直接返回Response了,而不会走后面的调用链。


OkHttp3的缓存策略

上面提到拦截器中有个缓存拦截器CacheInterceptor,它里面主要包含的是缓存的逻辑。下面我们来看看这个缓存策略是怎么样的。

首先我们看CacheInterceptor的构造函数

public final class CacheInterceptor implements Interceptor {
  final InternalCache cache;

  public CacheInterceptor(InternalCache cache) {
    this.cache = cache;
  }
}

从文章开头中我们在RealCall内部添加CacheInterceptor拦截器的方式可知,CacheInterceptor中的InternalCache来自OkHttpClient中

public class OkHttpClient implements Cloneable, Call.Factory, WebSocket.Factory {
    ...
    final @Nullable Cache cache;
    final @Nullable InternalCache internalCache;

    OkHttpClient(Builder builder) {
        //我们在RealCall中初始化CacheInterceptor时调用的就是这个方法
        InternalCache internalCache() {
            return cache != null ? cache.internalCache : internalCache;
        }
    }
    
    public static final class Builder {
    
        @Nullable Cache cache;
        @Nullable InternalCache internalCache;
        
        /** Sets the response cache to be used to read and write cached responses. */
        //这里需要注意的是这个方法并不是public的
        void setInternalCache(@Nullable InternalCache internalCache) {
          this.internalCache = internalCache;
          this.cache = null;
        }

        /** Sets the response cache to be used to read and write cached responses. */
        public Builder cache(@Nullable Cache cache) {
          this.cache = cache;
          this.internalCache = null;
          return this;
        }
    }
}

从上面的OkHttpClient的源码中我们可以得出以下结论:

(1)OkHttpClient中有2个跟缓存有关的变量,一个是Cache,一个是internalCache。其中我们可以通过Builder来设置Cache,但是不能设置internalCache。

(2)从上面可以看出,默认Cache和internalCache都是null,也就是OkHttpClient没有默认的缓存实现。

(3)缓存拦截器CacheInterceptor中的internalCache来自OkHttpClient的Cache,因为OkHttpClient中的internalCache一直是null,我们没法从外界设置,所以如果我们没有为OkHttpClient设置Cache,那么缓存拦截器中的internalCache就也为null了,也就没法提供缓存功能。

从上面的源码中我们还发现,internalCache虽然不能从外界设置,但是它却是cache的一个内部变量。下面我们来具体看看缓存Cache的实现。

缓存Cache

public final class Cache implements Closeable, Flushable {
    ...
    //internalCache的方法内部调用的都是Cache的方法
    final InternalCache internalCache = new InternalCache() {
        @Override 
        public Response get(Request request) throws IOException {
          return Cache.this.get(request);
        }
        
        @Override 
        public CacheRequest put(Response response) throws IOException {
          return Cache.this.put(response);
        }
        
        @Override 
        public void remove(Request request) throws IOException {
          Cache.this.remove(request);
        }
        
        @Override 
        public void update(Response cached, Response network) {
          Cache.this.update(cached, network);
        }
        
        @Override 
        public void trackConditionalCacheHit() {
          Cache.this.trackConditionalCacheHit();
        }
        
        @Override 
        public void trackResponse(CacheStrategy cacheStrategy) {
          Cache.this.trackResponse(cacheStrategy);
        }
    };
    //缓存是用DiskLruCache实现的
    final DiskLruCache cache;
    
    ...
    
    public Cache(File directory, long maxSize) {
        this(directory, maxSize, FileSystem.SYSTEM);
    }
    
    Cache(File directory, long maxSize, FileSystem fileSystem) {
        this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize);
    }
    //缓存为key-value形式,其中key为请求的URL的md5值
    public static String key(HttpUrl url) {
        return ByteString.encodeUtf8(url.toString()).md5().hex();
    }

    @Nullable 
    Response get(Request request) {
        String key = key(request.url());
        DiskLruCache.Snapshot snapshot;
        Entry entry;
        try {
          snapshot = cache.get(key);
          if (snapshot == null) {
            return null;
          }
        } catch (IOException e) {
          // Give up because the cache cannot be read.
          return null;
        }
    
        ...
    
        return response;
    }
  
    ...
}

这一看我们就明白了,internalCache的确是Cache的一个内部变量,我们设置了Cache也就有了internalCache。而实际上,internalCache是一个接口类型的变量,它的一系列get、put方法,都是调用的Cache的方法,这也是外观模式的一种典型应用。当然这里internalCache从名字也可以看出是给内部其他对象调用的,所以internalCache和Cache的职责很明确,Cache供外部设置,而internalCache供内部调用。

同时我们看到Cache内部是通过DiskLruCache来实现缓存的,缓存的key就是request的URL的md5值,缓存的值就是Response。我们设置自己的缓存时,可以通过Cache的构造函数传入我们想要存放缓存的文件路径,以及缓存文件大小即可。比如:

OkHttpClient.Builder builder = new OkHttpClient.Builder()
    .connectTimeout(15, TimeUnit.SECONDS)
    .writeTimeout(20, TimeUnit.SECONDS)
    .readTimeout(20, TimeUnit.SECONDS)
    .cache(new Cache(context.getExternalCacheDir(), 10*1024*1024));

下面我们回到缓存拦截器CacheInterceptor中,看看具体的缓存逻辑。主要的逻辑都在intercept方法中。

CacheInterceptor的缓存逻辑

@Override 
public Response intercept(Chain chain) throws IOException {
    //如果我们没有设置缓存或是当前request没有缓存,那么cacheCandidate就为null了
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();
    //如果我们没有设置缓存,或是当前request没有缓存,那么cacheCandidate就为null
    //获取具体的缓存策略
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    //后面会根据networkRequest和cacheResponse是否为空来做相应的操作
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;
    
    ...
}

可以看到intercept方法中会调用缓存策略工厂的get方法获取缓存策略CacheStrategy。我们进入CacheStrategy看看

public final class CacheStrategy {
    /** The request to send on the network, or null if this call doesn't use the network. */
    public final @Nullable Request networkRequest;
    
    /** The cached response to return or validate; or null if this call doesn't use a cache. */
    public final @Nullable Response cacheResponse;
    
    CacheStrategy(Request networkRequest, Response cacheResponse) {
        this.networkRequest = networkRequest;
        this.cacheResponse = cacheResponse;
    }
    ...
    public static class Factory {
        final long nowMillis;
        final Request request;
        final Response cacheResponse;
        
        ...
        public Factory(long nowMillis, Request request, Response cacheResponse) {
        
            this.nowMillis = nowMillis;
            this.request = request;
            this.cacheResponse = cacheResponse;
            ...
        }
        
        public CacheStrategy get() {
            CacheStrategy candidate = getCandidate();
    
            if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
                // We're forbidden from using the network and the cache is insufficient.
                return new CacheStrategy(null, null);
            }
    
            return candidate;
        }
        
        //根据不同的情况返回CacheStrategy
        private CacheStrategy getCandidate() {
            // No cached response.
            //如果缓存中的Response为null
            if (cacheResponse == null) {
                return new CacheStrategy(request, null);
            }
        
            // Drop the cached response if it's missing a required handshake.
            //如果缓存中的response缺少必要的握手信息
            if (request.isHttps() && cacheResponse.handshake() == null) {
                return new CacheStrategy(request, null);
            }
              
            //根据request和response是否能被缓存来生成CacheStrategy
            if (!isCacheable(cacheResponse, request)) {
                return new CacheStrategy(request, null);
            }
              
            CacheControl requestCaching = request.cacheControl();
            //如果Request中的noCache标志位为true或是request的请求头中包含"If-Modified-Since"或是"If-None-Match"标志位
            if (requestCaching.noCache() || hasConditions(request)) {
                return new CacheStrategy(request, null);
            }
            
            //如果缓存的response中的immutable标志位为true,则不请求网络
            CacheControl responseCaching = cacheResponse.cacheControl();
            if (responseCaching.immutable()) {
                return new CacheStrategy(null, cacheResponse);
            }
            ...
        }
    }
}

可以看到在CacheStrategy的内部工厂类Factory中有一个getCandidate方法,会根据具体的情况生成CacheStrategy类返回,是个典型的简单工厂模式。生成的CacheStrategy中有2个变量,networkRequest和cacheResponse,如果networkRequest为null,则表示不进行网络请求;而如果cacheResponse为null,则表示没有有效的缓存。

当我们没有设置缓存Cache时,显然cacheResponse始终都会为null。下面我们继续看intercept中的方法。

//如果networkRequest和cacheResponse都为null,则表示不请求网络而缓存又为null,那就返回504,请求失败
if (networkRequest == null && cacheResponse == null) {
  return new Response.Builder()
      .request(chain.request())
      .protocol(Protocol.HTTP_1_1)
      .code(504)
      .message("Unsatisfiable Request (only-if-cached)")
      .body(Util.EMPTY_RESPONSE)
      .sentRequestAtMillis(-1L)
      .receivedResponseAtMillis(System.currentTimeMillis())
      .build();
}

//如果不请求网络,但是存在缓存,那就直接返回缓存,不用请求网络
if (networkRequest == null) {
    return cacheResponse.newBuilder()
      .cacheResponse(stripBody(cacheResponse))
      .build();
}

//否则的话就请求网络,就会调用下一个拦截器链,将请求转发到下一个拦截器
Response networkResponse = null;
try {
    networkResponse = chain.proceed(networkRequest);
} finally {
    // If we're crashing on I/O or otherwise, don't leak the cache body.
    if (networkResponse == null && cacheCandidate != null) {
        closeQuietly(cacheCandidate.body());
    }
}

根据上述源码我们可以总结出缓存拦截器中的缓存策略:

(1)首先会尝试从缓存Cache中获取当前请求request的缓存response,并根据传入的请求request和获取的缓存response通过缓存策略对象CacheStrategy的工厂类的get方法生成缓存策略类CacheStrategy。

(2)CacheStrategy的工厂类的get方法里面会根据一些规则生成CacheStrategy,这里面的规则实际上控制着缓存拦截器CacheInterceptor的处理逻辑。而这些规则都是根据请求的Request和缓存的Response的header头部信息来生成的,比如是否有noCache标志位,是否是immutable不可变的,以及缓存是否过期等。

(3)CacheInterceptor会根据CacheStrategy中的networkRequest和cacheResponse是否为空,来判断是请求网络还是直接使用缓存。

总结

(1)拦截器链和缓存策略都是OkHttp3的亮点所在,此外还有复用连接池等

(2)拦截器链通过责任链的模式,将网络请求过程中的职责功能都分割开,分别用不同的拦截器来完成失败重连、缓存处理、网络连接等问题。而且用户还可以添加自定义的拦截器,非常灵活,满足面向对象的开闭原则。

(3)缓存策略指的是对于请求的响应的缓存。OkHttp中有专门类Cache来实现缓存,Cache中采用了DiskLruCache,以Request的URL的md5为key,相应Response为value。此外Cache中还通过外观模式对外提供了InternalCache接口变量,用于调用Cache中的方法,也满足面向对象的接口隔离原则和依赖倒置原则等。

(4)缓存拦截器中会根据生成的缓存策略类CacheStrategy的2个变量networkRequest和cacheResponse来决定是连接网络还是直接使用缓存。如果2个变量都为空,则直接返回504,请求失败;如果缓存不为空,则直接返回缓存;如果networkRequest不为空,就通过调用RealInterceptorChain的proceed方法将请求继续转发到下一个拦截器。



不错过每一点精彩,欢迎扫码关注

AntDream

目录
相关文章
|
5天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
16 2
|
21天前
|
数据采集 安全 数据管理
深度解析:DataHub的数据集成与管理策略
【10月更文挑战第23天】DataHub 是阿里云推出的一款数据集成与管理平台,旨在帮助企业高效地处理和管理多源异构数据。作为一名已经有一定 DataHub 使用经验的技术人员,我深知其在数据集成与管理方面的强大功能。本文将从个人的角度出发,深入探讨 DataHub 的核心技术、工作原理,以及如何实现多源异构数据的高效集成、数据清洗与转换、数据权限管理和安全控制措施。通过具体的案例分析,展示 DataHub 在解决复杂数据管理问题上的优势。
91 1
|
5天前
|
存储 安全 Linux
Golang的GMP调度模型与源码解析
【11月更文挑战第11天】GMP 调度模型是 Go 语言运行时系统的核心部分,用于高效管理和调度大量协程(goroutine)。它通过少量的操作系统线程(M)和逻辑处理器(P)来调度大量的轻量级协程(G),从而实现高性能的并发处理。GMP 模型通过本地队列和全局队列来减少锁竞争,提高调度效率。在 Go 源码中,`runtime.h` 文件定义了关键数据结构,`schedule()` 和 `findrunnable()` 函数实现了核心调度逻辑。通过深入研究 GMP 模型,可以更好地理解 Go 语言的并发机制。
|
8天前
|
监控 关系型数据库 MySQL
MySQL自增ID耗尽应对策略:技术解决方案全解析
在数据库管理中,MySQL的自增ID(AUTO_INCREMENT)属性为表中的每一行提供了一个唯一的标识符。然而,当自增ID达到其最大值时,如何处理这一情况成为了数据库管理员和开发者必须面对的问题。本文将探讨MySQL自增ID耗尽的原因、影响以及有效的应对策略。
30 3
|
18天前
|
安全 前端开发 Java
Web安全进阶:XSS与CSRF攻击防御策略深度解析
【10月更文挑战第26天】Web安全是现代软件开发的重要领域,本文深入探讨了XSS和CSRF两种常见攻击的原理及防御策略。针对XSS,介绍了输入验证与转义、使用CSP、WAF、HTTP-only Cookie和代码审查等方法。对于CSRF,提出了启用CSRF保护、设置CSRF Token、使用HTTPS、二次验证和用户教育等措施。通过这些策略,开发者可以构建更安全的Web应用。
56 4
|
17天前
|
安全 Go PHP
Web安全进阶:XSS与CSRF攻击防御策略深度解析
【10月更文挑战第27天】本文深入解析了Web安全中的XSS和CSRF攻击防御策略。针对XSS,介绍了输入验证与净化、内容安全策略(CSP)和HTTP头部安全配置;针对CSRF,提出了使用CSRF令牌、验证HTTP请求头、限制同源策略和双重提交Cookie等方法,帮助开发者有效保护网站和用户数据安全。
44 2
|
18天前
|
消息中间件 缓存 安全
Future与FutureTask源码解析,接口阻塞问题及解决方案
【11月更文挑战第5天】在Java开发中,多线程编程是提高系统并发性能和资源利用率的重要手段。然而,多线程编程也带来了诸如线程安全、死锁、接口阻塞等一系列复杂问题。本文将深度剖析多线程优化技巧、Future与FutureTask的源码、接口阻塞问题及解决方案,并通过具体业务场景和Java代码示例进行实战演示。
38 3
|
21天前
|
数据采集 机器学习/深度学习 数据挖掘
10种数据预处理中的数据泄露模式解析:识别与避免策略
在机器学习中,数据泄露是一个常见问题,指的是测试数据在数据准备阶段无意中混入训练数据,导致模型在测试集上的表现失真。本文详细探讨了数据预处理步骤中的数据泄露问题,包括缺失值填充、分类编码、数据缩放、离散化和重采样,并提供了具体的代码示例,展示了如何避免数据泄露,确保模型的测试结果可靠。
32 2
|
1月前
|
机器学习/深度学习 人工智能 算法
揭开深度学习与传统机器学习的神秘面纱:从理论差异到实战代码详解两者间的选择与应用策略全面解析
【10月更文挑战第10天】本文探讨了深度学习与传统机器学习的区别,通过图像识别和语音处理等领域的应用案例,展示了深度学习在自动特征学习和处理大规模数据方面的优势。文中还提供了一个Python代码示例,使用TensorFlow构建多层感知器(MLP)并与Scikit-learn中的逻辑回归模型进行对比,进一步说明了两者的不同特点。
64 2
|
1月前
|
缓存 前端开发 安全
前端开发者必备:HTTP状态码含义与用途解析,常见错误码产生原因及解决策略
前端开发者必备:HTTP状态码含义与用途解析,常见错误码产生原因及解决策略
107 0

推荐镜像

更多