天天看點

java攔截器封裝成架構,基于retrofit的網絡架構的終極封裝(二)-與retrofit的對接與解耦,以及遇到的坑...

在上一篇基于retrofit的網絡架構的終極封裝(一)中介紹了頂層api的設計.這裡再沿着代碼走向往裡說.

由于這裡講的是retrofit的封裝性使用,是以一些retrofit基礎性的使用和配置這裡就不講了.

參數怎麼傳遞到retrofit層的?

所有網絡請求相關的參數和配置全部通過第一層的api和鍊式調用封裝到了ConfigInfo中,最後在start()方法中調用retrofit層,開始網絡請求.

@Override

public ConfigInfo start(ConfigInfo configInfo) {

String url = Tool.appendUrl(configInfo.url, isAppendUrl());//組拼baseUrl和urltail

configInfo.url = url;

configInfo.listener.url = url;

//todo 這裡token還可能在請求頭中,應加上此類情況的自定義.

if (configInfo.isAppendToken){

Tool.addToken(configInfo.params);

}

if (configInfo.loadingDialog != null && !configInfo.loadingDialog.isShowing()){

try {//預防badtoken最簡便和直接的方法

configInfo.loadingDialog.show();

}catch (Exception e){

}

}

if (getCache(configInfo)){//異步,去拿緩存--隻針對String類型的請求

return configInfo;

}

T request = generateNewRequest(configInfo);//根據類型生成/執行不同的請求對象

return configInfo;

}

分類生成/執行各類請求:

private T generateNewRequest(ConfigInfo configInfo) {

int requestType = configInfo.type;

switch (requestType){

case ConfigInfo.TYPE_STRING:

case ConfigInfo.TYPE_JSON:

case ConfigInfo.TYPE_JSON_FORMATTED:

return newCommonStringRequest(configInfo);

case ConfigInfo.TYPE_DOWNLOAD:

return newDownloadRequest(configInfo);

case ConfigInfo.TYPE_UPLOAD_WITH_PROGRESS:

return newUploadRequest(configInfo);

default:return null;

}

}

是以,對retrofit的使用,隻要實作以下三個方法就行了:

如果切換到volley或者其他網絡架構,也是實作這三個方法就好了.

newCommonStringRequest(configInfo),

newDownloadRequest(configInfo);

newUploadRequest(configInfo)

String類請求在retrofit中的封裝:

@Override

protected Call newCommonStringRequest(final ConfigInfo configInfo) {

Call call;

if (configInfo.method == HttpMethod.GET){

call = service.executGet(configInfo.url,configInfo.params);

}else if (configInfo.method == HttpMethod.POST){

if(configInfo.paramsAsJson){//參數在請求體以json的形式發出

String jsonStr = MyJson.toJsonStr(configInfo.params);

Log.e("dd","jsonstr request:"+jsonStr);

RequestBody body = RequestBody.create(MediaType.parse("application/json;charset=UTF-8"), jsonStr);

call = service.executeJsonPost(configInfo.url,body);

}else {

call = service.executePost(configInfo.url,configInfo.params);

}

}else {

configInfo.listener.onError("不是get或post方法");//暫時不考慮其他方法

call = null;

return call;

}

configInfo.tagForCancle = call;

call.enqueue(new Callback() {

@Override

public void onResponse(Call call, final Response response) {

if (!response.isSuccessful()){

configInfo.listener.onCodeError("http錯誤碼為:"+response.code(),response.message(),response.code());

Tool.dismiss(configInfo.loadingDialog);

return;

}

String string = "";

try {

string = response.body().string();

Tool.parseStringByType(string,configInfo);

Tool.dismiss(configInfo.loadingDialog);

} catch (final IOException e) {

e.printStackTrace();

configInfo.listener.onError(e.toString());

Tool.dismiss(configInfo.loadingDialog);

}

}

@Override

public void onFailure(Call call, final Throwable t) {

configInfo.listener.onError(t.toString());

Tool.dismiss(configInfo.loadingDialog);

}

});

return call;

}

service中通用方法的封裝

既然要封裝,肯定就不能用retrofit的正常用法:ApiService接口裡每個接口文檔上的接口都寫一個方法,而是應該用QueryMap/FieldMap注解,接受一個以Map形式封裝好的鍵值對.這個與我們上一層的封裝思路和形式都是一樣的.

@GET()

Call executGet(@Url String url, @QueryMap Map maps);

@FormUrlEncoded

@POST()

Call executePost(@Url String url, @FieldMap Map map);

@POST()

Call executeJsonPost(@Url String url, @Body RequestBody body);

post參數體以json的形式發出時需要注意:

retrofit其實有請求時傳入一個javabean的注解方式,确實可以在架構内部轉換成json.但是不适合封裝.

其實很簡單,搞清楚以json形式發出參數的本質: 請求體中的json本質上還是一個字元串.那麼可以将Map攜帶過來的參數轉成json字元串,然後用RequestBody包裝一層就好了:

String jsonStr = MyJson.toJsonStr(configInfo.params);

RequestBody body = RequestBody.create(MediaType.parse("application/json;charset=UTF-8"), jsonStr);

call = service.executeJsonPost(configInfo.url,body);

不采用retrofit的json轉換功能:

Call的泛型不能采用二次泛型的形式--retrofit架構不接受:

@GET()

Call> getStandradJson(@Url String url, @QueryMap Map maps);

//注:BaseNetBean就是三個标準字段的json:

public class BaseNetBean{

public int code;

public String msg;

public T data;

}

這樣寫會抛出異常:

報的錯誤

Method return type must not include a type variable or wildcard: retrofit2.Call

JakeWharton的回複:

You cannot. Type information needs to be fully known at runtime in order for deserialization to work.

因為上面的原因,我們隻能通過retrofit發請求,傳回一個String,自己去解析.但這也有坑:

1.不能寫成下面的形式:

@GET()

Call executGet(@Url String url, @QueryMap Map maps);

你以為指定泛型為String它就傳回String,不,你還太年輕了.

這裡的泛型,意思是,使用retrofit内部的json轉換器,将response裡的資料轉換成一個實體類xxx,比如UserBean之類的,而String類明顯不是一個有效的實體bean類,自然轉換失敗.

是以,要讓retrofit不适用内置的json轉換功能,你應該直接指定類型為ResponseBody:

@GET()

Call executGet(@Url String url, @QueryMap Map maps);

2.既然不采用retrofit内部的json轉換功能,那就要在回調那裡自己拿到字元串,用自己的json解析了.那麼坑又來了:

泛型擦除:

回調接口上指定泛型,在回調方法裡直接拿到泛型,這是在java裡很常見的一個泛型接口設計:

public abstract class MyNetListener{

public abstract void onSuccess(T response,String resonseStr);

....

}

//使用:

call.enqueue(new Callback() {

@Override

public void onResponse(Call call, final Response response) {

String string = response.body().string();

Gson gson = new Gson();

Type objectType = new TypeToken() {}.getType();

final T bean = gson.fromJson(string,objectType);

configInfo.listener.onSuccess(bean,string);

...

}

...

}

但是,抛出異常:

java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to xxx

這是因為在運作過程中,通過泛型傳入的類型T丢失了,是以無法轉換,這叫做泛型擦除:.

要解析的話,還是老老實實傳入javabean的class吧.是以在最頂層的API裡,有一個必須傳的Class clazz:

postStandardJson(String url, Map map, Class clazz, MyNetListener listener)

綜上,我們需要傳入class對象,完全自己去解析json.解析已封裝成方法.也是根據三個不同的小類型(字元串,一般json,标準json)

這裡處理緩存時,如果要緩存内容,當然是緩存成功的内容,失敗的就不必緩存了.

Tool.parseStringByType(string,configInfo);

public static void parseStringByType(final String string, final ConfigInfo configInfo) {

switch (configInfo.type){

case ConfigInfo.TYPE_STRING:

//緩存

cacheResponse(string, configInfo);

//處理結果

configInfo.listener.onSuccess(string, string);

break;

case ConfigInfo.TYPE_JSON:

parseCommonJson(string,configInfo);

break;

case ConfigInfo.TYPE_JSON_FORMATTED:

parseStandJsonStr(string, configInfo);

break;

}

}

json解析架構選擇,gson,fastjson随意,不過最好也是自己再包一層api:

public static T parseObject(String str,Class clazz){

// return new Gson().fromJson(str,clazz);

return JSON.parseObject(str,clazz);

}

注意區分傳回的是jsonObject還是jsonArray,有不同的解析方式和回調.

private static void parseCommonJson( String string, ConfigInfo configInfo) {

if (isJsonEmpty(string)){

configInfo.listener.onEmpty();

}else {

try{

if (string.startsWith("{")){

E bean = MyJson.parseObject(string,configInfo.clazz);

configInfo.listener.onSuccessObj(bean ,string,string,0,"");

cacheResponse(string, configInfo);

}else if (string.startsWith("[")){

List beans = MyJson.parseArray(string,configInfo.clazz);

configInfo.listener.onSuccessArr(beans,string,string,0,"");

cacheResponse(string, configInfo);

}else {

configInfo.listener.onError("不是标準json格式");

}

}catch (Exception e){

e.printStackTrace();

configInfo.listener.onError(e.toString());

}

}

}

标準json的解析:

三個字段對應的資料直接用jsonObject.optString來取:

JSONObject object = null;

try {

object = new JSONObject(string);

} catch (JSONException e) {

e.printStackTrace();

configInfo.listener.onError("json 格式異常");

return;

}

String key_data = TextUtils.isEmpty(configInfo.key_data) ? NetDefaultConfig.KEY_DATA : configInfo.key_data;

String key_code = TextUtils.isEmpty(configInfo.key_code) ? NetDefaultConfig.KEY_CODE : configInfo.key_code;

String key_msg = TextUtils.isEmpty(configInfo.key_msg) ? NetDefaultConfig.KEY_MSG : configInfo.key_msg;

final String dataStr = object.optString(key_data);

final int code = object.optInt(key_code);

final String msg = object.optString(key_msg);

注意,optString後字元串為空的判斷:一個字段為null時,optString的結果是字元串"null"而不是null

public static boolean isJsonEmpty(String data){

if (TextUtils.isEmpty(data) || "[]".equals(data)

|| "{}".equals(data) || "null".equals(data)) {

return true;

}

return false;

}

然後就是相關的code情況的處理和回調:

狀态碼為未登入時,執行自動登入的邏輯,自動登入成功後再重發請求.登入不成功才執行unlogin()回調.

注意data字段可能是一個普通的String,而不是json.

private static void parseStandardJsonObj(final String response, final String data, final int code,

final String msg, final ConfigInfo configInfo){

int codeSuccess = configInfo.isCustomCodeSet ? configInfo.code_success : BaseNetBean.CODE_SUCCESS;

int codeUnFound = configInfo.isCustomCodeSet ? configInfo.code_unFound : BaseNetBean.CODE_UN_FOUND;

int codeUnlogin = configInfo.isCustomCodeSet ? configInfo.code_unlogin : BaseNetBean.CODE_UNLOGIN;

if (code == codeSuccess){

if (isJsonEmpty(data)){

if(configInfo.isResponseJsonArray()){

configInfo.listener.onEmpty();

}else {

configInfo.listener.onError("資料為空");

}

}else {

try{

if (data.startsWith("{")){

final E bean = MyJson.parseObject(data,configInfo.clazz);

configInfo.listener.onSuccessObj(bean ,response,data,code,msg);

cacheResponse(response, configInfo);

}else if (data.startsWith("[")){

final List beans = MyJson.parseArray(data,configInfo.clazz);

configInfo.listener.onSuccessArr(beans,response,data,code,msg);

cacheResponse(response, configInfo);

}else {//如果data的值是一個字元串,而不是标準json,那麼直接傳回

if (String.class.equals(configInfo.clazz) ){//此時,E也應該是String類型.如果有誤,會抛出到下面catch裡

configInfo.listener.onSuccess((E) data,data);

}else {

configInfo.listener.onError("不是标準的json資料");

}

}

}catch (final Exception e){

e.printStackTrace();

configInfo.listener.onError(e.toString());

return;

}

}

}else if (code == codeUnFound){

configInfo.listener.onUnFound();

}else if (code == codeUnlogin){

//自動登入

configInfo.client.autoLogin(new MyNetListener() {

@Override

public void onSuccess(Object response, String resonseStr) {

configInfo.client.resend(configInfo);

}

@Override

public void onError(String error) {

super.onError(error);

configInfo.listener.onUnlogin();

}

});

}else {

configInfo.listener.onCodeError(msg,"",code);

}

}

檔案下載下傳

先不考慮多線程下載下傳和斷點續傳的問題,就單單檔案下載下傳而言,用retrofit寫還是挺簡單的

1.讀寫的逾時時間的設定:

不能像上面字元流類型的請求一樣設定多少s,而應該設為0,也就是不限時:

OkHttpClient client=httpBuilder.readTimeout(0, TimeUnit.SECONDS)

.connectTimeout(30, TimeUnit.SECONDS).writeTimeout(0, TimeUnit.SECONDS) //設定逾時

2.接口需要聲明為流式下載下傳:

@Streaming //流式下載下傳,不加這個注解的話,會整個檔案位元組數組全部加載進記憶體,可能導緻oom

@GET

Call download(@Url String fileUrl);

3.聲明了流式下載下傳後,就能從回調而來的ResponseBody中拿到輸入流(body.byteStream()),然後開子線程寫到本地檔案中去.

這裡用的是一個異步任務架構,其實用Rxjava更好.

SimpleTask simple = new SimpleTask() {

@Override

protected Boolean doInBackground() {

return writeResponseBodyToDisk(response.body(),configInfo.filePath);

}

@Override

protected void onPostExecute(Boolean result) {

Tool.dismiss(configInfo.loadingDialog);

if (result){

configInfo.listener.onSuccess(configInfo.filePath,configInfo.filePath);

}else {

configInfo.listener.onError("檔案下載下傳失敗");

}

}

};

simple.execute();

進度回調的兩種實作方式

最簡單的,網絡流寫入到本地檔案時,獲得進度(writeResponseBodyToDisk方法裡)

byte[] fileReader = new byte[4096];

long fileSize = body.contentLength();

long fileSizeDownloaded = 0;

inputStream = body.byteStream();

outputStream = new FileOutputStream(futureStudioIconFile);

while (true) {

int read = inputStream.read(fileReader);

if (read == -1) {

break;

}

outputStream.write(fileReader, 0, read);

fileSizeDownloaded += read;

Log.d("io", "file download: " + fileSizeDownloaded + " of " + fileSize);// 這裡也可以實作進度監聽

}

利用okhttp的攔截器

1.添加下載下傳時更新進度的攔截器

okHttpClient .addInterceptor(new ProgressInterceptor())

2.ProgressInterceptor:實作Interceptor接口的intercept方法,攔截網絡響應

@Override

public Response intercept(Interceptor.Chain chain) throws IOException{

Response originalResponse = chain.proceed(chain.request());

return originalResponse.newBuilder().body(new ProgressResponseBody(originalResponse.body(),chain.request().url().toString())).build();

}

3 ProgressResponseBody: 繼承 ResponseBody ,在内部網絡流傳輸過程中讀取進度:

public class ProgressResponseBody extends ResponseBody {

private final ResponseBody responseBody;

private BufferedSource bufferedSource;

private String url;

public ProgressResponseBody(ResponseBody responseBody,String url) {

this.responseBody = responseBody;

this.url = url;

}

@Override

public MediaType contentType() {

return responseBody.contentType();

}

@Override

public long contentLength() {

return responseBody.contentLength();

}

@Override

public BufferedSource source() {

if (bufferedSource == null) {

bufferedSource = Okio.buffer(source(responseBody.source()));

}

return bufferedSource;

}

long timePre = 0;

long timeNow;

private Source source(final Source source) {

return new ForwardingSource(source) {

long totalBytesRead = 0L;

@Override

public long read(Buffer sink, long byteCount) throws IOException {

long bytesRead = super.read(sink, byteCount);

totalBytesRead += bytesRead != -1 ? bytesRead : 0;

timeNow = System.currentTimeMillis();

if (timeNow - timePre > NetDefaultConfig.PROGRESS_INTERMEDIATE || totalBytesRead == responseBody.contentLength()){//至少300ms才更新一次狀态

timePre = timeNow;

EventBus.getDefault().post(new ProgressEvent(totalBytesRead,responseBody.contentLength(),

totalBytesRead == responseBody.contentLength(),url));

}

return bytesRead;

}

};

}

}

進度資料以event的形式傳出(采用Eventbus),在listener中接收

一般進度資料用于更新UI,是以最好設定資料傳出的時間間隔,不要太頻繁:

事件的發出:

timeNow = System.currentTimeMillis();

if (timeNow - timePre > NetDefaultConfig.PROGRESS_INTERMEDIATE || totalBytesRead == responseBody.contentLength()){//至少300ms才更新一次狀态

timePre = timeNow;

EventBus.getDefault().post(new ProgressEvent(totalBytesRead,responseBody.contentLength(), totalBytesRead == responseBody.contentLength(),url));

}

事件的接收(MyNetListener對象中):

注意: MyNetListener與url綁定,以防止不同下載下傳間的進度錯亂.

@Subscribe(threadMode = ThreadMode.MAIN)

public void onMessage(ProgressEvent event){

if (event.url.equals(url)){

onProgressChange(event.totalLength,event.totalBytesRead);

if (event.done){

unRegistEventBus();

onFinish();

}

}

}

檔案上傳

檔案上傳相對于普通post請求有差別,你非常需要了解http檔案上傳的協定:

java攔截器封裝成架構,基于retrofit的網絡架構的終極封裝(二)-與retrofit的對接與解耦,以及遇到的坑...

1.送出一個表單,如果包含檔案上傳,那麼必須指定類型為multipart/form-data.這個在retrofit中通過@Multipart注解指定即可.

2.表單中還有其他鍵值對也要一同傳遞,在retrofit中通過@QueryMap以map形式傳入,這個與普通post請求一樣

3.伺服器接收檔案的字段名,以及上傳的檔案路徑,通過@PartMap以map形式傳入.這裡的字段名對應請求體中Content-Disposition中的name字段的值.大多數伺服器預設是file.(因為SSH架構預設的是file?)

4.請求體的content-type用于辨別檔案的具體MIME類型.在retrofit中,是在建構請求體RequestBody時指定的.需要我們指定.

那麼如何獲得一個檔案的MIMe類型呢?讀檔案的字尾名的話,不靠譜.最佳方式是讀檔案頭,從檔案頭中拿到MIME類型.不用擔心,Android有相關的api的

綜上,相關的封裝如下:

同下載下傳一樣,配置httpclient時,讀和寫的逾時時間都要置為0

OkHttpClient client=httpBuilder.readTimeout(0, TimeUnit.SECONDS)

.connectTimeout(0, TimeUnit.SECONDS).writeTimeout(0, TimeUnit.SECONDS) //設定逾時

ApiService中通用接口的定義

@POST()

@Multipart

Call uploadWithProgress(@Url String url,@QueryMap Map options,@PartMap Map fileParameters) ;

key-filepath到key-RequestBody的轉換:

這裡的回調就不用開背景線程了,因為流是在請求體中,而retrofit已經幫我們搞定了請求過程的背景執行.

protected Call newUploadRequest(final ConfigInfo configInfo) {

if (serviceUpload == null){

initUpload();

}

configInfo.listener.registEventBus();

Map requestBodyMap = new HashMap<>();

if (configInfo.files != null && configInfo.files.size() >0){

Map files = configInfo.files;

int count = files.size();

if (count>0){

Set> set = files.entrySet();

for (Map.Entry entry : set){

String key = entry.getKey();

String value = entry.getValue();

File file = new File(value);

String type = Tool.getMimeType(file);//拿到檔案的實際類型

Log.e("type","mimetype:"+type);

UploadFileRequestBody fileRequestBody = new UploadFileRequestBody(file, type,configInfo.url);

requestBodyMap.put(key+"\"; filename=\"" + file.getName(), fileRequestBody);

}

}

}

Call call = service.uploadWithProgress(configInfo.url,configInfo.params,requestBodyMap);

注意,RequestBody中的content-type不是multipart/form-data,而是檔案的實際類型.multipart/form-data是請求頭中的檔案上傳的統一type.

public class UploadFileRequestBody extends RequestBody {

private RequestBody mRequestBody;

private BufferedSink bufferedSink;

private String url;

public UploadFileRequestBody(File file,String mimeType,String url) {

// this.mRequestBody = RequestBody.create(MediaType.parse("multipart/form-data"), file);

this.mRequestBody = RequestBody.create(MediaType.parse(mimeType), file);

this.url = url;

}

@Override

public MediaType contentType() {

return mRequestBody.contentType();

}

進度的回調

封裝在UploadFileRequestBody中,無需通過okhttp的攔截器實作,因為可以在建構RequestBody的時候就包裝好(看上面代碼),就沒必要用攔截器了.

最後的話

到這裡,主要的請求執行和回調就算講完了,但還有一些,比如緩存控制,登入狀态的維護,以及cookie管理,請求的取消,gzip壓縮,本地時間校準等等必需的輔助功能的實作和維護,這些将在下一篇文章進行解析.

代碼