Retrofit响应数据及异常处理策略
今天我們來談談客戶端對通訊協議的處理,主要分為三部分:約定響應數據格式,響應數據的自動映射以及錯誤處理三部分。由于數據協議采用json的居多,因此我們在此基礎上進行說明。
約定響應數據格式
協議格式
通常來說,你拿到的設計文檔中會存在通信協議的說明,對于客戶端來說,一個良好的通信協議需要能描述操作狀態(操作碼+操作提示)以操作結果,因此,常見的響應數據的格式如下:
{"code": 0,"msg": "正常","data": {"id": 1,"account": "121313","accountName": "alipay","income": "600.000000"} }code定義
code為我們自定義的操作狀態碼,首先來看我們常用的定義:
| 0 | 操作成功的消息提示 |
| 1 | 客戶端認證失敗,一般是客戶端被惡意修改 |
| 2 | 用戶認證失敗 |
| 3 | 提交參數錯誤:參數缺失、參數名不對等 |
| 4 | 提交參數校驗失敗,一般是提交給服務端的數據格式有誤,多發生在表單提交的場景中 |
| 5 | 自定義錯誤,服務端發生不可恢復的錯誤等 |
msg定義
msg為服務器端返回的操作信息。
無論操作成功與否,客戶端都應該根據業務給出準確的提示,客戶端則根據實際情況選擇展示與否。
data 定義
data則是請求返回的具體內容,通常data根據請求接口的不同最終會被解析成不同的實體類。
示例
下面我們以獲取消息列表和消息詳情兩個接口返回的響應數據作為示例:
消息列表:
消息詳情
{"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>>() {@Overridepublic 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;}}}@Overridepublic 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") 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>>() {@Overridepublic void onCompleted() {}@Overridepublic void onError(Throwable e) {//handle throwable}@Overridepublic 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;}@Overridepublic Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations,Retrofit retrofit) {TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));return new GsonResponseBodyConverter<>(adapter);//創建響應轉換器}@Overridepublic 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;}@Overridepublic 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;}@Overridepublic 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);}@Overridepublic Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));return new STGsonResponseBodyConverter<>(gson, adapter);}@Overridepublic 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>>() {@Overridepublic void onCompleted() {}@Overridepublic 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://handlebreak;case ApiErrorCode.ERROR_USER_AUTHORIZED://handlebreak;case ApiErrorCode.ERROR_REQUEST_PARAM://handlebreak;case ApiErrorCode.ERROR_PARAM_CHECK://handlebreak;case ApiErrorCode.ERROR_OTHER://handlebreak;case ApiErrorCode.ERROR_NO_INTERNET://handlebreak;}else{//handle}}@Overridepublic 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;}@Overridepublic void onStart() {//請求開始之前,檢查是否有網絡。無網絡直接拋出異常//另外,在你無法確定當前代碼運行在什么線程的時候,不要將UI的相關操作放在這里。if (!TDevice.hasInternet()) {this.onError(new ApiException(ApiErrorCode.ERROR_NO_INTERNET, "network interrupt"));return;}}@Overridepublic void onCompleted() {}@Overridepublic void onError(Throwable e) {ApiErrorHelper.handleCommonError(mContext, e);}@Overridepublic 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>>() {@Overridepublic void onNext(Result<MessageList> result) {MessageList messageList = result.getData();//handle messageList}});大部分接口的使用都和以上類似,針對個別異常處理只需要重寫onError()方法即可。
總結
以上是生活随笔為你收集整理的Retrofit响应数据及异常处理策略的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: (附源码连接)利用AI将2D照片合成3D
- 下一篇: Web前端面试指导(四十四):什么是响应