Retrofit响应数据及异常处理策略
今天我们来谈谈客户端对通讯协议的处理,主要分为三部分:约定响应数据格式,响应数据的自动映射以及错误处理三部分。由于数据协议采用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>>() {@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")
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>>() {@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响应数据及异常处理策略相关推荐
- java day62【 响应数据和结果视图 、 SpringMVC 实现文件上传 、 SpringMVC 中的异常处理 、 SpringMVC 中的拦截器 】...
第1章 响应数据和结果视图 1.1返回值分类 1.1.1 字符串 1.1.2 void 1.1.3 ModelAndView 1.2转发和重定向 1.2.1 forward 转发 1.2.2 Redi ...
- Java17-day08【File(创建和删除文件、判断和获取功能、遍历目录)、IO流(字节流写数据、异常处理、字节流读数据、复制文本文件、复制图片)】
视频+资料(工程源码.笔记)[链接:https://pan.baidu.com/s/1MdFNUADVSFf-lVw3SJRvtg 提取码:zjxs] Java基础--学习笔记(零起点打开java ...
- Redis的内存回收机制和数据过期淘汰策略
本文来说下Redis的内存回收机制和数据过期淘汰策略 文章目录 概述 为什么需要内存回收 过期删除策略 定时删除 惰性删除 定期删除 删除策略比对 过期删除策略原理 redisDb结构体定义 expi ...
- SpringMVC处理请求或响应数据
SpringMVC处理请求数据或响应数据: 请求处理方法签名 Spring MVC 通过分析处理方法的签名,HTTP请求信息绑定到处理方法的相应人参中. Spring MVC 对控制器处理方法签名的限 ...
- 华为云大数据存储的冗余方式是三副本_大数据入门:HDFS数据副本存放策略
大数据处理当中,数据储存始终是一个重要的环节,从现阶段的市场现状来说,以Hadoop为首的大数据技术框架,仍然占据主流地位,而Hadoop的HDFS,在数据存储方面,仍然得到重用.今天的大数据入门分享 ...
- HTTP API响应数据规范整理
2019独角兽企业重金招聘Python工程师标准>>> 关于作者 马隆博(Lenbo Ma),Java,Javascript Blog: http://mlongbo.com E-M ...
- SAP Spartacus里unit list tree的页面显示和后台响应数据的对应关系
如下图所示: 页面数据显示如下,其中Rustic包含6个直接子节点,分别是Rustic Retail,Rustic Services和test1-test4. 后台SAP Commerce Cloud ...
- ASP.NET Web API 记录请求响应数据到日志的一个方法
原文:ASP.NET Web API 记录请求响应数据到日志的一个方法 原文:http://blog.bossma.cn/dotnet/asp-net-web-api-log-request-resp ...
- java获取response数据_Java中实现Http请求并获取响应数据
前言 在演示的功能代码中使用的请求http地址为:http://timor.tech/api/holiday/year/ 接口说明:获取指定年份或年月份的所有节假日信息.默认返回当前年份的所有信息. ...
最新文章
- php时间2小时以前,PHP版实现友好的时间显示方式(例如:2小时前)
- MNIST数据可视化
- debconf: DbDriver config: /var/cache/debconf/config.dat is locked by another process
- SQL注入——基于报错的注入(五)
- 能源局将提高光伏“领跑者”项目技术指标
- Linux多线程开发-线程同步-互斥锁pthread_mutex_t
- 容器编排技术 -- Kubernetes kubectl create configmap 命令详解
- java stringbuilder清空_Java中StringBuilder的清空方法比较
- oracle虚拟件不活动,BOM 中的虚拟件
- Unity 3D网页游戏与flash网页游戏的较量
- NoClassDefFoundError: Could not initialize class
- RSA的APT峰会会议纪要
- 个人公众号如何运营?可以从哪些方向突破?
- WeBRTC IOS视频采集流程
- 我国个人缴税计算实例
- html项目符号正方形,HTML无序列表| HTML项目符号列表
- png转成SVG方法
- OpenCV二值图像连通域分析
- PS唯美清新花朵调色
- Shell编程学习(四)read读取、函数、文件归档、定时任务
热门文章
- 监控服务器时间无法修改怎么办,监控服务器修改时间
- 国内优秀的多用户商城系统盘点(2022年整理)
- 互联网日报 | 2月24日 星期三 | 华为去年收入利润保持正增长;特斯拉公开全国统一维保价目表;途虎养车回应赴美上市传闻...
- 【学习笔记】《深入浅出Pandas》第17章:Pandas实战案例
- 2020-12-16 今日学习Date类
- 李航第二章课后习题答案
- 双色球大乐透开奖查询软件
- 国内OCR供应商及其演示链接
- 2020写给未来 100w 粉丝的年终总结
- 无聊的小技巧-idea启动springboot时设置启动参数