今天我們來談談客戶端對通訊協議的處理,主要分爲三部分:約定響應數據格式,響應數據的自動映射以及錯誤處理三部分。由於數據協議採用json的居多,因此我們在此基礎上進行說明。
約定響應數據格式
協議格式
通常來說,你拿到的設計文檔中會存在通信協議的說明,對於客戶端來說,一個良好的通信協議需要能描述操作狀態(操作碼+操作提示)以操作結果,因此,常見的響應數據的格式如下:
{
"code": 0,
"msg": "正常",
"data": {
"id": 1,
"account": "121313",
"accountName": "alipay",
"income": "600.000000"
}
}
code定義
code爲我們自定義的操作狀態碼,首先來看我們常用的定義:
code | 說明 |
---|---|
0 | 操作成功的消息提示 |
1 | 客戶端認證失敗,一般是客戶端被惡意修改 |
2 | 用戶認證失敗 |
3 | 提交參數錯誤:參數缺失、參數名不對等 |
4 | 提交參數校驗失敗,一般是提交給服務端的數據格式有誤,多發生在表單提交的場景中 |
5 | 自定義錯誤,服務端發生不可恢復的錯誤等 |
msg定義
msg爲服務器端返回的操作信息。
無論操作成功與否,客戶端都應該根據業務給出準確的提示,客戶端則根據實際情況選擇展示與否。
data 定義
data則是請求返回的具體內容,通常data根據請求接口的不同最終會被解析成不同的實體類。
示例
下面我們以獲取消息列表和消息詳情兩個接口返回的響應數據作爲示例:
消息列表:
{
"code": 0,
"data": {
"list": [
{
"content": "你參加的活動已經開始了...",
"createtime": "2016-09-23 16:44:02",
"id": "4480",
"status": 0,
"title": "活動開始",
"type": "1"
},
{
"content": "你參加的活動已經結束...",
"createtime": "2016-09-19 14:30:02",
"id": "4444",
"status": 0,
"title": "活動結束",
"type": "1"
}
],
"total": 2
},
"msg": "正常"
}
消息詳情
{
"code": 0,
"data": {
"detail":
{
"content": "你參加的活動已經開始了,請準時到你的活動中去執行",
"createtime": "2016-09-23 16:44:02",
"id": "4480",
"status": 0,
"title": "活動開始",
"type": "1"
},
},
"msg": "正常"
}
響應數據映射實體數據模型
當我們接受到如上格式的響應數據時,下面便是考慮如何應用的問題,也就是如何將協議轉換?是在獲取響應的時候自動轉換還是手動轉換?轉換成java實體類還是String?
“偷懶”是程序員的天性,我們當然不希望花費時間在這種無創造性的工作上,所以我們考慮在收到響應的時候直接將其轉換爲java實體類。
確定了我們的目標之後,接下來,首要任務是對數據協議進行抽象?什麼叫做數據協議抽象?
所謂的數據協議抽象就是根據聚合性,通用性,隔離性三原則將整個數據協議進行切分複用,以便更好的映射成我們需要的數據模型。
我們對剛纔約定的數據協議格式進行協議抽象後,可以拿到類似以下的實體模型:
public class Result<T> {
private int code;
private String msg;
private T data;
//...set和get方法
}
Result做爲所有響應模型的公共基類,其中的code,msg,data分別用來映射我們通信協議。其中,泛型化的data確保接受不同的實體模型,可以看出,我們通過數據協議抽象之後,最終得到了一個良好的數據模型。
爲了下面的需要我們一同將消息列表和消息詳情的實體類放上來:
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方法
}
現在來看看我們理想的獲取消息列表和獲取消息詳情的接口應該是什麼樣的:
@GET("/user/message/list")
Call<Result<MessageList>> getMessageList(@Query("page") int page);
@GET("/user/message")
Call<Result<Message>> getMessage(@Query("mid") int mid);
結合我們上面所述,我們希望每個api最後返回給我們的都是Result
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,響應體被映射成實體數據模型。
上面我們談到了通訊協議格式以及如何利用retrofit的Converter實現協議和實體之間的自動映射。此時我們調用任何服務接口其使用大體如下,以獲取消息列表接口爲例:
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講到這裏已經足夠了,在此基礎上在進行二次封裝形成自己的框架也很不錯。但是由於RxJava發展確實不錯,因此retrofit引入對rxjava的支持,二者的有效結合才能發揮更強大的力量。
不瞭解RxJava同學可以就此打住或者先去了解相關資料。rxjava並無多大難度,明白理論之後再加上多練即可。對rxjava實現感興趣的童鞋可以參看去年寫的教你寫響應式框架
再來說說,在新項目開始的時候,我爲什麼選擇引入rxjava,不引入不行麼?
我並未考慮引入rxjava的原因我只想使用retrofit這個網絡請求庫代替原有的async-http-client,後面發現引入rxjava能夠非常容易的幫助我們進行線程切換以及合理的處理網絡異常。
如何引入rxjava?
引入rxjava非常簡單,需要添加以下依賴:
compile 'io.reactivex:rxjava:1.1.0'
compile 'io.reactivex:rxandroid:1.1.0'
接下來還需要引入adapter來將retrofit中Call轉換爲rxjava中的Observable:
compile 'com.squareup.retrofit2:adapter-rxjava:2.0.2'
最後需要在代碼中啓用該adapter:
Retrofit.Builder mBuilder = new
Retrofit.Builder().addCallAdapterFactory(RxJavaCallAdapterFactory.create())
現在看引入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統一錯誤處理
我們對異常總是感覺麻煩,在客戶端開發中,網絡異常更是重中之重。現在讓我們回到開始,來看這段代碼:
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()好像不是那麼純粹,既要承擔正常業務邏輯還是處理異常的錯誤邏輯,換言之,onNext()幹了onError()的事情,這看起來很不協調?另外,如果每個接口都要這麼做,不但繁瑣而且還會長城很多重複性的代碼,長久以往,整個項目的工程質量將無法把控。
實際上,我們希望所有的異常都是統一在onError()中進行處理。那麼這裏我們急需要明確下異常的範圍:響應數據中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;
/** 用戶授權失敗*/
int ERROR_USER_AUTHORIZED = 2;
/** 請求參數錯誤*/
int ERROR_REQUEST_PARAM = 3;
/** 參數檢驗不通過 */
int ERROR_PARAM_CHECK = 4;
/** 自定義錯誤*/
int ERROR_OTHER = 10;
/** 無網絡連接*/
int ERROR_NO_INTERNET = 11;
}
現在問題就是如何將ApiException納入到rxjava的onError()當中,也就是在哪裏拋出該類異常。retrofit中的Converter承擔了協議映射的功能,而ApiException只有在映射之後才能拋出,因此Converter是拋出ApiException的切入點。
先來對Converter接口有個初步的瞭解,其源碼如下:
public interface Converter<F, T> {
T convert(F value) throws IOException;
//用於創建Converter實例
abstract class Factory {
//響應體轉換
public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations,
Retrofit retrofit) {
return null;
}
//請求體轉換
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的實現看起.
其結構非常簡單:GsonConverterFactory,
GsonRequestBodyConverter及GsonResponseBodyConverter,分別來看一下起源碼:
GsonRequestBodyConverter源碼:
//請求體轉換
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源碼:
//響應體轉換
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源碼:
//轉換器
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);//創建響應轉換器
}
@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);//創建請求轉換器
}
}
到這裏我們已經有思路了:我們需要在修改GsonResponseBodyConverter,在其中加入拋出ApiException的代碼.仿照converter-gson結構,我們自定義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);
//關注的重點,自定義響應碼中非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())
通過上面的改進,我們已經成功的將所有異常處理點轉移至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
}
}
});
到現在,已經解決了統一異常處理點的問題,接下來便是要解決公共異常。不難發現,對於大部分網絡異常來說,我們處理策略是相同的,因此我們希望抽取公共異常處理。除此之外,在網絡真正請求之前,需要對網絡進行判斷,無網絡的情況下直接拋出響應異常。
這時候就需要自定BaseSubscriber,並在其中做相關的處理:
public class BaseSubscriber<T> extends Subscriber<T> {
private Context mContext;
public BaseSubscriber() {
}
public BaseSubscriber(Context context) {
mContext = context;
}
@Override
public void onStart() {
//請求開始之前,檢查是否有網絡。無網絡直接拋出異常
//另外,在你無法確定當前代碼運行在什麼線程的時候,不要將UI的相關操作放在這裏。
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();
}
}
}
現在再來看看獲取消息列表接口的使用
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()方法即可。