网络请求异常拦截优化

作者: 杨充211 | 来源:发表于2019-05-22 18:59 被阅读7次

    目录介绍

    • 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/yangchong211/LifeHelper

    相关文章

      网友评论

        本文标题:网络请求异常拦截优化

        本文链接:https://www.haomeiwen.com/subject/boqpzqtx.html