1. 程式人生 > >網路請求異常攔截優化

網路請求異常攔截優化

目錄介紹

  • 01.網路請求異常分類
  • 02.開發中注意問題
  • 03.原始的處理方式
  • 04.如何減少程式碼耦合性
  • 05.異常統一處理步驟
  • 06.完成版程式碼展示

好訊息

  • 部落格筆記大彙總【16年3月到至今】,包括Java基礎及深入知識點,Android技術部落格,Python學習筆記等等,還包括平時開發中遇到的bug彙總,當然也在工作之餘收集了大量的面試題,長期更新維護並且修正,持續完善……開源的檔案是markdown格式的!同時也開源了生活部落格,從12年起,積累共計N篇[近100萬字,陸續搬到網上],轉載請註明出處,謝謝!
  • 連結地址:https://github.com/yangchong211/YCBlogs
  • 如果覺得好,可以star一下,謝謝!當然也歡迎提出建議,萬事起於忽微,量變引起質變!

01.網路請求異常分類

  • 網路請求異常大概有哪些?
    • 第一種:訪問介面異常,比如404,500等異常,出現這類異常,Retrofit會自動丟擲異常。
    • 第二種:解析資料異常,資料體發生變化可能會導致這個問題。
    • 第三種:其他型別異常,比如伺服器響應超時異常,連結失敗異常,網路未連線異常等等。
    • 第四種:網路請求成功,但是伺服器定義了異常狀態,比如token失效,引數傳遞錯誤,或者統一給提示(這個地方比較拗口,比如購物app,你購買n件商品請求介面成功,code為200,但是伺服器發現沒有這麼多商品,這個時候就會給你一個提示,然後客戶端拿到這個進行吐司)

02.開發中注意問題

  • 在獲取資料的流程中,訪問介面和解析資料時都有可能會出錯,我們可以通過攔截器在這兩層攔截錯誤。
    • 1.在訪問介面時,我們不用設定攔截器,因為一旦出現錯誤,Retrofit會自動丟擲異常。比如,常見請求異常404,500,503等等。為了方便後期排查問題,這個可以在debug環境下列印日誌就可以。
    • 2.在解析資料時,我們設定一個攔截器,判斷Result裡面的code是否為成功,如果不成功,則要根據與伺服器約定好的錯誤碼來丟擲對應的異常。比如,token失效後跳轉登入頁面,禁用同賬號登陸多臺裝置,缺少引數,引數傳遞異常等等。
    • 3.除此以外,為了我們要儘量避免在View層對錯誤進行判斷,處理,我們必須還要設定一個攔截器,攔截onError事件,然後使用ExceptionUtils,讓其根據錯誤型別來分別處理。

03.原始的處理方式

  • 最簡單的處理方式,直接對返回的throwable進行型別判斷處理
    //請求,對throwable進行判斷
    ServiceHelper.getInstance()
          .getModelResult(param1, param2)
          .subscribeOn(Schedulers.io())
          .observeOn(AndroidSchedulers.mainThread())
          .subscribe(new Subscriber<Model>() {
                 @Override
                  public void onCompleted() {
    
                  }
    
                  @Override
                  public void onError(Throwable e) {
                      if(e instanceof HttpException){
    	                  //獲取對應statusCode和Message
                          HttpException exception = (HttpException)e;
                          String message = exception.response().message();
                          int code = exception.response().code();
                      }else if(e instanceof SSLHandshakeException){
    					//接下來就是各種異常型別判斷...
                      }else if(e instanceof ...){
    
                      }...
    			  }
    
                  @Override
                  public void onNext(Model model) {
                      if(model.getCode != CODE_SUCCESS){
                            int code = model.getCode();
                            switch (code){
                                case CODE_TOKEN_INVALID:
                                    ex.setDisplayMessage("重新登陸");
                                    break;
                                case CODE_NO_OTHER:
                                    ex.setDisplayMessage("其他情況");
                                    break;
                                case CODE_SHOW_TOAST:
                                    ex.setDisplayMessage("吐司伺服器返回的提示");
                                    break;
                                case CODE_NO_MISSING_PARAMETER:
                                    ex.setDisplayMessage("缺少引數,用log記錄伺服器提示");
                                    break;
                                default:
                                    ex.setDisplayMessage(message);
                                    break;
                            }
                      }else{
                          //正常處理邏輯
                      }
                  }
          });
    
    

04.如何減少程式碼耦合性

  • 為了不改變以前的程式碼結構,那麼如何做才能夠徹底解耦呢?一般情況下使用Retrofit網路請求框架,會有回撥方法,如下所示:
    package retrofit2;
    
    public interface Callback<T> {
        void onResponse(Call<T> var1, Response<T> var2);
    
        void onFailure(Call<T> var1, Throwable var2);
    }
    
  • 不管以前程式碼封裝與否,都希望一句程式碼即可實現網路請求攔截處理邏輯。那麼這個時候,我是怎麼處理的呢?
    public class ResponseData<T> {
    
        private int code;
        private String message;
        private T t;
    
        public int getCode() {
            return code;
        }
    
        public String getMessage() {
            return message;
        }
    
        public T getT() {
            return t;
        }
    }
    
    new Callback<ResponseData<HomeBlogEntity>>(){
        @Override
        public void onResponse(Call<ResponseData<HomeBlogEntity>> call,
                               Response<ResponseData<HomeBlogEntity>> response) {
            int code = response.body().getCode();
            String message = response.body().getMessage();
            HomeBlogEntity t = response.body().getT();
            if (code!= CODE_SUCCESS){
                //網路請求成功200,不過業務層執行服務端制定的異常邏輯
                ExceptionUtils.serviceException(code,message);
            } else {
                //網路請求成功,業務邏輯正常處理
            }
        }
    
        @Override
        public void onFailure(Call call, Throwable throwable) {
            ExceptionUtils.handleException(throwable);
        }
    };
    

05.異常統一處理步驟

  • 第一步:定義請求介面網路層失敗的狀態碼
    /**
     * 對應HTTP的狀態碼
     */
    private static final int BAD_REQUEST = 400;
    private static final int UNAUTHORIZED = 401;
    private static final int FORBIDDEN = 403;
    private static final int NOT_FOUND = 404;
    private static final int METHOD_NOT_ALLOWED = 405;
    private static final int REQUEST_TIMEOUT = 408;
    private static final int CONFLICT = 409;
    private static final int PRECONDITION_FAILED = 412;
    private static final int INTERNAL_SERVER_ERROR = 500;
    private static final int BAD_GATEWAY = 502;
    private static final int SERVICE_UNAVAILABLE = 503;
    private static final int GATEWAY_TIMEOUT = 504;
    
  • 第二步,介面請求成功,業務層失敗,服務端定義異常狀態碼
    • 比如,登入過期,提醒使用者重新登入;
    • 比如,新增商品,但是服務端發現庫存不足,這個時候介面請求成功,服務端定義業務層失敗,服務端給出提示語,客戶端進行吐司
    • 比如,請求介面,引數異常或者型別錯誤,請求code為200成功狀態,不過給出提示,這個時候客戶端用log列印服務端給出的提示語,方便快遞查詢問題
    • 比如,其他情況,介面請求成功,但是服務端定義業務層需要吐司服務端返回的對應提示語
    /**
     * 伺服器定義的狀態嗎
     * 比如:登入過期,提醒使用者重新登入;
     *      新增商品,但是服務端發現庫存不足,這個時候介面請求成功,服務端定義業務層失敗,服務端給出提示語,客戶端進行吐司
     *      請求介面,引數異常或者型別錯誤,請求code為200成功狀態,不過給出提示,這個時候客戶端用log列印服務端給出的提示語,方便快遞查詢問題
     *      其他情況,介面請求成功,但是服務端定義業務層需要吐司服務端返回的對應提示語
     */
    /**
     * 完全成功
     */
    private static final int CODE_SUCCESS = 0;
    /**
     * Token 失效
     */
    public static final int CODE_TOKEN_INVALID = 401;
    /**
     * 缺少引數
     */
    public static final int CODE_NO_MISSING_PARAMETER = 400400;
    /**
     * 其他情況
     */
    public static final int CODE_NO_OTHER = 403;
    /**
     * 統一提示
     */
    public static final int CODE_SHOW_TOAST = 400000;
    
  • 第三步,自定義Http層的異常和伺服器定義的異常類
    public class HttpException extends Exception {
    
        private int code;
        private String displayMessage;
    
        public HttpException(Throwable throwable, int code) {
            super(throwable);
            this.code = code;
        }
    
        public void setDisplayMessage(String displayMessage) {
            this.displayMessage = displayMessage;
        }
    
        public String getDisplayMessage() {
            return displayMessage;
        }
    
        public int getCode() {
            return code;
        }
    }
    
    public class ServerException extends RuntimeException {
    
        public int code;
        public String message;
    
        public int getCode() {
            return code;
        }
    
        public void setCode(int code) {
            this.code = code;
        }
    
        @Override
        public String getMessage() {
            return message;
        }
    
        public void setMessage(String message) {
            this.message = message;
        }
    }
    
  • 第四步,統一處理異常邏輯如下所示
    /**
     * 這個可以處理伺服器請求成功,但是業務邏輯失敗,比如token失效需要重新登陸
     * @param code                  自定義的code碼
     */
    public static void serviceException(int code , String content){
        if (code != CODE_SUCCESS){
            ServerException serverException = new ServerException();
            serverException.setCode(code);
            serverException.setMessage(content);
            handleException(serverException);
        }
    }
    
    /**
     * 這個是處理網路異常,也可以處理業務中的異常
     * @param e                     e異常
     */
    public static void handleException(Throwable e){
        HttpException ex;
        //HTTP錯誤   網路請求異常 比如常見404 500之類的等
        if (e instanceof retrofit2.HttpException){
            retrofit2.HttpException httpException = (retrofit2.HttpException) e;
            ex = new HttpException(e, ErrorCode.HTTP_ERROR);
            switch(httpException.code()){
                case BAD_REQUEST:
                case UNAUTHORIZED:
                case FORBIDDEN:
                case NOT_FOUND:
                case METHOD_NOT_ALLOWED:
                case REQUEST_TIMEOUT:
                case CONFLICT:
                case PRECONDITION_FAILED:
                case GATEWAY_TIMEOUT:
                case INTERNAL_SERVER_ERROR:
                case BAD_GATEWAY:
                case SERVICE_UNAVAILABLE:
                    //均視為網路錯誤
                default:
                    ex.setDisplayMessage("網路錯誤"+httpException.code());
                    break;
            }
        } else if (e instanceof ServerException){
            //伺服器返回的錯誤
            ServerException resultException = (ServerException) e;
            int code = resultException.getCode();
            String message = resultException.getMessage();
            ex = new HttpException(resultException, ErrorCode.SERVER_ERROR);
            switch (code){
                case CODE_TOKEN_INVALID:
                    ex.setDisplayMessage("token失效");
                    //下面這裡可以統一處理跳轉登入頁面的操作邏輯
                    break;
                case CODE_NO_OTHER:
                    ex.setDisplayMessage("其他情況");
                    break;
                case CODE_SHOW_TOAST:
                    ex.setDisplayMessage("吐司");
                    break;
                case CODE_NO_MISSING_PARAMETER:
                    ex.setDisplayMessage("缺少引數");
                    break;
                default:
                    ex.setDisplayMessage(message);
                    break;
            }
        } else if (e instanceof JsonParseException
                || e instanceof JSONException
                || e instanceof ParseException){
            ex = new HttpException(e, ErrorCode.PARSE_ERROR);
            //均視為解析錯誤
            ex.setDisplayMessage("解析錯誤");
        }else if(e instanceof ConnectException){
            ex = new HttpException(e, ErrorCode.NETWORK_ERROR);
            //均視為網路錯誤
            ex.setDisplayMessage("連線失敗");
        } else if(e instanceof java.net.UnknownHostException){
            ex = new HttpException(e, ErrorCode.NETWORK_ERROR);
            //網路未連線
            ex.setDisplayMessage("網路未連線");
        } else if (e instanceof SocketTimeoutException) {
            ex = new HttpException(e, ErrorCode.NETWORK_ERROR);
            //網路未連線
            ex.setDisplayMessage("伺服器響應超時");
        }  else {
            ex = new HttpException(e, ErrorCode.UNKNOWN);
            //未知錯誤
            ex.setDisplayMessage("未知錯誤");
        }
        String displayMessage = ex.getDisplayMessage();
        //這裡直接吐司日誌異常內容,注意正式專案中一定要注意吐司合適的內容
        ToastUtils.showRoundRectToast(displayMessage);
    }
    
  • 第五步,如何呼叫
    @Override
    public void onError(Throwable e) {
        //直接呼叫即可
        ExceptionUtils.handleException(e);
    }
    

06.完成版程式碼展示

  • 如下所示
    public class ExceptionUtils {
    
        /*
         * 在使用Retrofit+RxJava時,我們訪問介面,獲取資料的流程一般是這樣的:訂閱->訪問介面->解析資料->展示。
         * 如上所說,異常和錯誤本質是一樣的,因此我們要儘量避免在View層對錯誤進行判斷,處理。
         *
         * 在獲取資料的流程中,訪問介面和解析資料時都有可能會出錯,我們可以通過攔截器在這兩層攔截錯誤。
         * 1.在訪問介面時,我們不用設定攔截器,因為一旦出現錯誤,Retrofit會自動丟擲異常。
         * 2.在解析資料時,我們設定一個攔截器,判斷Result裡面的code是否為成功,如果不成功,則要根據與伺服器約定好的錯誤碼來丟擲對應的異常。
         * 3.除此以外,為了我們要儘量避免在View層對錯誤進行判斷,處理,我們必須還要設定一個攔截器,攔截onError事件,然後使用ExceptionHandler,讓其根據錯誤型別來分別處理。
         */
    
    
    
        /**
         * 對應HTTP的狀態碼
         */
        private static final int BAD_REQUEST = 400;
        private static final int UNAUTHORIZED = 401;
        private static final int FORBIDDEN = 403;
        private static final int NOT_FOUND = 404;
        private static final int METHOD_NOT_ALLOWED = 405;
        private static final int REQUEST_TIMEOUT = 408;
        private static final int CONFLICT = 409;
        private static final int PRECONDITION_FAILED = 412;
        private static final int INTERNAL_SERVER_ERROR = 500;
        private static final int BAD_GATEWAY = 502;
        private static final int SERVICE_UNAVAILABLE = 503;
        private static final int GATEWAY_TIMEOUT = 504;
    
        /**
         * 伺服器定義的狀態嗎
         * 比如:登入過期,提醒使用者重新登入;
         *      新增商品,但是服務端發現庫存不足,這個時候介面請求成功,服務端定義業務層失敗,服務端給出提示語,客戶端進行吐司
         *      請求介面,引數異常或者型別錯誤,請求code為200成功狀態,不過給出提示,這個時候客戶端用log列印服務端給出的提示語,方便快遞查詢問題
         *      其他情況,介面請求成功,但是服務端定義業務層需要吐司服務端返回的對應提示語
         */
        /**
         * 完全成功
         */
        private static final int CODE_SUCCESS = 0;
        /**
         * Token 失效
         */
        public static final int CODE_TOKEN_INVALID = 401;
        /**
         * 缺少引數
         */
        public static final int CODE_NO_MISSING_PARAMETER = 400400;
        /**
         * 其他情況
         */
        public static final int CODE_NO_OTHER = 403;
        /**
         * 統一提示
         */
        public static final int CODE_SHOW_TOAST = 400000;
    
    
    
        /**
         * 這個可以處理伺服器請求成功,但是業務邏輯失敗,比如token失效需要重新登陸
         * @param code                  自定義的code碼
         */
        public static void serviceException(int code , String content){
            if (code != CODE_SUCCESS){
                ServerException serverException = new ServerException();
                serverException.setCode(code);
                serverException.setMessage(content);
                handleException(serverException);
            }
        }
    
        /**
         * 這個是處理網路異常,也可以處理業務中的異常
         * @param e                     e異常
         */
        public static void handleException(Throwable e){
            HttpException ex;
            //HTTP錯誤   網路請求異常 比如常見404 500之類的等
            if (e instanceof retrofit2.HttpException){
                retrofit2.HttpException httpException = (retrofit2.HttpException) e;
                ex = new HttpException(e, ErrorCode.HTTP_ERROR);
                switch(httpException.code()){
                    case BAD_REQUEST:
                    case UNAUTHORIZED:
                    case FORBIDDEN:
                    case NOT_FOUND:
                    case METHOD_NOT_ALLOWED:
                    case REQUEST_TIMEOUT:
                    case CONFLICT:
                    case PRECONDITION_FAILED:
                    case GATEWAY_TIMEOUT:
                    case INTERNAL_SERVER_ERROR:
                    case BAD_GATEWAY:
                    case SERVICE_UNAVAILABLE:
                        //均視為網路錯誤
                    default:
                        ex.setDisplayMessage("網路錯誤"+httpException.code());
                        break;
                }
            } else if (e instanceof ServerException){
                //伺服器返回的錯誤
                ServerException resultException = (ServerException) e;
                int code = resultException.getCode();
                String message = resultException.getMessage();
                ex = new HttpException(resultException, ErrorCode.SERVER_ERROR);
                switch (code){
                    case CODE_TOKEN_INVALID:
                        ex.setDisplayMessage("重新登陸");
                        break;
                    case CODE_NO_OTHER:
                        ex.setDisplayMessage("其他情況");
                        break;
                    case CODE_SHOW_TOAST:
                        ex.setDisplayMessage("吐司");
                        break;
                    case CODE_NO_MISSING_PARAMETER:
                        ex.setDisplayMessage("缺少引數");
                        break;
                    default:
                        ex.setDisplayMessage(message);
                        break;
                }
            } else if (e instanceof JsonParseException
                    || e instanceof JSONException
                    || e instanceof ParseException){
                ex = new HttpException(e, ErrorCode.PARSE_ERROR);
                //均視為解析錯誤
                ex.setDisplayMessage("解析錯誤");
            }else if(e instanceof ConnectException){
                ex = new HttpException(e, ErrorCode.NETWORK_ERROR);
                //均視為網路錯誤
                ex.setDisplayMessage("連線失敗");
            } else if(e instanceof java.net.UnknownHostException){
                ex = new HttpException(e, ErrorCode.NETWORK_ERROR);
                //網路未連線
                ex.setDisplayMessage("網路未連線");
            } else if (e instanceof SocketTimeoutException) {
                ex = new HttpException(e, ErrorCode.NETWORK_ERROR);
                //網路未連線
                ex.setDisplayMessage("伺服器響應超時");
            }  else {
                ex = new HttpException(e, ErrorCode.UNKNOWN);
                //未知錯誤
                ex.setDisplayMessage("未知錯誤");
            }
            String displayMessage = ex.getDisplayMessage();
            //這裡直接吐司日誌異常內容,注意正式專案中一定要注意吐司合適的內容
            ToastUtils.showRoundRectToast(displayMessage);
        }
    }
    

其他介紹

01.關於部落格彙總連結

02.關於我的部落格

開原始碼案例:https://github.com/yangcho