文章目录:
- 前言
- 封装成果
- 封装细节
- 如何使用
- 注意
- 作者
写在前面
集android技术于一体,你们想要的都在这里
项目 Github:
初衷:
之所以写[从零开始]这系列,是技术更新的太快,导致以前的一些编码和设计不在适用目前公司情况,从零开始搭建起一个更适用目前公司的框架,但是更重要的一点是在一个团队里快速上手和熟悉一个新的各类框架,以及简单原理.常言之授人予鱼不如授人与渔,在这里也是为了记录这过程,也希望能给到大家一点帮助
为什么要封装适合自己公司的网络请求框架
虽然在是世面上有很多的成熟的网络请求框架
比如:被Google在android6.0移除的HttpClent
,以及最古老也久远的HttpUrlConnection,被我们抛弃的android-async-http,还有慢慢淡出我们视线的XUtil,2013年Google I/O大会上推出Volley 到现在的Retrofit +okHttp ,okGo,NoHttp等等,(这里要感谢这些开源贡献者,为Android生态注入了新鲜的血液),但是属于适合自己的项目的可以是简单封装过的HttpClient, HttpUrlConnection,okhttp也可以是开箱即用的android-async-http,XUtil,Volley,Retrofit,okGo,NoHttp等等,我想说的是适合项目的是最好,这里给大家做出一些参考
项目选择请求框架: Retrofit
理由:
可以使用不同的反序列化工具(Converter),比方说json, protobuff, xml, moshi等等
注:
RESTful:是指一种软件架构风格,设计风格而不是标准,只是提供了一组设计原则和约束条件组合使用
想要用Retrofi框架,最好是和RxJava联用,否者和普通的网络框架没有优势
注:
RxJava:是一个实现异步操作的库
所以使用的是Retrofit+Rxjava +Okhttp+Gson这样的一个组合进行封装,从请求到异步到解析都有了
导入依赖
在adle添加引用
`/*rx-android-java*/compile java2:rxandroid:+'compile java2:rxjava:+'compile 'permissions2:rxpermissions:+'compile llo:rxlifecycle-components:+'/*rotrofit*/compile 'fit2:retrofit:+'compile 'fit2:converter-gson:+'compile 'fit2:adapter-rxjava2+'compile le.code.gson:gson:+'
(这里算是一个小技巧吧)为了以后拓展和更换网络框架会把网络请求封装成一个Helper类并实现IDataHelper接口接口
内容为将要对外暴露的方法等.
public interface IDataHelper {void init(Context context);<S> S getApi(Class<S> serviceClass);<S> S createApi(Class<S> serviceClass);<S> S getApi(Class<S> serviceClass, OkHttpClient client);<S> S createApi(Class<S> serviceClass, OkHttpClient client);OkHttpClient getClient();.....
}
(如果还没有使用过Dagger2的同学就跳过吧)如果项目中使用了Dagger2依赖注入的话好处就体现出来了,以后更换的话框架的话只需要更改IDataHelper的实现类就行了不用修改其他的代码,在通过注入的方式注入到各个模块中去.如下图:
或者使用简单工厂设计模式
public class DataHelperProvider {private static IDataHelper dataHelper;public static IDataHelper getHttpHelper(Context context) {if (dataHelper == null) {synchronized (DataHelperProvider.class) {if (dataHelper == null) {dataHelper = new HttpHelper(context);}}}return dataHelper;}.....}
更换的时候只需要更改IDataHelper的实现
HttpHelper分为 :
请求相关:
Okhttp基础相关的设置:
常规设置: 连接超时,cacheInterceptor缓存处理
个性化设置: ClearableCookieJar管理,优化防重复,https,定制请求头,定义请求体(加密,解密,)
public OkHttpClient getOkHttpClient() {ClearableCookieJar cookieJar =//对cooke自动管理管理new PersistentCookieJar(new SetCookieCache(), new SharedPrefsCookiePersistor(context));File cacheFile = new ExternalCacheDir(), "KairuCache");//缓存路径Cache cache = new Cache(cacheFile, 1024 * 1024 * 40);//设置缓存大小为40M//缓存CacheInterceptor cacheInterceptor = new CacheInterceptor(context);//token管理TokenInterceptor tokenInterceptor = new TokenInterceptor();OkHttpClient.Builder builder =new OkHttpClient.Builder().cache(cache)//缓存大小的设置.addInterceptor(cacheInterceptor) //对api缓存设置.addNetworkInterceptor(cacheInterceptor)//对api缓存设置.retryOnConnectionFailure(true) //是否失败重新请求连接.connectTimeout(15, TimeUnit.SECONDS)//连接超时.writeTimeout(3, TimeUnit.SECONDS)//写超时超时.readTimeout(3, TimeUnit.SECONDS)//读超时.addInterceptor(tokenInterceptor)//token请求头相关设置.cookieJar(cookieJar);//cookie的本地保存管理if (AppUtil.isDebug()) {//如果当前是debug模式就开启日志过滤器HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);builder.addInterceptor(loggingInterceptor);}//当前okHttpClientokHttpClient = builder.build();return okHttpClient;}
==注意== :
- retryOnConnectionFailure(true)
方法这里的失败重连的机制是:只要网络请求失败后就会一直重连并不能满足重连次数和时长的产品需求,实际项目中发现并不是那么使用和灵活,所以这里我还是放弃了该方法,(后续也许讲到这里)自己用Rxjava重新设计了一个能提供重连次数和重连时长的一个方法. (这里的话还是要看实际的需求)
com.github.franmontiel:PersistentCookieJar:v1.0.0
但是没听见但是吗? 实际上服务器安全认证的方式多种多样,Token提交存放方式也是多种多样的,花样百出,有没有token的,有直接写死,有放在Head里面的,有当请求参数的,这些情况下Token过期了之后欧就需要自己去根据实际情况去刷新获取Token,往后的话也会一一介绍大部分的的技巧
public Retrofit getRetrofit(String host) {if (gson == null)gson = new GsonBuilder().create();//Gson解析if (okHttpClient == null)okHttpClient = getOkHttpClient();retrofit = new Retrofit.Builder().baseUrl(host)//baseurl路径.client(okHttpClient)//添加okHttpClient客户端.addConverterFactory(new StringConverterFactory())//添加String格式化工厂.ate(gson))//添加Gson格式化工厂.ate()).build();return retrofit;}
==说明== : 这里提供了两种数据解析方式: 解析为String类型和使用Gson解析成对应的Bean以便对应开发的实际需求,这里也可以对Adapter再深一步的拓展在此就先不深入了
public <S> S createApi(Class<S> serviceClass, OkHttpClient client) {String baseURL = "";try {Field field1 = Field("baseURL");baseURL = (String) (serviceClass);} catch (NoSuchFieldException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.getMessage();e.printStackTrace();}if(retrofit!=null&&retrofit.baseUrl().host()==baseURL){return ate(serviceClass);}else{return getRetrofit(baseURL).create(serviceClass);}}
==说明== : 这里利用了反射原理拿到每个ApiService中写好的BaseUrl,如果没有的话就会使用同一的BaseUrl进行请求
统一处理请求数据格式:
如果说后台服务器返回的数据格式非常的规范的话,就可以尝试将将一些公共部分抽取出来, 比如 : Code (这里Code指的是业务代码) 和 ErrorMessage(错误提示) 获取其他的封装到一个基类,这样的好处:
public class ApiResponse<T> {public T data;public String code;public String message;... get() set()
}
举个简单例子 :
比如 :
{code:200 ,message: success,loginInfo :{...}}
这里只需要写成,ApiResponse,实际的数据LoginInfoBean就在T data 里获取使用就行了了,对于Code在上一层就过滤处理完了.如果需要的话可以把泛型再抽出一个基类来,防止后台数据千变万化呀
/**public static <T extends ApiResponse> rxSchedulerHelper<T, T> rxSchedulerHelper(int count,long delay) { //compose简化线程return new FlowableTransformer<T, T>() {@Overridepublic Flowable<T> apply(Flowable<T> upstream) {return upstream.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());}};}
BaseSubscriber继承Rxjava2.0上ResourceSubscriber,这些都是支持背压的,
像业务状态码统一处理,异常处理,Checkout,控制菊花等处理都放在了BaseSubscriber中去处理了,直接上代码了也是比较简单的.
public class BaseSubscriber<T extends ApiResponse> extends ResourceSubscriber<T> implements ProgressCancelListener {private static final String TAG = "BaseSubscriber";private SubscriberListener mSubscriberOnNextListener;private ProgressDialogHandler mHandler;Context aContext;/*** 该构造会出现一个自动弹出和消失的dialog,一般使用与通用情况,特殊情况请自行处理,也可以通过{@link SubscriberListener#isShowLoading()方法控制}** @param mSubscriberOnNextListener* @param aContext*/public BaseSubscriber(SubscriberListener mSubscriberOnNextListener, Context aContext) {this.mSubscriberOnNextListener = mSubscriberOnNextListener;mHandler = new ProgressDialogHandler(aContext, this, false);this.aContext = aContext;}/*** 使用该构造方法没有LoadingDialog** @param mSubscriberOnNextListener*/public BaseSubscriber(SubscriberListener mSubscriberOnNextListener) {this.mSubscriberOnNextListener = mSubscriberOnNextListener;}@Overrideprotected void onStart() {Start();if (!NetworkUtil.())){ToastUtil.(),"网络错误,请检查你的网络");if (isDisposed())this.dispose();return;}if (mSubscriberOnNextListener != null && mSubscriberOnNextListener.isShowLoading())showProgressDialog();onBegin();}/*** 订阅开始时调用* 显示ProgressDialog*/public void onBegin() {Log.i(TAG, "onBegin");if (mSubscriberOnNextListener != null) {Begin();}}private void showProgressDialog() {if (mHandler != null) {mHandler.obtainMessage(ProgressDialogHandler.SHOW_PROGRESS_DIALOG).sendToTarget();}}private void dismissProgressDialog() {if (mHandler != null) {mHandler.obtainMessage(ProgressDialogHandler.DISMISS_PROGRESS_DIALOG).sendToTarget();mHandler = null;}}/*** 对错误进行统一处理* 隐藏ProgressDialog** @param e*/@Overridepublic void onError(Throwable e) {Log.i(TAG, "onError:" + e.toString());if (mSubscriberOnNextListener != null) {Error(e);}onComplete();}/*** 完成,隐藏ProgressDialog*/@Overridepublic void onComplete() {Log.i(TAG, "onCompleted");if (mSubscriberOnNextListener != null && mSubscriberOnNextListener.isShowLoading())dismissProgressDialog();if (mSubscriberOnNextListener != null) {Completed();}if (!this.isDisposed()) {this.dispose();}}/*** 将onNext方法中的返回结果交给Activity或Fragment自己处理,可以根据实际情况再封装** @param response 创建Subscriber时的泛型类型*/@Overridepublic void onNext(T response) {Log.i(TAG, "onNext");if (mSubscriberOnNextListener != null) {if (Constants.SUCCEED.de)) {//成功Success(response.data);} else if (Constants.STATUS_RE_LOGIN.de) || Constants.STATUS_NO_LOGIN.de))//未登录或者登陆过期 {//判断是否需要重新登录mSubscriberOnNextListener.de, ssage);} else {//业务异常或者服务器异常de, ssage);}}}@Overridepublic void onCancelProgress() {//取消菊花的转动if (isDisposed())this.dispose();if (mHandler != null)mHandler = null;}
}
虽然到这里一些常用网络请求的功能也差不多了,但是在配合Rxjava使用的时候会出现一些小问题,下面将给出一点点的小技巧
public abstract class KrSubscriberListener<T> extends SubscriberListener<T> {public void onFail(String errorCode, String errorMsg) {//todo}@Overridepublic void onError(Throwable e) {//这里对异常重新定义,分为网络异常,解析异常业务异常等NetError error = null;if (e != null) {if (!(e instanceof NetError)) {if (e instanceof UnknownHostException) {error = new NetError(e, NetError.NoConnectError);} else if (e instanceof JSONException|| e instanceof JsonParseException|| e instanceof JsonSyntaxException) {error = new NetError(e, NetError.ParseError);} else if (e instanceof SocketException|| e instanceof SocketTimeoutException) {error = new NetError(e, NetError.SocketError);} else {error = new NetError(e, NetError.OtherError);}} else {error = (NetError) e;}Type(), Message());}}@Overridepublic void checkReLogin(String errorCode, String errorMsg) {//todo}
}
上面也说了认证和刷新Token方式有很多种,这里的话就挑常用的几种来说明
1 . 第一种方案
通过okhttp提供的Authenticator接口,但是查看okhttp的源码会发现,只有返回HTTP的状态码为401时,才会使用Authenticator接口,如果服务端设计规范,可以尝试如下方法。
实现Authenticator接口
==注意== : 这里要清楚401指的是HTTP状态码
public class TokenAuthenticator implements Authenticator {@Overridepublic Request authenticate(Proxy proxy, Response response) throws IOException {//取出本地的refreshToken,如果该Token过期的话就要重新登录了String refreshToken = "XXXXXXXXXXXXXXXXXXX";// 通过一个特定的接口获取新的token,此处要用到同步的retrofit请求HttpHelper httpHelper= new HttpHelper(application);ApiService service = Api(ApiService.class);Call<String> call = freshToken(refreshToken);//使用retrofit的同步方式String newToken = ute().body();quest().newBuilder().header("token", newToken).build();}@Overridepublic Request authenticateProxy(Proxy proxy, Response response) throws IOException {return null;}
}
然后给添加给OkHttpClient,这样就对Token的刷新OkHttpClient就自动搞定了,刷新完Token后okhttp也会再次发起请求
OkHttpClient client = new OkHttpClient();
client.setAuthenticator(new TokenAuthenticator());
2 . 第二种方案
但是如果服务器不能返回401**HTTP状态码**的话,那么上面的方案就不能解决问题了
有些时候后台会把Token过期归为业务异常,所以出现了服务器返回200成功的HTTP状态码,但是返回了401的业务Code码,比如:
{"data":{//什么都没有返回},"message":"success""code":401 //这里是业务Code代表Token过期了,没有返回任何数据,需要刷新Token,每个服务器定义的TokenCode值也是不一样的}
通过okhttp的拦截器,okhttp 2.2.0 以后提供了拦截器的功能
public class TokenInterceptor implements Interceptor {@Overridepublic Response intercept(Chain chain) throws IOException {Response response = chain.proceed(request);Request.Builder requestBuilder = wBuilder();String access_token = (String) ((),"access_token","");if(!TextUtils.isEmpty(access_token)){requestBuilder.addHeader("Authentication", access_token);}if (isTokenExpired(response)) {//根据和服务端的约定判断token过期//同步请求方式,获取最新的Tokenaccess_token = getNewToken();requestBuilder.header("Authentication", access_token);}Request request = requestBuilder.build();return chain.proceed(request);}/*** 根据Response,判断Token是否失效** @param response* @return*/private boolean isTokenExpired(Response response) {if (de() == 401) {return true;}return false;}/*** 同步请求方式,获取最新的Token** @return*/private String getNewToken() throws IOException {// 通过一个特定的接口获取新的token,此处要用到同步的retrofit请求String refreshToken = "XXXXXXXXXXXXXXXXXXX";HttpHelper httpHelper= new HttpHelper(application);ApiService service = Api(ApiService.class);Call<String> call = freshToken(refreshToken);//使用retrofit的同步方式String newToken = ute().body();SPHelper.(),"access_token",newToken)//存本地return newToken;}
}
3 . 当然还有其他的解决方式,只要能在统一请求回来之后,需要再次请求刷新Token时候,请求服务器就行,重点是要抓住这个时机,比如在Gson解析的时候修改GsonConverterFactory,判断业务Code再刷新Token,或者是在借助RxJava的flatMap实现都是同样的原来.这些方法不同的就是这个时机点可以是:okhttp拦截器 ,也可以是Gson解析的时候,也可以在Gson解析后的配合Rxjava的flatMap是一样的
public class StringConverterFactory<T> implements Converter<ResponseBody, Object> {private final TypeAdapter<T> adapter;StringConverterFactory(TypeAdapter<T> adapter) {this.adapter = adapter;}@Overridepublic Object convert(ResponseBody value) throws IOException {try {String aseKey= AesSecretKey();KLog.d("解密秘钥aseKey = " + aseKey);String data = null;try {data = AES.dencrypt(value.string(), aseKey);KLog.e("返回数据:"+data);BaseResponse baseResponse = new Gson().fromJson(data, BaseResponse.class);if(!StringUtils.NewAesKey())){//保证AESkey在缓存中KLog.i("获得新的key====="NewAesKey());//缓存中获取秘钥UserCache.NewAesKey());}} catch (Exception e) {e.printStackTrace();}} finally {value.close();}}
}
==注意==:这里也可以用来重新刷新Token就看个人习惯和项目实际的需求吧
虽然说在Okhttp基础相关的设置中有涉及到说失败重连的问题,但是不能满足实际开发,接下来就使用Rxjava来进行改造,就在线程调度基础上进行改造.如下:
/*** 统一线程处理** @param <T>* @return*/public static <T extends ApiResponse> FlowableTransformer<T, T> rxSchedulerHelper() { //compose简化线程return rxSchedulerHelper(3,5);}/*** 统一线程处理和失败重连* @param count 失败重连次数* @param delay 延迟时间* @param <T> 返回数据data实际的 数据* @return 返回数据data实际的 数据*/public static <T extends ApiResponse> FlowableTransformer<T, T> rxSchedulerHelper(int count,long delay) { //compose简化线程return new FlowableTransformer<T, T>() {@Overridepublic Flowable<T> apply(Flowable<T> upstream) {return upstream.subscribeOn(Schedulers.io()).flatMap(new Function<T, Flowable<T>>() {@Overridepublic Flowable<T> apply(T t) throws Exception {//TODO 做些对返回结果做一些操作return Flowable.just(t);}}).retryWhen(new RetryWhenHandler(count, delay)).observeOn(AndroidSchedulers.mainThread());}};}public class RetryWhenHandler implements Function<Flowable<? extends Throwable>, Flowable<?>> {private int mCount = 3;private long mDelay = 3; //sprivate int counter = 0;public RetryWhenHandler() {}public RetryWhenHandler(int count) {this.mCount = count;}public RetryWhenHandler(int count, long delay) {this(count);this.mCount = count;this.mDelay = delay;}@Overridepublic Flowable<?> apply(Flowable<? extends Throwable> flowable) throws Exception {return flowable.flatMap(new Function<Throwable, Flowable<?>>() {@Overridepublic Flowable<?> apply(Throwable throwable) throws Exception {if (counter < mCount && (throwable instanceof UnknownHostException|| throwable instanceof SocketException|| throwable instanceof HttpException)) {counter++;return Flowable.timer(mDelay, TimeUnit.SECONDS);} else if ((counter < mCount && throwable instanceof NullPointerException&& Message() != null) {counter++;return Flowable.timer(0, TimeUnit.SECONDS);}(throwable);}});}
}
==说明== : 这里的话是利用Rxjava中的retryWhen()方法来实现,当发现抛出的异常是属于网络异常就进行再次请求,这样对重连次数以及时长的控制(这里默认的话是重连三次,并且每次重连事件为3s,这里可以根据具体的情况拟定),如果还么有了解Rxjava的童靴们赶紧学起来吧.
服务器提供了文件系统该怎么上传多文件
有两种方式
- 使用List
/*** 将文件路径数组封装为{@link List<MultipartBody.Part>}* @param key 对应请求正文中name的值。目前服务器给出的接口中,所有图片文件使用<br>* 同一个name值,实际情况中有可能需要多个* @param filePaths 文件路径数组* @param imageType 文件类型*/public static List<MultipartBody.Part> files2Parts(String key,String[] filePaths, MediaType imageType) {List<MultipartBody.Part> parts = new ArrayList<>(filePaths.length);for (int i = 0; i <filePaths.length ; i++) {File file = new File(filePaths[i]);// 根据类型及File对象创建RequestBody(okhttp的类)RequestBody requestBody = ate(imageType, file);// 将RequestBody封装成MultipartBody.Part类型(同样是okhttp的)MultipartBody.Part part = ateFormData(key+i, Name(), requestBody);// 添加进集合parts.add(part);}return parts;}/*** 其实也是将File封装成RequestBody,然后再封装成Part,<br>* 不同的是使用MultipartBody.Builder来构建MultipartBody* @param key 同上* @param filePaths 同上* @param imageType 同上*/public static MultipartBody filesToMultipartBody(String key,String[] filePaths,MediaType imageType) {MultipartBody.Builder builder = new MultipartBody.Builder();for (int i = 0; i <filePaths.length ; i++) {File file = new File(filePaths[i]);RequestBody requestBody = ate(imageType, file);builder.addFormDataPart(key+i, Name(), requestBody);}builder.setType(MultipartBody.FORM);return builder.build();}
然后使用Retrofit定义好Api
public interface FileUploadApi {String baseURL= Constants.MOCK_FILE_UPLOAD;/*** 注意1:必须使用{@code @POST}注解为post请求<br>* 注意:使用{@code @Multipart}注解方法,必须使用{@code @Part}/<br>* {@code @PartMap}注解其参数<br>* 本接口中将文本数据和文件数据分为了两个参数,是为了方便将封装<br>* {@link MultipartBody.Part}的代码抽取到工具类中<br>* 也可以合并成一个{@code @Part}参数* @param params 用于封装文本数据* @param parts 用于封装文件数据* @return BaseResp为服务器返回的基本Json数据的Model类*/@Multipart@POST()Flowable<ApiResponse<UploadBackBean>> requestUploadWork(@Url String url,@PartMap Map<String, RequestBody> params,@Part() List<MultipartBody.Part> parts);/*** 注意1:必须使用{@code @POST}注解为post请求<br>* 注意2:使用{@code @Body}注解参数,则不能使用{@code @Multipart}注解方法了<br>* 直接将所有的{@link MultipartBody.Part}合并到一个{@link MultipartBody}中*/@POST()Flowable<ApiResponse<UploadBackBean>> requestUploadWork(@Url String url,@Body MultipartBody body);}
最后使用;
public void requestUploadWork(String[] files) {List<MultipartBody.Part> parts = UploadUtil.files2Parts("file", files, MediaType.parse("multipart/form-data"));Api(FileUploadApi.class).requestUploadWork(Constants.BASE_URL+"file/upload/img",parts)pose(RxUtil.<ApiResponse<String>>rxSchedulerHelper(0, 5)).subscribe(new BaseSubscriber<ApiResponse<String>>(new KrSubscriberListener<String>() {@Overridepublic void onSuccess(String response) {//todo}@Overridepublic void onFail(String errorCode, String errorMsg) {Fail(errorCode, errorMsg);//todo}}));
}public void requestUploadWork(String[] files) {MultipartBody parts = UploadUtil.filesToMultipartBody("file", files, MediaType.parse("multipart/form-data"));Api(FileUploadApi.class).requestUploadWork(Constants.BASE_URL+"file/upload/img",parts).requestUploadWork(Constants.BASE_URL+"file/upload/img",parts)pose(RxUtil.<ApiResponse<String>>rxSchedulerHelper(0, 5)).subscribe(new BaseSubscriber<ApiResponse<String>>(new KrSubscriberListener<String>() {@Overridepublic void onSuccess(String response) {//todo}@Overridepublic void onFail(String errorCode, String errorMsg) {Fail(errorCode, errorMsg);//todo}}));;
}
这样就实现了多文件的上传
最后
对于前面的一些常规设置也提供了拓展
项目地址:
Flyabbit
该项目也会随着文章的更新而更新敬请关注
Github:
CSDN :
个人博客 : /
最后感谢 : 代码家 ,以及一些大佬的帮助
参考 :
本文发布于:2024-02-01 21:44:42,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/170679508339607.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |