Android编程之接口数据处理
来源:互联网 发布:java ocr身份证识别 编辑:程序博客网 时间:2024/04/25 22:00
作为一枚android程序员,处理API数据接口可谓家常便饭,既然是一项不可少的事,那就把它做好。本篇博客的内容也是我近期做的项目中模块的一隅,代码组织的好与不好暂且不论,首先解决问题才是最应该被关注的,当然如果同学们有好的意见也随时跟我沟通。我先声明这篇博客不会去分析某个网络请求框架,也不会从头开始写一个网络请求框架,我们应该注重实效,并不是我不建议同学去分析某个知名的框架,恰恰相反我很鼓励同学们去分析精品框架源码并尝试写一些框架。不罗嗦了,下面进入本文正题。
制定规约
首先要明白一点,我们客户端从提供数据的后台请求数据的规约必须先明确之,其中包括是采用SOAP Webservice还是Restful Webservice,采用XML协议作为数据交换格式还是采用JSON协议作为数据交换格式,接口返回的数据结构是什么样子的,具体每一个接口请求方式是什么。我目前的项目是采用Restful Webservice,交换数据格式为JSON。接口返回数据统一格式是这样的:
{ status:xxx msg:xxx result:xxx }
当然这些数据是在一次请求成功后才能获得,先不考虑获取不到数据的情况,请求异常情况后文再做进一步探讨;status表明一次请求的状态,一般是指业务层级上的状态值,msg记录一次请求的说明信息文本,可直接在UI显示(建议采用这种方式,可后台灵活配置),result存储某一次请求所返回的业务实体或者业务实体集合,当然也有例外,暂且不议。
解决方案
既然已经和后台制定好了规约,那接下来的工作就是客户端如果实施的问题。网络请求框架本文采用Square开源的Retrofit框架,对它还比较陌生的同学请阅读一下说明文档,接口返回数据解析采用的Google开源的Gson框架;关于如何配置Retrofit,我想同学同学们心里都有一个考量标准,以下是我目前项目的使用Retrofit的具体配置:
@Singletonpublic class ApiConnector { private final Interceptor mSignInterceptor; private Retrofit mRetrofit; private OkHttpClient okHttpClient; @Inject public ApiConnector(Interceptor signInterceptor) { mSignInterceptor = signInterceptor; init(); } private void init() { HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(); loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); okHttpClient = getHttpBuilder() .addInterceptor(loggingInterceptor) .addInterceptor(mSignInterceptor) .connectTimeout(NetworkConfig.REQUEST_TIME_OUT_DURATION, TimeUnit.SECONDS) .build(); mRetrofit = new Retrofit.Builder() .baseUrl(getApiBaseUrl()) .client(okHttpClient) .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .addConverterFactory(GsonConverterFactory.create(new Gson())) .build(); } protected OkHttpClient.Builder getHttpBuilder() { return new OkHttpClient.Builder(); } protected String getApiBaseUrl() { return NetworkConfig.BASE_API_URL; } public Retrofit getApiCreator() { return mRetrofit; }}
首先构造函数中我们传入Interceptor某个实现对象,看到对象名signInterceptor就应该能猜到它是对请求进一步签名,接着分析init/0函数,在函数中我们又定义一个HttpLoggingInterceptor对象,看类名就该知道它是一个Http request日志拦截器,升级版的Retrofit2具有更高的可配置性,可将OkHttp作为Retrofit的Request client,OkHttp提供可配置的拦截器,HttpLoggingInterceptor是OkHttp提供的也可根据具体需求自定义实现,处理结果大致是这样的:
我想多半app都会收集一些用户的信息,我项目中叫做KPI,我项目中是通过自定义拦截器做统一收集,也就是前文提到的SignInterceptor,具体实现如下:
@Singletonpublic final class SigningInterceptor implements Interceptor { private Context mContext; private AccountEntity mAccountEntity; private AuthToken mAuthToken; @Inject AccountCache mAccountCache; @Inject AuthTokenCache mAuthTokenCache; @Inject SigningInterceptor(Context context) { mContext = context.getApplicationContext(); } @Override public Response intercept(Chain chain) throws IOException { if (null == mAccountEntity) mAccountEntity = mAccountCache.get(); if (null == mAuthToken) mAuthToken = mAuthTokenCache.getAuthToken(); Request originalRequest = chain.request(); Request.Builder requestBuilder = originalRequest.newBuilder() .addHeader(ParamConstants.KPI.CLIENT_TYPE, ParamConstants.Value.CLIENT) .addHeader(ParamConstants.KPI.FIRST_SRC, ClientUtil.getFirstSrc(mContext , ParamConstants.KPI.CHANNEL_PLACE_HOLDER)) .addHeader(ParamConstants.KPI.LAST_SRC, ClientUtil.getLastSrc(mContext , ParamConstants.KPI.CHANNEL_PLACE_HOLDER)) .addHeader(ParamConstants.KPI.VERSION_NAME, ManifestUtils.getVersionName(mContext)) .addHeader(ParamConstants.KPI.VERSION_CODE, String.valueOf(ManifestUtils.getVersionCode(mContext))) .method(originalRequest.method(), originalRequest.body()); if (null != mAccountEntity) requestBuilder.addHeader(ParamConstants.KPI.ACCOUNT_ID, String.valueOf(mAccountEntity.getAccountId())); if (null != mAuthToken) requestBuilder.addHeader(ParamConstants.KPI.TOKEN, String.valueOf(mAuthToken.getToken())); return chain.proceed(requestBuilder.build()); }}
很容易发现Interceptor采用是责任链模式,Request.Builder也就是Builder模式,不熟悉的同学们请自行补脑,这里我们只需要在intercept/0函数中将所需要统计的KPI数据加入Request的Header中即可。大家先不要关注@Inject这个注解,以后我会说明用法,现在只需要知道它能通过配置把AccountCache、AuthTokenCache具体实现对象注入到需要它的对象中,也就是依赖注入。细心的同学应该能注意到在ApiConnector中getApiBaseUrl/0、getHttpBuilder/0函数访问级别是protected,而且会有疑惑为什么在baseUrl/0不直接使用NetworkConfig.BASE_API_URL而非要多此一举呢,我的项目所有账号系统都是通过Https方式传输,之所以ApiConnector不是final修饰getApiBaseUrl/0、getHttpBuilder/0函数访问级别是protected的原因所在了,我们看下ApiConnector的扩展,代码如下:
@Singletonpublic final class ApiSSLConnector extends ApiConnector { private static List<byte[]> CERTIFICATES_DATA = new ArrayList<>(); private Context mContext; @Inject public ApiSSLConnector(Context context, Interceptor signInterceptor) { super(signInterceptor); mContext = context; initHttpCerts(); } private void initHttpCerts() { try { String[] certFiles = mContext.getAssets().list("certs"); if (certFiles != null) { for (String cert : certFiles) { InputStream is = mContext.getAssets().open("certs/" + cert); addCertificate(is); } } } catch (IOException e) { } } private synchronized static void addCertificate(InputStream inputStream) { if (null != inputStream) { try { int ava; int len = 0; ArrayList<byte[]> data = new ArrayList<>(); while ((ava = inputStream.available()) > 0) { byte[] buffer = new byte[ava]; inputStream.read(buffer); data.add(buffer); len += ava; } byte[] buff = new byte[len]; int dstPos = 0; for (byte[] bytes : data) { int length = bytes.length; System.arraycopy(bytes, 0, buff, dstPos, length); dstPos += length; } CERTIFICATES_DATA.add(buff); } catch (IOException e) { } } } private static List<byte[]> getCertificatesData() { return CERTIFICATES_DATA; } @Override protected OkHttpClient.Builder getHttpBuilder() { List<InputStream> certificates = new ArrayList<>(); List<byte[]> certsData = getCertificatesData(); if (certsData != null && !certsData.isEmpty()) { for (byte[] bytes : certsData) { certificates.add(new ByteArrayInputStream(bytes)); } } HttpsManager.SSLParams sslParams = HttpsManager.getSslSocketFactory(certificates, null, null); OkHttpClient.Builder builder = new OkHttpClient.Builder(); builder.sslSocketFactory(sslParams.sSLSocketFactory, sslParams.trustManager) .hostnameVerifier(new HttpsManager.UnSafeHostnameVerifier()); return builder; } @Override protected String getApiBaseUrl() { return NetworkConfig.SSL.BASE_API_URL; }}
这里需要注意一点,如果客户端和服务端采用非单向验证,那么getSslSocketFactory/3函数后两个参数必须要传入对应的参数信息。
了解Retrofit的同学会知道,使用Retrofit访问网络接口需要定义Service接口,该类型接口需要明确客户端需要请求哪些接口,哪种方式请求、请求参数、返回信息,下面是一个精简后的实例:
public interface AccountService { @PUT("/user/sendSMS/{mobile}") Observable<DataResponse<String>> getVerifyCode(@Path("mobile") String phoneNumber, @Body Map<String, String> verify); @GET("/user/detail/{accountId}") Observable<DataResponse<AccountEntity>accountDetail(@Path("accountId") int accountId);}
多了两个陌生的面孔DataResponse和Observable;先说下DataResponse,前文说道服务端返回的数据格式为:
{ status:xxx msg:xxx result:xxx }
我们不可能把每个业务bean定义都wrapper成这样子的吧:
public final class AccountWrapper{ private String status; private String msg; private Account result;}
显然这样做是不合理的,格式确定下来了,我们完全可以通过泛型解决这个问题,贴上干巴巴代码:
public final class DataResponse<T> { @SerializedName("status") private int status; @SerializedName("msg") private String message; @SerializedName("result") private T result; public int getStatus() { return status; } public void setStatus(int status) { this.status = status; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public T getResult() { return result; } public void setResult(T result) { this.result = result; }}
Observable是RxJava提供的可观察对象,关于RxJava的介绍本文不做探究,本文默认同学们已经会RxJava的基本使用,我会更新关于RxJava分析和使用的博客。在此基础上我们需要对DataResponse进行处理,简化后的处理代码如下:
public final class ResponseFlatResult { public static <T> Observable<T> flatResult(final DataResponse<T> result) { return Observable.create(new Observable.OnSubscribe<T>() { @Override public void call(Subscriber<? super T> subscriber) { switch (result.getStatus()) { case NetworkConstants.SUCCESS_CODE: if (null != result.getResult()) subscriber.onNext(result.getResult()); break; case NetworkConstants.FEED_NOT_FOUND_EXCEPTION: subscriber.onError(new FeedNotFoundException(result.getMessage())); break; default: subscriber.onError(new BusinessException(result.getMessage())); } subscriber.onCompleted(); } });}
flatResult/0函数用意很明确就是要在上层对返回数据处理后再往下分发,当status为200同时result不为空时notify订阅者,其它状态均视为异常情况,也就是触发订阅者的onError/0回调函数。
接下来如何触发一次网络请求呢?我采用的方式如下:
@Singletonpublic final class CloudAccountDataStore implements AccountDataStore { private final AccountService mAccountService; private final TokenInteractor mTokenInteractor; private final AccountCache mAccountCache; private final AuthTokenCache mAuthTokenCache; @Inject public CloudAccountDataStore(ApiSSLConnector apiConnector, TokenInteractor tokenInteracto , AccountCache accountCache , AuthTokenCache authTokenCache) { mAccountService = apiConnector.getApiCreator().create(AccountService.class); mTokenInteractor = tokenInteractor; mAccountCache = accountCache; mAuthTokenCache = authTokenCache; } @Override public Observable<AccountEntity> getAccountEntityDetail(final AccountParamProvider accountParamProvider) { return mAccountService.accountDetail(accountParamProvider.getAccountId()) .flatMap(new Func1<DataResponse<AccountEntity>, Observable<AccountEntity>>() { @Override public Observable<AccountEntity> call(DataResponse<AccountEntity> accountEntityBaseResponse) { return ResponseFlatResult.flatResult(accountEntityBaseResponse); } }) .onErrorResumeNext(mTokenInteractor.refreshTokenAndRetry(Observable.defer(new Func0<Observable<AccountEntity>>() { @Override public Observable<AccountEntity> call() { return mAccountService.accountDetail(accountParamProvider.getAccountId()) .flatMap(new Func1<DataResponse<AccountEntity>, Observable<AccountEntity>>() { @Override public Observable<AccountEntity> call(DataResponse<AccountEntity> accountEntityBaseResponse) { return ResponseFlatResult.flatResult(accountEntityBaseResponse); } }); } }))); }}
还是先看一下CloudAccountDataStore的构造函数,其接受必要的4个参数,ApiSSLConnector就是上文定义提供Retrofit对象的Connector,由于是Account的相关的DataStore所以这里使用的是ApiSSLConnector而不是ApiConnector,在解释TokenInteractor的用途前先简单说下我目前项目客户端和服务端的请求验证机制,Token同学们都应该了解,业务层面上客户端和服务器请求的令牌,服务端根据Token来判定请求的合法性。用户一次登录操作获取Token,之后的相关请求附带Token参数,但是Token又不可能再下次登录前永远有效,这样就失去Token的意图,所以我项目中采用Token和Retoken机制,过期时间分别位3天和15天,Token过期后用Retoken去请求一个新的Token,当Retoken也过期时客户端重新登录,用Retoken更新Token的过程肯定是用户无感知的,所以就引入了TokenInteractor,具体实现如下:
@Singletonpublic final class TokenInteractor { private final AuthTokenService mAuthTokenService; private final AuthTokenCache mAuthTokenCache; @Inject RxBus mRxBus; @Inject public TokenInteractor(ApiConnector apiConnector, AuthTokenCache authTokenCache) { mAuthTokenService = apiConnector.getApiCreator().create(AuthTokenService.class); mAuthTokenCache = authTokenCache; } public <T> Func1<Throwable, ? extends Observable<? extends T>> refreshTokenAndRetry(final Observable<T> toBeResumed) { return new Func1<Throwable, Observable<? extends T>>() { @Override public Observable<? extends T> call(Throwable throwable) { if (isHttp401Error(throwable)) { return refreshToken() .doOnError(new Action1<Throwable>() { @Override public void call(Throwable throwable) { if (isHttp403Error(throwable)) mRxBus.post(new UnauthorizedEvent()); } }) .doOnNext(new Action1<AuthToken>() { @Override public void call(AuthToken authToken) { mAuthTokenCache.saveAuthToken(authToken); } }) .flatMap(new Func1<AuthToken, Observable<? extends T>>() { @Override public Observable<? extends T> call(AuthToken token) { return toBeResumed; } }); } return Observable.error(throwable); } }; } private Observable<AuthToken> refreshToken() { ReTokenParamProvider reTokenParamProvider = new ReTokenParamProvider(); reTokenParamProvider.reToken(mAuthTokenCache.getAuthToken().getReToken()); return mAuthTokenService.refreshAuthToken(reTokenParamProvider.getOptionalParam().getMap()) .flatMap(new Func1<DataResponse<AuthToken>, Observable<AuthToken>>() { @Override public Observable<AuthToken> call(DataResponse<AuthToken> authTokenBaseResponse) { return ResponseFlatResult.flatResult(authTokenBaseResponse); } }); } private boolean isHttp401Error(Throwable throwable) { if (throwable instanceof HttpException) { HttpException exception = (HttpException) throwable; return exception.code() == 401; } else return false; } private boolean isHttp403Error(Throwable throwable) { if (throwable instanceof HttpException) { HttpException exception = (HttpException) throwable; return exception.code() == 403; } else return false; }}
逻辑并不复杂,refreshTokenAndRetry/0 接收一个Observable对象,也就是针对某个请求的Observable对象,在遇到异常时对异常类型做判断,如果状态码401就去请求新的Token并恢复流,如果状态是403就通知客户端重新登录,回到CloudAccountDataStore继续分析getAccountEntityDetail/1函数,视角移到onErrorResumeNext操作上,这个RxJava操作符的作用是一旦源Observable遇到错误,onErrorResumeNext会把源Observable用一个新的Observable替换掉,可以看到mAccountService.accountDetail在新的Observable重新发起请求;继而是defer操作符,这个操作符与create、just、from等操作符一样,是创建类操作符,不过所有与该操作符相关的数据都是在订阅是才生效的,这就是避免了多余的订阅。
文章到此也告一段落了,如果有不明白的地方可以联系我,接下的几天我会把项目继续分解讲解,会引入MVP,Dagger2等一些新的编程方式同时会一直伴随的讲解的就是Refactor,觉得对您有所帮助的话请多关注我的博客 0.0
- Android编程之接口数据处理
- android 数据处理之 SharedPreferences
- Android网络编程之Apache接口
- python系列之数据处理编程实例
- 【Spark大数据处理技术】RDD及编程接口:(一)
- C#编程之接口
- MyBatis之接口编程
- Spring 之 接口编程
- Android 面向接口编程
- C#接口编程之接口概述
- C#接口编程之接口概述
- 剖析C#接口编程之接口概述
- Delphi 接口编程之:接口委托
- Kotlin编程之接口和实现接口
- Android学习第七周_网络编程数据处理
- geekband android #5 第七周分享(网络编程数据处理)
- 《Android之大话设计模式》设计原则 第一章:针对接口编程 不要针对实现编程
- 设计模式之接口编程
- bean->map或map->bean的工具类主要针对基本类型
- MFC中的winmain
- web安全基础
- 集合的并查
- Shell部分16
- Android编程之接口数据处理
- MySQL服务器日志(1)
- Java递归遍历删除拷贝文件以及获取文件夹大小
- C++自定义矩阵并重载“+”运算符,指针返回错误
- 网易2017秋招编程题集合
- Centos下oracle插入数据中文乱码问题
- 利用函数bin_search实现折半查找
- 前端总结之图片文字隐藏
- 一篇通俗易懂的讲解OpenGL ES的文章