InterceptorChain模式

最近在看OkHttp源码时,看到了Interceptor的使用,觉得还是很巧妙的设计,所以就提炼出来,已被不时之需。暂且就称为InterceptorChain模式吧。

先来看看OkHttp中InterceptorChain的使用场景吧。

Activity InterceptorChain-1.jpg

OkHttp的每个网络请求都需要创建一个Request,最后返回一个Response。但并不是简单地请求-响应过程,在这其中还有多个Interceptor参与其中。如上图所示,Request依次经过Interceptor1,Interceptor2...(如果有多个Interceptor),最后到达Inner Interceptor。Inner Interceptor创建一个Response,然后再原路返回。在这个过程中,每一个Interceptor都可以对Request和Response做操作。这些Interceptor就如同一个个铁环般连接在一个形成一个链条,这就是InterceptorChain名字的由来。
最简单的一个例子就是LogInterceptor,它只是简单的打印Request和Response中的内容。
怎么才能组成这样一个执行链条呢?OkHttp使用了List保存Interceptor,然后再象递归一样,一个个的从中取出Intercepor执行,这样的一个过程会在后面的例子中看到。

在什么场景下使用这个模式呢?

如果你需要实现一个类似请求-响应的功能,并且你想对请求体或响应结果做一些处理,但是又不想在这个业务逻辑中掺入不相关的逻辑(比如打印日志,数据分析),那么你就可以使用这个模式。当然你还可以想出其他的方式来使用它。下面就来看看具体的例子吧。
场景:图片的读取
这是我假设的一个场景。假设,我需要写一个图片加载器,它最基本的作用就是传入图片在磁盘中的地址,然后读取图片文件并返回原始图片。那么刚开始的代码可能像下面这样:
<pre>
public Image load(String filePath) {
File imageFile = new File(filePath);
//File to Image
....
return image
}
</pre>

之后的某一天你需要记录一下获取的图片名称,然后你的代码就变成了这样:
<pre>
public Image load(String filePath) {

System.out.println("image file:" + filePath);
File imageFile = new File(filePath);

//File to Image
....

return image

}
</pre>
这看起来并不很糟。

又过了一段时间,又需要记录下图片文件被访问的次数。嗯,好吧,还是在原来的方法上直接修改:
<pre>
public Image load(String filePath) {

    System.out.println("image file:" + filePath);
    
    recordCount();

    File imageFile = new File(filePath);
    //File to Image
    ....
    return image

}
</pre>

好吧,暂时这样吧。

再然后,来了一个新需求:读取完图片后需要对图片进行放缩,返回放缩后的图片。这时候怎么办呢?还是在load方法中增加缩放的逻辑?不太好吧,有的地方又不需要缩放。另外写一个缩放方法?好像可以,不过需要在所有调用load方法的地方增加一条调用缩放方法的代码,有点麻烦哦。怎么办呢?
这时候你可能就需要重构代码了。说到这里就该InterceptorChain登场了。

设想一下,你可以有一个最基本的读取图片的方法,其他的诸如日志、记录访问次数、缩放功能都能随意配置,这是不是很爽。
下面我们就看看怎么才能达到这个目的。

整体结构:

class diagram.png

我们把加载图片的逻辑提了出来,封装在一个单独的类:ImageLoader中。ImageLoader中用List保存Interceptor,load方法入参是Request,返回Response。请求的图片地址保存在Request中,读取到的图片内容保存在Response中。Interceptor的intercept方法传入Chain,Chain有个接口request,用于返回Request。Chain还有个接口proceed,用于处理Request。整体的思路是ImageLoader调用load方法,load方法里创建一个ChainAdapter,ChainAdapter用于封装Interceptor的执行。在ChainAdapter的proceed方法中执行Interceptor的intercept方法,然后取出下一个Interceptor,进行封装。这样就形成了一个链条。这样说还是很抽象的,下面就上代码。

先来看看ImageLoader :
<pre>
public class ImageLoader {

private List<Interceptor> interceptors = new LinkedList<>();
private final IOInterceptor io = new IOInterceptor();

private Request request;

public void setRequest(Request request) {
    this.request = request;
}

public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
}

public Response load() {
    interceptors.add(io);
    
    ChainAdapter chain = new ChainAdapter(interceptors, 0, request);
    
    return chain.proceed(request);
}

private class IOInterceptor implements Interceptor {

    @Override
    public Response intercept(Chain chain) {
        
        final Request request = chain.request();
        
        return getImageFromFile(request.fileName);
    }
    
    private Response getImageFromFile(String fileName) {
        Response respose = new Response();
        
        String[] strs = fileName.split("\\\\");
        
        respose.setImage(strs[strs.length - 1]);
        
        return respose;
    }

}

}
</pre>

我们看到load方法中有一句:
<pre>
interceptors.add(io);
</pre>

这一句很关键哦,io是内部类IOInterceptor,外部是不能创建它的,它的intercept方法有点特殊,后面我们会讲到。在interceptors.add(io)之后是
<pre>
ChainAdapter chain = new ChainAdapter(interceptors, 0, request);
return chain.proceed(request);
</pre>
形成链的奥秘都在ChainAdapter里,它实现了Chain接口,看完Interceptor和Chain接口我们再回来说说它。

再看Interceptor这个接口:
<pre>
public interface Interceptor {

Response intercept(Chain chain);

  interface Chain {
    Request request();

    Response proceed(Request request);
  }

}
</pre>
很简单,没什么好说的。下面就进入ChainAdapter了
<pre>
public class ChainAdapter implements Chain {

private final List<Interceptor> interceptors;
private final int index;
private final Request request;

public ChainAdapter(List<Interceptor> interceptors, int index, Request request) {
    this.index = index;
    this.request = request;
    this.interceptors = interceptors;
}

@Override
public Request request() {
    return request;
}

@Override
public Response proceed(Request request) {
    ChainAdapter next = new ChainAdapter(interceptors, index + 1, request);
    
    Interceptor interceptor = interceptors.get(index);
    Response response = interceptor.intercept(next);
    
    return response;
}

}
</pre>

来看看proceed方法。首先是取出下一个Interceptor,然后也封装成ChainAtapter。接着从List中取出当前的Interceptor,调用它的intercept方法,参数传入的就是之前封装好的ChainAtapter。好了,到现在调用链条形成了。等等,好像哪里不对劲?next的proceed方法并没有被调用,链条断了!说到这就需要为这个模式引入一个约定了。那就是在Interceptor的intercept方法中,程序员要自行调用作为参数传入的Chain的proceed方法(这是我觉得这个模式不够爽的一点)。
程序员在调用了proceed方法后会怎么样呢?通过之前的代码我们看到,每一个Interceptor都被封装到ChainAdapter里,那么intercept方法中的Chain实际就只能是这个ChainAdapter,ChainAdapter的proceed方法又封装下一个Interceptor ,然后调用intercept...递归调用形成了,直到某个Interceptor的intercept方法中没有调用Chain的proceed方法为止。不是说好了,程序员都要遵守在intercept中调用Chain的proceed方法这一约定吗!对的,只有一个Interceptor 可以不遵守这个约定,那就是之前提到的IOInterceptor。下面我们就来看看IOInterceptor:
<pre>
private class IOInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) {
final Request request = chain.request();
return getImageFromFile(request.fileName);
}
private Response getImageFromFile(String fileName) {
Response respose = new Response();
String[] strs = fileName.split("\");
respose.setImage(strs[strs.length - 1]);
return respose;
}
}
</pre>
它是个私有类,所以并没有违反约定,程序员不知道它的存在。这个类也很简单,没什么好说的了。再来看看我写的LogInterceptor。拿它和IOInterceptor 做个对比。
<pre>
public class LogInterceptor implements Interceptor {

@Override
public Response intercept(Chain chain) {
    
    final Request request = chain.request();
    
    System.out.println("Request image file:" + request.fileName);
    
    final Response response = chain.proceed(request);
    
    System.out.println(request.fileName + " size:" + response.getImageSize());
    
    return response;
}

}
</pre>
在intercept方法中,首先从chain中取出Request进行打印,然后按约定调用proceed方法,接着再打印proceed方法返回的Response,最后返回Response。回头去看IOInterceptor的intercept方法,它没有调用chain.proceed,因为它是链条的最后一环,所以是时候结束了。
到这里InterceptorChain模式的整个运行过程就讲完了。最后来模拟一下ImageLoader的使用。
<pre>
Request request = new Request();

request.fileName = "images" + File.separator + "icon.png";

ImageLoader loader = new ImageLoader();
loader.setRequest(request);

loader.addInterceptor(new LogInterceptor());
loader.addInterceptor(new CountInterceptor());

Response response = loader.load();

// Do some thing for response
System.out.println("handle " + response.getImage());
</pre>

每个Interceptor各司其职,还能灵活的组合使用,是不是很不错。

(有需要源码的请留言。java代码)

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

推荐阅读更多精彩内容

  • 缓存的一般思路 下面是我理解的网络请求框架的缓存基本实现。大致的过程是有缓存用缓存的数据,没缓存发起http请求取...
    原件阅读 2,791评论 3 12
  • 关于okhttp是一款优秀的网络请求框架,关于它的源码分析文章有很多,这里分享我在学习过程中读到的感觉比较好的文章...
    蕉下孤客阅读 3,593评论 2 38
  • 这篇文章主要讲 Android 网络请求时所使用到的各个请求库的关系,以及 OkHttp3 的介绍。(如理解有误,...
    小庄bb阅读 1,139评论 0 4
  • 简介 目前在HTTP协议请求库中,OKHttp应当是非常火的,使用也非常的简单。网上有很多文章写了关于OkHttp...
    第八区阅读 1,368评论 1 5
  • 1.阳光 遥望着海南岛的丘陵,白云黑山看着也不赖。 光在这里是某种生活必需品,刺痛感强烈,却可以烘干入夜潮湿的影子...
    lesley氤阅读 390评论 0 1