RxJava2 + Retrofit2 完全指南 之 統一狀態碼/Exception處理
前言
直接上資料結構:
{ "code": 200, "data": { "id": "1", "name": "name1", "stargazers_count": 1 }, "msg": "請求成功" }
上面的資料結構是一般比較簡單而常見的資料結構,在正確的情況下我們只關心 data
裡面的資料,錯誤的情況下我們關心 code
和 msg
提示,而區分這兩種情況又要不斷的寫大量的樣板程式碼,這不是首選。所以就有了兩種方法:
- 通過定義泛型T來處理
- 自定義ResponseBodyConverter實現統一處理
首先講講泛型T的處理方式,基本如下:
public class BaseDataModel<T> { private int code; private T data; private String msg; public int getCode() { return code; } public void setCode(int code) { this.code = code; } public T getData() { return data; } public void setData(T data) { this.data = data; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public boolean isSuccessful() { return code == 200; } }
使用也比較簡單,只需要將泛型替換為相應的實體類就行。但是這也有一個不好的地方就是一旦專案中對接的資料結構變得繁雜的時候,就需要不斷的頂部相應的實體了,還是比較麻煩,後面會講到,所以我們還是使用自定義ResponseBodyConverter實現統一處理。
實現
分析
我在使用Retrofit的時候,一般是使用的是 GsonConverterFactory
,而 GsonConverterFactory
則是負責將我們的提交的資料進行序列化和將返回的資料進行反序列化的,所以只要我們分析完成其中的程式碼,看明白他是怎麼對資料進行反序列化的,那麼我們就能做到統一的資料轉換了。
原始碼目錄

GsonConverter原始碼目錄
從上圖我們可以看到,其實只有三個類,分別是:GsonConverterFactory、GsonRequestBodyConverter和GsonResponseBodyConverter。
@Body
其中GsonResponseBodyConverter的原始碼內容如下:
final class GsonResponseBodyConverter<T> implements Converter<ResponseBody, T> { private final Gson gson; private final TypeAdapter<T> adapter; GsonResponseBodyConverter(Gson gson, TypeAdapter<T> adapter) { this.gson = gson; this.adapter = adapter; } @Override public T convert(ResponseBody value) throws IOException { // 這裡就是對返回結果進行處理 JsonReader jsonReader = gson.newJsonReader(value.charStream()); try { T result = adapter.read(jsonReader); if (jsonReader.peek() != JsonToken.END_DOCUMENT) { throw new JsonIOException("JSON document was not fully consumed."); } return result; } finally { value.close(); } } }
從上面的原始碼可以看到,convert方法中其實就是通過Gson進行看一次序列化而已,而ResponseBody提供了string方法,直接將返回結果轉換為了字串,那麼我們就可以在中間加一道工序,將其中的資料進行相關的判斷處理,再進行返回。
編碼
首先將 GsonConverter
的原始碼copy過來,然後改個名字,如下:

HandlerErrorGsonConvert
為了模擬不同的資料介面,硬編碼了以下三種資料結構。
- 資料結構·1
{ "code": 200, "message": "成功,但是沒有資料", "data": [] }
- 資料結構·2
{ "code": -1, "message": "這裡是介面返回的:錯誤的資訊,丟擲錯誤資訊提示!", "data": [] }
- 資料結構·3
{ "code": 401, "message": "這裡是介面返回的:許可權不足,請重新登入!", "data": [] }
從上面三種資料結構分析,我們需要在 code
為 200
的情況下直接序列化 data
,其它情況下丟擲 code
和 msg
,所以這裡我們還需要定義一個 Exception
類來承載錯誤資訊。
建立Exception類
Exception類如下,主要是對一些和後端約定的狀態碼進行轉義和包裹。
public class NetErrorException extends IOException { private Throwable exception; private int mErrorType = NO_CONNECT_ERROR; private String mErrorMessage; /*無連線異常*/ public static final int NoConnectError = 1; /** * 資料解析異常 */ public static final int PARSE_ERROR = 0; /** * 無連線異常 */ public static final int NO_CONNECT_ERROR = 1; /*網路連線超時*/ public static final int SocketTimeoutError = 6; /** * 無法連線到服務 */ public static final int ConnectExceptionError = 7; /** * 伺服器錯誤 */ public static final int HttpException = 8; /** * 登陸失效 */ public static final int LOGIN_OUT = 401; /** * 其他 */ public static final int OTHER = -99; /** * 沒有網路 */ public static final int UNOKE = -1; /** * 無法找到 */ public static final int NOT_FOUND = 404; /*其他*/ public NetErrorException(Throwable exception, int mErrorType) { this.exception = exception; this.mErrorType = mErrorType; } public NetErrorException(String message, Throwable cause) { super(message, cause); } public NetErrorException(String message, int mErrorType) { super(message); this.mErrorType = mErrorType; this.mErrorMessage = message; } @Override public String getMessage() { if (!TextUtils.isEmpty(mErrorMessage)) { return mErrorMessage; } switch (mErrorType) { case PARSE_ERROR: return "資料解析異常"; case NO_CONNECT_ERROR: return "無連線異常"; case OTHER: return mErrorMessage; case UNOKE: return "當前無網路連線"; case ConnectExceptionError: return "無法連線到伺服器,請檢查網路連線後再試!"; case HttpException: try { if (exception.getMessage().equals("HTTP 500 Internal Server Error")) { return "伺服器發生錯誤!"; } } catch (Exception e) { e.printStackTrace(); } if (exception.getMessage().contains("Not Found")) return "無法連線到伺服器,請檢查網路連線後再試!"; return "伺服器發生錯誤"; } try { return exception.getMessage(); } catch (Exception e) { return "未知錯誤"; } } /** * 獲取錯誤型別 */ public int getErrorType() { return mErrorType; } }
統一訂閱處理
由於這裡使用的是 RxJava
,需要自定義一個 Subscriber
來對 Convert
丟擲的 Exception
進行捕獲,也需要對其它 Exception
進行捕獲和包裹,防止發生錯誤後直接崩潰,程式碼不多,如下:
public abstract class ApiSubscriber<T> extends ResourceSubscriber<T> { @Override public void onError(Throwable e) { NetErrorException error = null; if (e != null) { // 對不是自定義丟擲的錯誤進行解析 if (!(e instanceof NetErrorException)) { if (e instanceof UnknownHostException) { error = new NetErrorException(e, NetErrorException.NoConnectError); } else if (e instanceof JSONException || e instanceof JsonParseException) { error = new NetErrorException(e, NetErrorException.PARSE_ERROR); } else if (e instanceof SocketTimeoutException) { error = new NetErrorException(e, NetErrorException.SocketTimeoutError); } else if (e instanceof ConnectException) { error = new NetErrorException(e, NetErrorException.ConnectExceptionError); } else { error = new NetErrorException(e, NetErrorException.OTHER); } } else { error = new NetErrorException(e.getMessage(), NetErrorException.OTHER); } } // 回撥抽象方法 onFail(error); } /** * 回撥錯誤 */ protected abstract void onFail(NetErrorException error); }
修改HandlerErrorGsonResponseBodyConverter
這一步其實沒什麼難度,只是在 convert
方法中提前將ResponseBody.string()取出來,通過最簡單的 JSONObject
來判斷 code
,為200則返回 data
,否則丟擲自定義錯誤。詳細程式碼如下:
final class HandlerErrorGsonResponseBodyConverter<T> implements Converter<ResponseBody, T> { private final TypeAdapter<T> adapter; /**模擬的假資料*/ private final List<String> mockResult; private final Random random; HandlerErrorGsonResponseBodyConverter(TypeAdapter<T> adapter) { this.random = new Random(); this.adapter = adapter; mockResult = new ArrayList<>(); mockResult.add("{\"code\":200,\"message\":\"成功,但是沒有資料\",\"data\":[]}"); mockResult.add("{\"code\":-1,\"message\":\"這裡是介面返回的:錯誤的資訊,丟擲錯誤資訊提示!\",\"data\":[]}"); mockResult.add("{\"code\":401,\"message\":\"這裡是介面返回的:許可權不足,請重新登入!\",\"data\":[]}"); } @Override public T convert(ResponseBody value) throws IOException { // 這裡就是對返回結果進行處理 String jsonString = value.string(); try { // 這裡為了模擬不同的網路請求,所以採用了本地字串的格式然後進行隨機選擇判斷結果。 int resultIndex = random.nextInt(mockResult.size() + 1); if (resultIndex == mockResult.size()) { return adapter.fromJson(jsonString); } else { // 這裡模擬不同的資料結構 jsonString = mockResult.get(resultIndex); Log.e("TAG", "這裡進行了返回結果的判斷"); // ------------------ JsonObject 只做了初略的判斷,具體情況自定 JSONObject object = new JSONObject(jsonString); int code = object.getInt("code"); if (code != 200) { throw new NetErrorException(object.getString("message"), code); } return adapter.fromJson(object.getString("data")); } } catch (JSONException e) { e.printStackTrace(); throw new NetErrorException("資料解析異常", NetErrorException.PARSE_ERROR); } finally { value.close(); } } }
如何呼叫
主要是有以下兩個地方:
- 建立Retrofit的時候將
addConverterFactory
換成自定義的HandlerErrorGsonConverterFactory.create()
- RxJava的subscribeWith換成自定義的ApiSubscriber<T>。
部分程式碼如下:
...... retrofit = new Retrofit.Builder() .baseUrl("https://api.github.com/") .addConverterFactory(HandlerErrorGsonConverterFactory.create()) // 這裡使用的是用自己自定義的轉換器 .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .build(); service = retrofit.create(GitHubService.class); ...... mResultTv.setText("建立請求................\n"); disposable = service.listRxJava2FlowableRepos("aohanyao", "owner") .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribeWith(new ApiSubscriber<List<Repo>>() { @Override public void onNext(List<Repo> repos) { mResultTv.append("請求成功,repoCount:" + repos.size() + ":\n"); for (Repo repo : repos) { mResultTv.append("repoName:" + repo.getName() + "star:" + repo.getStargazers_count() + "\n"); } } @Override protected void onFail(NetErrorException error) { mResultTv.append("請求失敗" + error.getMessage() + "................\n"); } @Override public void onComplete() { mResultTv.append("請求成功................\n"); } });
演示

HandlerResponseError
結束
這裡只寫了對返回狀態碼和錯誤的統一處理,並未對不同的資料結構進行處理,下一篇將對不同的資料結構進行統一處理。