最近在看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代碼)