最近在看OkHttp源码时,看到了Interceptor的使用,觉得还是很巧妙的设计,所以就提炼出来,已被不时之需。暂且就称为InterceptorChain模式吧。
先来看看OkHttp中InterceptorChain的使用场景吧。
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登场了。
设想一下,你可以有一个最基本的读取图片的方法,其他的诸如日志、记录访问次数、缩放功能都能随意配置,这是不是很爽。
下面我们就看看怎么才能达到这个目的。
整体结构:
我们把加载图片的逻辑提了出来,封装在一个单独的类: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代码)