今天我們來談談客戶端對通訊協(xié)議的處理科汗,主要分為三部分:約定響應數(shù)據(jù)格式捎迫,響應數(shù)據(jù)的自動映射以及錯誤處理三部分。由于數(shù)據(jù)協(xié)議采用json的居多故源,因此我們在此基礎(chǔ)上進行說明。
歡迎關(guān)注我的博客代碼之道汞贸,編程之法绳军,不定時更新
約定響應數(shù)據(jù)格式
協(xié)議格式
通常來說,你拿到的設計文檔中會存在通信協(xié)議的說明著蛙,對于客戶端來說删铃,一個良好的通信協(xié)議需要能描述操作狀態(tài)(操作碼+操作提示)以操作結(jié)果,因此踏堡,常見的響應數(shù)據(jù)的格式如下:
{
"code": 0,
"msg": "正常",
"data": {
"id": 1,
"account": "121313",
"accountName": "alipay",
"income": "600.000000"
}
}
code定義
code為我們自定義的操作狀態(tài)碼猎唁,首先來看我們常用的定義:
code | 說明 | |
---|---|---|
0 | 操作成功的消息提示 | |
1 | 客戶端認證失敗,一般是客戶端被惡意修改 | |
2 | 用戶認證失敗 | |
3 | 提交參數(shù)錯誤:參數(shù)缺失顷蟆、參數(shù)名不對等 | |
4 | 提交參數(shù)校驗失敗诫隅,一般是提交給服務端的數(shù)據(jù)格式有誤,多發(fā)生在表單提交的場景中 | |
5 | 自定義錯誤帐偎,服務端發(fā)生不可恢復的錯誤等 |
msg定義
msg為服務器端返回的操作信息逐纬。
無論操作成功與否,客戶端都應該根據(jù)業(yè)務給出準確的提示削樊,客戶端則根據(jù)實際情況選擇展示與否豁生。
data 定義
data則是請求返回的具體內(nèi)容,通常data根據(jù)請求接口的不同最終會被解析成不同的實體類漫贞。
示例
下面我們以獲取消息列表和消息詳情兩個接口返回的響應數(shù)據(jù)作為示例:
消息列表:
{
"code": 0,
"data": {
"list": [
{
"content": "你參加的活動已經(jīng)開始了...",
"createtime": "2016-09-23 16:44:02",
"id": "4480",
"status": 0,
"title": "活動開始",
"type": "1"
},
{
"content": "你參加的活動已經(jīng)結(jié)束...",
"createtime": "2016-09-19 14:30:02",
"id": "4444",
"status": 0,
"title": "活動結(jié)束",
"type": "1"
}
],
"total": 2
},
"msg": "正常"
}
消息詳情
{
"code": 0,
"data": {
"detail":
{
"content": "你參加的活動已經(jīng)開始了,請準時到你的活動中去執(zhí)行",
"createtime": "2016-09-23 16:44:02",
"id": "4480",
"status": 0,
"title": "活動開始",
"type": "1"
},
},
"msg": "正常"
}
響應數(shù)據(jù)映射實體數(shù)據(jù)模型
當我們接受到如上格式的響應數(shù)據(jù)時甸箱,下面便是考慮如何應用的問題,也就是如何將協(xié)議轉(zhuǎn)換迅脐?是在獲取響應的時候自動轉(zhuǎn)換還是手動轉(zhuǎn)換芍殖?轉(zhuǎn)換成java實體類還是String?
“偷懶”是程序員的天性谴蔑,我們當然不希望花費時間在這種無創(chuàng)造性的工作上豌骏,所以我們考慮在收到響應的時候直接將其轉(zhuǎn)換為java實體類。
確定了我們的目標之后隐锭,接下來窃躲,首要任務是對數(shù)據(jù)協(xié)議進行抽象?什么叫做數(shù)據(jù)協(xié)議抽象钦睡?
所謂的數(shù)據(jù)協(xié)議抽象就是根據(jù)聚合性蒂窒,通用性,隔離性三原則將整個數(shù)據(jù)協(xié)議進行切分復用,以便更好的映射成我們需要的數(shù)據(jù)模型刘绣。
我們對剛才約定的數(shù)據(jù)協(xié)議格式進行協(xié)議抽象后樱溉,可以拿到類似以下的實體模型:
public class Result<T> {
private int code;
private String msg;
private T data;
//...set和get方法
}
Result做為所有響應模型的公共基類,其中的code纬凤,msg福贞,data分別用來映射我們通信協(xié)議。其中停士,泛型化的data確保接受不同的實體模型挖帘,可以看出,我們通過數(shù)據(jù)協(xié)議抽象之后恋技,最終得到了一個良好的數(shù)據(jù)模型拇舀。
為了下面的需要我們一同將消息列表和消息詳情的實體類放上來:
public class message{
private String content;
private String createtime;
private String id;
private int status;
private String title;
private String type;
//...set和get方法
}
public class messageList {
private int total;
private List<Message> list;
//...set和get方法
}
現(xiàn)在來看看我們理想的獲取消息列表和獲取消息詳情的接口應該是什么樣的:
@GET("/user/message/list")
Call<Result<MessageList>> getMessageList(@Query("page") int page);
@GET("/user/message")
Call<Result<Message>> getMessage(@Query("mid") int mid);
結(jié)合我們上面所述,我們希望每個api最后返回給我們的都是Result<?>這樣的實體類蜻底。在傳統(tǒng)的其他請求框架中骄崩,我們需要在拿到請求之后需要自己手動添加轉(zhuǎn)換代碼,或是使用傳統(tǒng)的JSONObject,或是Gson薄辅,亦或是fastjson等要拂。而retrofit中已經(jīng)為我們封裝好了這部分(Converter),并實現(xiàn)提供了幾個常用Converter:
- Gson: com.squareup.retrofit2:converter-gson
- Jackson: com.squareup.retrofit2:converter-jackson
- Moshi: com.squareup.retrofit2:converter-moshi
- Protobuf: com.squareup.retrofit2:converter-protobuf
- Wire: com.squareup.retrofit2:converter-wire
- Simple XML: com.squareup.retrofit2:converter-simplexml
- Scalars (primitives, boxed, and String): com.squareup.retrofit2:converter-scalars
在沒有使用Converter的情況下,retrofit默認發(fā)送的請求體是RequestBody站楚,而接受的響應是ResponseBody脱惰。那現(xiàn)在就看看retrofit中Converter的使用,這里以添加Gson為例。
每天添加Gson依賴的首先添加依賴:
provided 'com.google.code.gson:gson:2.7'
接下來是添加Converter依賴:
com.squareup.retrofit2:converter-gson
最后為retrofit設置Converter:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com")
.addConverterFactory(GsonConverterFactory.create())
.build();
GitHubService service = retrofit.create(GitHubService.class);
這樣窿春,我們的請求和響應由Gson進行處理:請求體(使用@Body)被映射成json拉一,響應體被映射成實體數(shù)據(jù)模型。
上面我們談到了通訊協(xié)議格式以及如何利用retrofit的Converter實現(xiàn)協(xié)議和實體之間的自動映射旧乞。此時我們調(diào)用任何服務接口其使用大體如下蔚润,以獲取消息列表接口為例:
Call<Result<MessageList>> call = ApiFactory.getUserApi().getMessageList(mCurrentPage * getPageSize(), getPageSize());
call.enqueue(new Callback<Result<MessageList>>() {
@Override
public void onResponse(Call<Result<MessageList>> call, Response<Result<MessageList>> response) {
Result<MessageList> result = response.body();
if (result.isOk()) {//操作正確
} else {//操作失敗
switch (result.getCode()) {
case 1:
break;
case 2:
break;
case 3:
break;
case 4:
break;
case 5:
break;
}
}
}
@Override
public void onFailure(Call<Result<MessageList>> call, Throwable t) {
//響應失敗
}
});
錯誤處理
引入RxJava之前哪點事
按道理說,retrofit講到這里已經(jīng)足夠了良蛮,在此基礎(chǔ)上在進行二次封裝形成自己的框架也很不錯抽碌。但是由于RxJava發(fā)展確實不錯悍赢,因此retrofit引入對rxjava的支持决瞳,二者的有效結(jié)合才能發(fā)揮更強大的力量。
不了解RxJava同學可以就此打住或者先去了解相關(guān)資料左权。rxjava并無多大難度皮胡,明白理論之后再加上多練即可。對rxjava實現(xiàn)感興趣的童鞋可以參看去年寫的教你寫響應式框架
再來說說赏迟,在新項目開始的時候屡贺,我為什么選擇引入rxjava,不引入不行么?
我并未考慮引入rxjava的原因我只想使用retrofit這個網(wǎng)絡請求庫代替原有的async-http-client甩栈,后面發(fā)現(xiàn)引入rxjava能夠非常容易的幫助我們進行線程切換以及合理的處理網(wǎng)絡異常泻仙。
如何引入rxjava?
引入rxjava非常簡單量没,需要添加以下依賴:
compile 'io.reactivex:rxjava:1.1.0'
compile 'io.reactivex:rxandroid:1.1.0'
接下來還需要引入adapter來將retrofit中Call轉(zhuǎn)換為rxjava中的Observable:
compile 'com.squareup.retrofit2:adapter-rxjava:2.0.2'
最后需要在代碼中啟用該adapter:
Retrofit.Builder mBuilder = new
Retrofit.Builder().addCallAdapterFactory(RxJavaCallAdapterFactory.create())
現(xiàn)在看引入RxJava之后接口的變化玉转,同樣還是以獲取消息列表為例:
引入之前:
@GET("/user/message/list")
Call<Result<MessageList>> getMessageList(@Query("start") int start, @Query("length") int length);
引入之后:
@GET("/user/message/list")
Observable<Result<MessageList>> getMessageList(@Query("start") int start, @Query("length") int length);
得益于retrofit良好的設計,加入對rxjava的支持對我們接口的影響非常之小殴蹄。
自定義Converter統(tǒng)一錯誤處理
我們對異尘孔ィ總是感覺麻煩,在客戶端開發(fā)中袭灯,網(wǎng)絡異常更是重中之重〈滔拢現(xiàn)在讓我們回到開始,來看這段代碼:
ApiFactory.getUserApi().getMessageList(0, 10).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<Result<MessageList>>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
//handle throwable
}
@Override
public void onNext(Result<MessageList> result) {
if (result.isOk()) {
MessageList messageList = result.getData();
//handle messageList
}else{
int code = result.getCode();
switch (code) {
case 1:
break;
case 2:
break;
case 3:
break;
case 4:
break;
case 5:
break;
}
}
}
});
看起很棒稽荧,我們用了rxjava中線程切換避免以往繁瑣的操作橘茉。但是好像不是那么完美:在rxjava中,所有的異常都是放在onError()
,而這里的onNext()好像不是那么純粹姨丈,既要承擔正常業(yè)務邏輯還是處理異常的錯誤邏輯捺癞,換言之,onNext()干了onError()的事情构挤,這看起來很不協(xié)調(diào)髓介?另外,如果每個接口都要這么做筋现,不但繁瑣而且還會長城很多重復性的代碼唐础,長久以往,整個項目的工程質(zhì)量將無法把控矾飞。
實際上一膨,我們希望所有的異常都是統(tǒng)一在onError()中進行處理。那么這里我們急需要明確下異常的范圍:響應數(shù)據(jù)中code非0的情況以及其他異常洒沦。為了更好描述code非0的情況豹绪,我們定義ApiException異常類:
public class ApiException extends RuntimeException {
private int errorCode;
public ApiException(int code, String msg) {
super(msg);
this.errorCode = code;
}
public int getErrorCode() {
return errorCode;
}
}
另外為了更好描述code,我們也將其定義成ApiErrorCode:
public interface ApiErrorCode {
/** 客戶端錯誤*/
int ERROR_CLIENT_AUTHORIZED = 1;
/** 用戶授權(quán)失敗*/
int ERROR_USER_AUTHORIZED = 2;
/** 請求參數(shù)錯誤*/
int ERROR_REQUEST_PARAM = 3;
/** 參數(shù)檢驗不通過 */
int ERROR_PARAM_CHECK = 4;
/** 自定義錯誤*/
int ERROR_OTHER = 10;
/** 無網(wǎng)絡連接*/
int ERROR_NO_INTERNET = 11;
}
現(xiàn)在問題就是如何將ApiException納入到rxjava的onError()當中申眼,也就是在哪里拋出該類異常瞒津。retrofit中的Converter承擔了協(xié)議映射的功能,而ApiException只有在映射之后才能拋出括尸,因此Converter是拋出ApiException的切入點巷蚪。
先來對Converter接口有個初步的了解,其源碼如下:
public interface Converter<F, T> {
T convert(F value) throws IOException;
//用于創(chuàng)建Converter實例
abstract class Factory {
//響應體轉(zhuǎn)換
public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations,
Retrofit retrofit) {
return null;
}
//請求體轉(zhuǎn)換
public Converter<?, RequestBody> requestBodyConverter(Type type,
Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
return null;
}
public Converter<?, String> stringConverter(Type type, Annotation[] annotations,
Retrofit retrofit) {
return null;
}
}
}
接下來,我們從retrofit提供的converter-gson的實現(xiàn)看起.
其結(jié)構(gòu)非常簡單:GsonConverterFactory濒翻,
GsonRequestBodyConverter及GsonResponseBodyConverter屁柏,分別來看一下起源碼:
GsonRequestBodyConverter源碼:
//請求體轉(zhuǎn)換
final class GsonRequestBodyConverter<T> implements Converter<T, RequestBody> {
private static final MediaType MEDIA_TYPE = MediaType.parse("application/json; charset=UTF-8");
private static final Charset UTF_8 = Charset.forName("UTF-8");
private final Gson gson;
private final TypeAdapter<T> adapter;
GsonRequestBodyConverter(Gson gson, TypeAdapter<T> adapter) {
this.gson = gson;
this.adapter = adapter;
}
@Override public RequestBody convert(T value) throws IOException {
Buffer buffer = new Buffer();
Writer writer = new OutputStreamWriter(buffer.outputStream(), UTF_8);
JsonWriter jsonWriter = gson.newJsonWriter(writer);
adapter.write(jsonWriter, value);
jsonWriter.close();
return RequestBody.create(MEDIA_TYPE, buffer.readByteString());
}
}
GsonResponseBodyConverter源碼:
//響應體轉(zhuǎn)換
final class GsonResponseBodyConverter<T> implements Converter<ResponseBody, T> {
private final TypeAdapter<T> adapter;
GsonResponseBodyConverter(TypeAdapter<T> adapter) {
this.adapter = adapter;
}
@Override public T convert(ResponseBody value) throws IOException {
try {
return adapter.fromJson(value.charStream());
} finally {
value.close();
}
}
}
GsonConverterFactory源碼:
//轉(zhuǎn)換器
public final class GsonConverterFactory extends Converter.Factory {
private final Gson gson;
public static GsonConverterFactory create() {
return create(new Gson());
}
public static GsonConverterFactory create(Gson gson) {
return new GsonConverterFactory(gson);
}
private GsonConverterFactory(Gson gson) {
if (gson == null) throw new NullPointerException("gson == null");
this.gson = gson;
}
@Override
public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations,
Retrofit retrofit) {
TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
return new GsonResponseBodyConverter<>(adapter);//創(chuàng)建響應轉(zhuǎn)換器
}
@Override
public Converter<?, RequestBody> requestBodyConverter(Type type,
Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
return new GsonRequestBodyConverter<>(gson, adapter);//創(chuàng)建請求轉(zhuǎn)換器
}
}
到這里我們已經(jīng)有思路了:我們需要在修改GsonResponseBodyConverter啦膜,在其中加入拋出ApiException的代碼.仿照converter-gson結(jié)構(gòu),我們自定義custom-converter-gson:
仿照GsonResponseBodyConverter編寫MyGsonResponseBodyConverter:
public class MyGsonResponseBodyConverter<T> implements Converter<ResponseBody, T> {
private static final Charset UTF_8 = Charset.forName("UTF-8");
private final Gson mGson;
private final TypeAdapter<T> adapter;
public MyGsonResponseBodyConverter(Gson gson, TypeAdapter<T> adapter) {
mGson = gson;
this.adapter = adapter;
}
@Override
public T convert(ResponseBody value) throws IOException {
String response = value.string();
Result re = mGson.fromJson(response, Result.class);
//關(guān)注的重點淌喻,自定義響應碼中非0的情況僧家,一律拋出ApiException異常。
//這樣裸删,我們就成功的將該異常交給onError()去處理了啸臀。
if (!re.isOk()) {
value.close();
throw new ApiException(re.getCode(), re.getMsg());
}
MediaType mediaType = value.contentType();
Charset charset = mediaType != null ? mediaType.charset(UTF_8) : UTF_8;
ByteArrayInputStream bis = new ByteArrayInputStream(response.getBytes());
InputStreamReader reader = new InputStreamReader(bis,charset);
JsonReader jsonReader = mGson.newJsonReader(reader);
try {
return adapter.read(jsonReader);
} finally {
value.close();
}
}
}
仿照GsonRequestBodyConverter編寫MyGsonRequestBodyConverter:
public class MyGsonRequestBodyConverter<T> implements Converter<T, RequestBody> {
private static final MediaType MEDIA_TYPE = MediaType.parse("application/json; charset=UTF-8");
private static final Charset UTF_8 = Charset.forName("UTF-8");
private final Gson gson;
private final TypeAdapter<T> adapter;
public MyGsonRequestBodyConverter(Gson gson, TypeAdapter<T> adapter) {
this.gson = gson;
this.adapter = adapter;
}
@Override
public RequestBody convert(T value) throws IOException {
Buffer buffer = new Buffer();
Writer writer = new OutputStreamWriter(buffer.outputStream(), UTF_8);
JsonWriter jsonWriter = gson.newJsonWriter(writer);
adapter.write(jsonWriter, value);
jsonWriter.close();
return RequestBody.create(MEDIA_TYPE, buffer.readByteString());
}
}
仿照GsonConverterFactory編寫MyGsonConverterFactory:
public class MyGsonConverterFactory extends Converter.Factory {
private final Gson gson;
private MyGsonConverterFactory(Gson gson) {
if (gson == null) throw new NullPointerException("gson == null");
this.gson = gson;
}
public static MyGsonConverterFactory create() {
return create(new Gson());
}
public static MyGsonConverterFactory create(Gson gson) {
return new MyGsonConverterFactory(gson);
}
@Override
public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
return new STGsonResponseBodyConverter<>(gson, adapter);
}
@Override
public Converter<?, RequestBody> requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
return new STGsonRequestBodyConverter<>(gson, adapter);
}
}
接下來只需要在的Retrofit中使用MyGsonConverterFactory即可:
Retrofit.Builder mBuilder = new
Retrofit.Builder().addConverterFactory(MyGsonConverterFactory.create())
//.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
通過上面的改進,我們已經(jīng)成功的將所有異常處理點轉(zhuǎn)移至onError()當中了烁落。這時乘粒,我們再來對比一下獲取消息列表接口的使用:
ApiFactory.getUserApi().getMessageList(0, 10).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<Result<MessageList>>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
if(e instanceof HttpException){
//handle
}else if(e instance of IOExcepton){
//handle
}else if(e instanceof ApiException){
ApiException exception=(ApiException)e;
int code = result.getErrorCode();
switch (code) {
case ApiErrorCode.ERROR_CLIENT_AUTHORIZED:
//handle
break;
case ApiErrorCode.ERROR_USER_AUTHORIZED:
//handle
break;
case ApiErrorCode.ERROR_REQUEST_PARAM:
//handle
break;
case ApiErrorCode.ERROR_PARAM_CHECK:
//handle
break;
case ApiErrorCode.ERROR_OTHER:
//handle
break;
case ApiErrorCode.ERROR_NO_INTERNET:
//handle
break;
}else{
//handle
}
}
@Override
public void onNext(Result<MessageList> result) {
MessageList messageList = result.getData();
//handle messageList
}
}
});
到現(xiàn)在,已經(jīng)解決了統(tǒng)一異常處理點的問題伤塌,接下來便是要解決公共異常灯萍。不難發(fā)現(xiàn),對于大部分網(wǎng)絡異常來說每聪,我們處理策略是相同的旦棉,因此我們希望抽取公共異常處理。除此之外药薯,在網(wǎng)絡真正請求之前绑洛,需要對網(wǎng)絡進行判斷,無網(wǎng)絡的情況下直接拋出響應異常童本。
這時候就需要自定BaseSubscriber真屯,并在其中做相關(guān)的處理:
public class BaseSubscriber<T> extends Subscriber<T> {
private Context mContext;
public BaseSubscriber() {
}
public BaseSubscriber(Context context) {
mContext = context;
}
@Override
public void onStart() {
//請求開始之前,檢查是否有網(wǎng)絡穷娱。無網(wǎng)絡直接拋出異常
//另外绑蔫,在你無法確定當前代碼運行在什么線程的時候,不要將UI的相關(guān)操作放在這里泵额。
if (!TDevice.hasInternet()) {
this.onError(new ApiException(ApiErrorCode.ERROR_NO_INTERNET, "network interrupt"));
return;
}
}
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
ApiErrorHelper.handleCommonError(mContext, e);
}
@Override
public void onNext(T t) {
}
}
//輔助處理異常
public class ApiErrorHelper {
public static void handleCommonError(Context context, Throwable e) {
if (e instanceof HttpException) {
Toast.makeText(context, "服務暫不可用", Toast.LENGTH_SHORT).show();
} else if (e instanceof IOException) {
Toast.makeText(context, "連接失敗", Toast.LENGTH_SHORT).show();
} else if (e instanceof ApiException) {
//ApiException處理
} else {
Toast.makeText(context, "未知錯誤", Toast.LENGTH_SHORT).show();
}
}
}
現(xiàn)在再來看看獲取消息列表接口的使用
ApiFactory.getUserApi().getMessageList(mCurrentPage * getPageSize(), getPageSize()).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new BaseSubscriber<Result<MessageList>>() {
@Override
public void onNext(Result<MessageList> result) {
MessageList messageList = result.getData();
//handle messageList
}
});
大部分接口的使用都和以上類似配深,針對個別異常處理只需要重寫onError()方法即可。