本文长话短说,不啰嗦
项目地址:BW_Libs
这个测试接口,我试的时候时不时的没有数据,别的关键字我也没试
思考
我看很多人在做 http 网络请求工具时,都是把业务层逻辑和 lib 层逻辑放在一起了,这样不方便以后更换网络框架
想 retrofit 对象,okhttpclient 对象这些是 lib 层的代码
但是像添加 head 的 intercepter 拦截器,公共业务code处理这些就应该放在业务层了,应该是和 lib 层分离的
至于 retrofit 的注解网络接口,我个人倾向于使用公共的 get、post 请求,这样不会一改起来,整个 app 整个改,改动的地方会少很多
现在的框架越做越好,但是不得不承认的是,实现仙童功能的框架之间差异性越来越大,这给我们造成了另一个烦恼,如何对开源框架再封装
基于以上几点,我开始构建我的网络 lib
功能封层
我的实现很简单的,代码很少,主要看个思路吧,不喜欢的请喷我吧~
lib 层:
- CommonService 提供统一的 get、post 请求,另外也支持具体的 retrofit interface
- ApiException 自定义的 exception 对象
- ErrorInterceptor 错误嘛拦截器,用于统一处理网络状态码,返回适合本公司的 message 提示文字,注意这里处理的不是业务 code
- HttpManager 网络工具,单例对外提供读服务
业务层:
- BlueService 用于测试的一个 retrofit interface
- BaseResponse<T> 公共数据返回类型
- CommonHttpFunctionByBaseResponse 继承 Function 用于统一处理公司业务,比如 T 票,用户异地登录,踢掉当前使用者
- BookResponse 非标准数据类型,这是因为测试接口返回不是上面的标准数据类型
- BookRepositroy 摸个业务的公共数据层
这里面好些都是测试用的,有用的没几个,很容易理解
lib 层思路
1. 先来看 CommonService
CommonService 提供统一的 get、post 访问,也支持具体的 retrofit interface
public interface CommonService {
@GET("{xxxUrl}")
Observable<ResponseBody> getMethod(@Path("xxxUrl") String url, @QueryMap Map<String, String> options);
@GET("{xxxUrl}")
Observable<ResponseBody> getMethod(@Path("xxxUrl") String url);
@FormUrlEncoded
@POST("{xxxUrl}")
Observable<ResponseBody> postMethod(@Path("xxxUrl") String url, @FieldMap Map<String, String> options);
}
统一的 get、post 接口无法使用泛型,所以这里我返回 ResponseBody, response 网络响应原生类,这个 ResponseBody 是 okhttp3 的
2. ErrorInterceptor
我不知道大家需不需要这个处理,我司这里不让显示框架提示的文字,需要我们自己根据网络相应码自己抛一个 exception 给最后的 onError 处理
class ErrorInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
val response = chain.proceed(request)
if (401 == response.code()) {
throw ApiException("身份验证错误!")
} else if (403 == response.code()) {
throw ApiException("禁止访问!")
} else if (404 == response.code()) {
throw ApiException("链接错误")
} else if (408 == response.code()) {
throw ApiException("请求超时!")
} else if (503 == response.code()) {
throw ApiException("服务器升级中!")
} else if (500 == response.code()) {
throw ApiException("服务器内部错误!")
}
return response
}
}
我这里没写太多,但是我查了查网络错误吗有很多,若是用的话,大家还是去 baidu 写全吧~
3. HttpManager
HttpManager 肯定是单例的啦,然后 HttpManager 里面有一个通用的 okHttpClient、retrofit 对象,这种情况只能应对一个 baseUrl,若是您的单位的 app 有多个 baseUrl ,那么请创建 map 来保存 baseUrl 对应 okHttpClient、retrofit
HttpManager 对外提供 init 初始化方法,我没有对 okHttpClient 对象的 build 配置项再做 build 了,okHttpClient 的 build 配置项是在太多了,init 初始化中直接由外接传递进来一个 okHttpClient.build,这样最省事,虽然会造成一部分耦合,未来换框架会改,但是不是方便我们当下使用嘛,而且改的地方大家也都知道,就集中在这一处,也好改,算是代码封装和现实的拖鞋吧
剩下的没啥好说的了,都很简单,大家一看便知
class HttpManager {
lateinit private var okHttpClient: OkHttpClient
lateinit private var retrofit: Retrofit
lateinit var baseUrl: String
companion object {
var connectTimeout: Long = 10 * 1000
var readTimeout: Long = 10 * 1000
var writeTimeout: Long = 10 * 1000
var instance: HttpManager = HttpManager()
}
/**
* 初始化网络数据,使用 OkHttpClient.Builder 传入主要参数到 ohkttp 对象中
*/
fun init(baseUrl: String, builder: OkHttpClient.Builder?) {
if (builder == null) {
okHttpClient = OkHttpClient.Builder()
// 超时时间
.connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
.readTimeout(readTimeout, TimeUnit.MILLISECONDS)
.writeTimeout(writeTimeout, TimeUnit.MILLISECONDS)
// 网络相应 code 码处理,不含 app 业务 code 处理
.addInterceptor(ErrorInterceptor())
.build()
} else {
okHttpClient = builder
.addInterceptor(ErrorInterceptor())
.build()
}
retrofit = Retrofit.Builder()
.client(okHttpClient)
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()
}
/**
* 标准 get 请求
*/
fun get(url: String, options: Map<String, String>): Observable<ResponseBody> {
return retrofit.create(CommonService::class.java).getMethod(url, options).subscribeOn(Schedulers.io())
}
/**
* 标准 get 请求
*/
fun get(url: String): Observable<ResponseBody> {
return retrofit.create(CommonService::class.java).getMethod(url).subscribeOn(Schedulers.io())
}
/**
* 标准 post 请求
*/
fun post(url: String, options: Map<String, String>): Observable<ResponseBody> {
return retrofit.create(CommonService::class.java).postMethod(url, options).subscribeOn(Schedulers.io())
}
/**
* 支持用户使用自己定义的 RetrofitService,而非公共的 RetrofitService
*/
fun <S> createRetrofitService(service: Class<S>): S {
return retrofit.create(service)
}
}
业务层思路
业务层没啥好说的了,除了拿来直接用的,剩下的值得我们关注的点就是怎么统一公共处理,思路有5:
- 写在 intercepter 拦截器里,这种思路太死了,有的接口不要处理某些 code ,还有若是统一处理逻辑要是和页面联系很紧密的话,写在 intercepter 拦截器里我们没法和页面交互
- 写一个统一的 function,再网络请求是同一添加,flatMap rxjava 操作符大家都知道吧
- 就是最原始的写法了,每个接口都写一遍,复制粘贴就行,缺点就是有改动太麻烦,改的地方很多
- 仿照 rxjava 提供的数据转换器思路,自定义一个数据转换器,在其中加入处理公共业务 code 的逻辑,这个思路缺点还是不够灵活,有的接口不需要处理某些 code 怎么办,错误逻辑需要页面配合怎么办,但是从代码封装的角度看,这块值得我们自己联系,就算不用也是值得自己写写,找找感觉的
Retrofit.Builder().addCallAdapterFactory(RxJavaCallAdapterFactory.create())
详细请参考:
- rxjava 种所有的错误处理我们都是在 最后的 onError 中处理,这时的 exception 有可能是系统抛给我们的,也有可能是我们自己抛出的业务错误,这块如何统一处理呢?思路就是我们继承 Subscriber<T> 自己写一个 BaseSubscriber<T>,在 onError 中统一处理下错误,比如下面,我们统一处理下错误 message 应该显示的是什么
//辅助处理异常
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();
}
}
}
我是愿意使用第二种方法的,写一个统一的 function 处理公共业务
public class CommonHttpFunctionByBaseResponse<T> implements Function<BaseResponse<T>, T> {
@Override
public T apply(BaseResponse<T> baseResponse) throws Exception {
if (!baseResponse.isSuccess()) {
// 有特殊处理,可以在这里进行,比如 T 票,并不是所有的接口都要相应 Token 被 T 的问题
// 这里在具体的 response 类里自行判断是不是要添加处理,也是为了灵活一些
Observable.error(new Exception(baseResponse.getMessage()));
}
return baseResponse.getData();
}
}
但是我在这里遇到过不去的问题了,gson 这块我不知道怎么写了,汗一个,学艺不精啊,就是拿到:接口返回 ResponseBody 对象,responseBody.string() 拿到 json 字符串,怎么通过传泛型 T 来转换成 BaseResponse<T> 类型我就不会写了。无奈 gson 这块我只能每个接口都写一遍了了。
但是对于 BaseResponse.code 我还是封装了 Function 对象统一处理
最后大家看一下如何使用
- activity 层
getWindow().getDecorView().post(new Runnable() {
@Override
public void run() {
Disposable disposable = new BookRepositroy()
.get("小王子", "", "0", "20")
.subscribe(
new Consumer<BookResponse>() {
@Override
public void accept(BookResponse bookResponse) throws Exception {
List<BookResponse.Book> books = bookResponse.getBooks();
adapter.refreshData(books);
}
},
new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
// ApiException.getErrorInfo(throwable) 获取统一错误信息
Log.d("AA", "错误:" + ApiException.getErrorInfo(throwable));
}
}
);
}
- 数据层
public class BookRepositroy {
public static final String URL_BOOK_LIST = "book/search";
public Observable<BookResponse> get(String title, String tag, String startCount, String wantCount) {
Map<String, String> map = new HashMap<>();
map.put("q", title);
map.put("tag", tag);
map.put("start", startCount);
map.put("count", wantCount);
return HttpManager.Companion.getInstance().get(URL_BOOK_LIST, map)
.map(new Function<ResponseBody, BookResponse>() {
@Override
public BookResponse apply(ResponseBody responseBody) throws Exception {
BookResponse bookResponse = null;
try {
bookResponse = new Gson().fromJson(responseBody.string(), BookResponse.class);
} catch (Exception e) {
Observable.error(new ApiException("数据异常"));
}
return bookResponse;
}
})
.observeOn(AndroidSchedulers.mainThread());
}
}
- 初始化网络配置
这里我演示了下添加 head 和 我司的 MD5 加密
fun initHttp() {
var baseUrl = "https://api.douban.com/v2/"
/**
* 请求头拦截器
* 1. 可以判断网络地址是否需要特殊处理
* if (s.contains("androidxx")) {
request = request.newBuilder().url("http://www.androidxx.cn").build();
}
*/
var headInterceptor = object : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
// 获取 post MD5 加密串
var md5: String = ""
var method: String = request.method()
if (method.equals("post")) {
val requestBody = request.body()
if (requestBody is FormBody && requestBody.size() > 0) {
var json = JSONObject()
var formBody: FormBody = requestBody as FormBody
formBody.size()
for (index in 0..formBody.size()) {
json.put(formBody.encodedName(index), formBody.encodedValue(index))
}
md5 = json.toString()
}
}
// 添加请求头
var requestBuilder: Request = request.newBuilder()
.addHeader("Connection", "AA")
.addHeader("token", "token-value")
.addHeader("MD5", md5)
.method(request.method(), request.body())
.build()
return chain.proceed(requestBuilder);
}
}
var httpBuild = OkHttpClient.Builder()
.connectTimeout(HttpManager.connectTimeout, TimeUnit.MILLISECONDS)
.readTimeout(HttpManager.readTimeout, TimeUnit.MILLISECONDS)
.writeTimeout(HttpManager.writeTimeout, TimeUnit.MILLISECONDS)
// .addInterceptor(headInterceptor)
HttpManager.instance.init(baseUrl, httpBuild)
}
大家对于 retrofit ,okhttp 的优秀应用
这里我只记录在做网路开发中大伙做过的有意思的处理
- 并发 token 的处理
这位兄弟的做法是利用了拦截器,在请求时判断 head 里面的 token 和当前存储的 token 一样不一样,不一样的话把新的 token 写进 head 再请求
判断 response 的 code 要是 token 过期的话,用 synchronized 同步代码块先锁死网络请求,然后启动一个申请新的 token 的网络操作,在结果回来后,刷新本地记录的 token ,再重新进行请求
我是不太赞同这样的做法的,太耗时了,用户在不知情的情况下可能要等待很久,另外申请新 token 请求要是不成功呢,怎么处理
- 图片上传
图片上传看下面这个例子,知乎图片选择器 + uri 转 file + 鲁班压缩,这一套下来非常 nice
- 对 OKHttp 缓存的实战处理
这个还是值得看的,经过实战,有的点作者会指出来
- retrofit 封装方案
- RxHttp 我看到的非常好的再封装库