概括

在上一篇博客android网络框架OkHttp之get请求(源码初识) 讲解了OkHttp的简单使用和OkHttp源码的分析,主要讲解的还是理论上的知识,还是没有去实践下,那么这篇博客里面就来实践下。在上一篇博客里面说到了OkHttp类似HttpUrlConnection。按这样说的话,我们在项目中肯定还是要封装一层。如果嫌封装麻烦的话,也可以拿来主义,比如使用鸿洋大神的OkHttpUtils,网络上对它也好评如潮。又或者曾经很火的Volley框架。为什么说曾经呢?也不是说它用的少了,只能说有更火的框架出来了。是什么呢?没错,就是这篇文章说到的Retrofit框架。既然是新框架,那为什么前面又说是OkHttp的实践呢?这里我们就要理解Retrofit这个框架了。Retrofit这个框架网络请求层事用的是OkHttp,它同样是Square开源组合推出的一个框架。在Retrofit2.0以前,还可以选择HttpUrlConnection或者HttpClient去请求。Retrofit最近推出的2.0版本以后,直接强制用户使用OkHttp去做网络请求了。所以可以说Retrofit和OkHttp已经是一对同胞兄弟了。

其实Retrofit还没有广泛使用的时候,使用的最多的还是Volley框架的。Retrofit和Volley一样对HttpURLConnection或者OkHttp进行封装。然后有一天,你和你的同事说,咱们把Volley改成Retrofit框架吧,你同事就问你,Volley用的好好的,干嘛要换。那我们要怎么劝服他去使用呢?你就会要说,Volley的原理我们通过一系列封装成为一个Request对象,然后我们把它添加到RequestQueue里面,然后通过NetworkDispatcher进行网络请求,而Retrofit只需要定义一个API。就可以直接返回我们要请求的数据了。当然,它最好是一个RestfulAPI。

RestfulAPI的理解

网上对RestfulAPI这个概念有很多种理解,说的已经让我们摸不着头脑了。怎么来理解RestfulAPI呢?符合Restful风格的就是RestfulAPI。Restful风格有是什么鬼?RESTful即Representational State Transfer,可以把它翻译成(资源)表现层状态转换。理解这个名词就懂了。

  • 资源,服务器给客户端的文字,图片,视频都可以理解为资源。我们一般都是URL这个资源实体去指向资源所在的路径,当然这个路径必须是名词组成的,不能是动词。比如https://www.google.com.hk/这个网址就可以说是一种资源。
  • 表现层(Representational ),http请求的时候,会有http协议的head部分,post请求的时候还有http body。它描述了请求资源的Content-Type和Content-length等等。这就是一种表示层。又或者我们常使用的json格式也是一种表示层。
  • 状态转换(State Transfer)在http请求中,GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源。都是状态转换,而这些状态转换又是建立在表现层之上的,http头部表现层就会描述请求是通过get或者post方式等来请求的。

如果还是不太理解,可以看这篇文章理解RESTful架构,推酷上看了很多文章。还是看了这篇之后才明白这个概念的。为什么难理解呢,主要是Restful只是一种风格,没有一套完整的标准,所以网络上各有各的理解。

准备RESTful API

既然这样,那么这里我们就要先准备下几个基本的RESTful API。我这里准备了
一个user表

一个新闻列表(news)表

3个API(我的本地ip为192.168.1.103:8080)
注册接口 http://192.168.1.103:8080/GoachWeb/RegisterDataServlet
参数:username、password(POST/Get)
返回:

{"resultCode": 200, "responseTime": "2016-06-14 22:38:49", "data": {"errorCode": 1, "userId": 1000000, "userName": "Goach"}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

登录接口 http://192.168.1.103:8080/GoachWeb/LoginDataServlet

参数:username、password(POST/Get)
返回:

{"resultCode": 200, "responseTime": "2016-06-14 22:38:49", "data": {"errorCode": 1, "userId": 1000000, "userName": "Goach"}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

新闻列表接口
参数:userId(POST/Get)
返回

{"resultCode": 200, "responseTime": "2016-06-18 22:17:30", "data": {"newsItem": [{"id": 1, "title": "高盛:中国房地产可能在6-9个月内迎来“拐点", "content": "6月14日,王逸等高盛分析师在报告中写道,预计2017年房价将疲弱,因为该行业因杠杆率上升、需求减弱,不久将见到拐点。"}, {"id": 2, "title": "国产大飞机C919首飞时间曝光 已接517架次订单", "content": "《经济参考报》记者日前从多个权威渠道获悉,我国自主研制的C919大型客机将于今年下半年首飞,最快2017年完成后续各项技术验证,并开始正式交付。"}, {"id": 3, "title": "解放军大批巨炮同时开火 现场升硕大火球", "content": " 6月10日,陆军第42集团军某防空旅全员全装在粤东某陌生地域展开战场机动、侦察预警、陆空对抗、实弹射击等课目训练,锤炼部队实战本领。"}, {"id": 4, "title": "拳王邹市明,一场比赛460万奖金,只开90万的车", "content": "中国拳王邹市明,一年的收入有多少?和帕奎奥,梅威瑟这种级别的相比,邹市明的收入只能算是小收入,从最初打职业比赛时的30万美金的奖金,到最高70万美金奖金,这其中受了多少伤只有他自己最清楚。如果能7场比赛速成世界拳王,奖金不过也就100万美金,或许他”永远“也不能成为梅威瑟这样的拳王。"}, {"id": 5, "title": "40万人看杨毅直播讲道理???", "content": " 由总决赛第四场比赛中,杨毅对于詹姆斯和格林的一次冲突而进行的评述,引发的一系列事件,还在持续发酵中。"}, {"id": 11, "title": "女王杯穆雷双抢7险胜 瓦林卡爆冷止步首轮", "content": "腾讯体育6月15日讯 ATP500赛伦敦女王杯草地公开赛今日继续男单首轮比赛的争夺,赛会头号种子、英国名将穆雷通过两盘抢7以7-6(8)和7-6(1)险胜法国选手马胡特,惊险晋级次轮;而2号种子瑞士名将瓦林卡则连丢两盘以2-6和6-7(3)不敌西班牙选手沃达斯科,爆冷止步首轮。"}]}
}

接口比较简单。主要是自己后台开发比较low。这里后台使用的是通过servlet和jdbc通过gson转换为json进行开发的。

使用Retrofit框架

接口准备好了。下面就来集成Retrofit框架。

添加几个权限

 <uses-permission android:name="android.permission.INTERNET" /><uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
  • 1
  • 2

build.gradle添加依赖,下面会用到的也在这里了:

compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
compile 'com.squareup.retrofit2:retrofit:2.0.2'
compile 'com.squareup.retrofit2:converter-gson:2.0.2'
compile 'com.squareup.okhttp3:okhttp:3.3.0'
compile 'com.squareup.okio:okio:1.7.0'compile 'com.squareup.okhttp3:logging-interceptor:3.2.0'
compile 'io.reactivex:rxandroid:1.1.0'
compile 'io.reactivex:rxjava:1.1.0'
compile 'com.android.support:recyclerview-v7:23.4.0'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

两个retrofit依赖包,两个okhttp依赖包,okhttp3:logging-interceptor依赖包主要是拦截请求日志使用,引入下面要使用的rxandroid的两个依赖包reactivex:rxandroid和reactivex:rxjava,recyclerview主要是新闻列表页要使用的。

基本UI页面

下面就是写登录注册页面。
登录页面效果如下

注册页面效果如下

新闻页面布局效果

布局代码后面源码提供下载,而且比较简单。

创建Retrofit对象

页面写好了。下面我们通过单例形式创建一个Retrofit对象。

public class HRetrofitNetHelper{public static HRetrofitNetHelper mInstance;public Retrofit mRetrofit;//本地ip为192.168.1.103public static final String BASE_URL = "http://192.168.1.103:8080/GoachWeb/";private HRetrofitNetHelper(){mRetrofit = new Retrofit.Builder().baseUrl(BASE_URL).build();}public static HRetrofitNetHelper getInstance(){if(mInstance==null){synchronized (HRetrofitNetHelper.class){if(mInstance==null)mInstance = new HRetrofitNetHelper ();}}return mInstance ;}
}

简单的创建好了一个Retrofit。这里只是配置了一个接口的baseUrl,也就是根路径。

配置ConverterFactory

如果要Retrofit直接将json转换为为Dao对象。那么我们就要通过addConverterFactory来配置,如下:

 mRetrofit = new Retrofit.Builder().addConverterFactory(GsonConverterFactory.create()).build();
  • 1
  • 2
  • 3

上面是使用依赖:

compile'com.squareup.retrofit2:converter-gson:2.0.2'
  • 1

包。然后addConverterFactory来配置。通过源码方法

addConverterFactory(Converter.Factory factory)
  • 1

我们可以看到要传入一个继承Converter.Factory的对象。Retrofit里面就有这样的对象,这里我们用的是Gson来进行解析,那就有对应的GsonConverterFactory。那好下面就来创建这个对象

创建这个对象有两种方式

  • 一种是像上面写的一样
GsonConverterFactory.create()
  • 1

这种方式就是简单的创建默认的Gson对象,然后像我们平常一样转换为Dao对象。

  • 还有一种方式就是通过GsonBuilder创建Gson对象,比如这里统一把后台提供的带有yyyy-MM-dd HH:mm:ss格式的Date对象,客户端如果用上面这种方式创建的话,会报下面这个错
java.text.ParseException: Failed to parse date ["2016-06-11 20:57:28']: Invalid time zone indicator ' ' (at offset 0)
  • 1

这种情况下,我们就可以这样:

Gson mGson = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create();
  • 1
  • 2

然后再创建GsonConverterFactory对象的时候传入Gson

.addConverterFactory(GsonConverterFactory.create(mGson))
  • 1

就可以很好的解决这个问题了。

这里只是说了使用Gson进行解析,其实Retrofit还提供了其他的一些解析工具,如下:

Gson: com.squareup.retrofit2:converter-gson
Jackson: com.squareup.retrofit2:converter-jackson
Moshi: com.squareup.retrofit2:converter-moshi
Protobuf: com.squareup.retrofit2:converter-protobuf
Wire: com.squareup.retrofit2:converter-wire
Simple XML: com.squareup.retrofit2:converter-simplexml
Scalars (primitives, boxed, and String): com.squareup.retrofit2:converter-scalars
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

用法类似这样
导入包(xx可以指Jackson或者Moshi等等):

compile 'com.squareup.retrofit2:converter-xx:2.0.2'
  • 1

然后:

.addConverterFactory(xxConverterFactory.create(mGson))
  • 1

当然,我们还是可以设置多个converter
比如支持 proto 格式和json格式。那么如下添加:

Retrofit retrofit = new Retrofit.Builder()//....addConverterFactory(ProtoConverterFactory.create()).addConverterFactory(GsonConverterFactory.create()).build();
  • 1
  • 2
  • 3
  • 4
  • 5

ProtoConverterFactory和GsonConverterFactory添加 converter 的顺序很重要。Retrofit会依次询问每一个 converter 能否处理一个类型。当Retrofit试图反序列化一个 proto 格式,它其实会被当做 JSON 来对待。所以Retrofit会先要检查 proto buffer 格式,然后才是 JSON。所以要先添加ProtoConverterFactory,然后是GsonConverterFactory。

又比如我们需要Retrofit支持RxJava。添加:

.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
  • 1

就好了。

配置HttpLoggingInterceptor

Retrofit还可以添加OkHttpClient对象。比如我们可以添加一个拦截器来监听每次请求体:
依赖的包

compile'com.squareup.okhttp3:logging-interceptor:3.2.0'
  • 1
 HttpLoggingInterceptor  interceptor = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {@Overridepublic void log(String message) {Log.d("zgx", "OkHttp====message " + message);}});
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);         
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

创建好后,然后通过retrofit对象添加client,如下:

mRetrofit = new Retrofit.Builder()//....client(mOkHttpClient).build();
  • 1
  • 2
  • 3
  • 4

这样我们就通过HttpLoggingInterceptor 拦截器可以获取道http请求体,可以获取我们请求方式,请求的参数,然后的json数据。这里以登录接口为例,如下:

06-11 22:16:11.064 31186-8789/com.goach.client D/zgx: OkHttp====message --> POST http://192.168.1.102:8080/GoachWeb/LoginDataServlet http/1.1
06-11 22:16:11.064 31186-8789/com.goach.client D/zgx: OkHttp====message Content-Type: application/x-www-form-urlencoded
06-11 22:16:11.068 31186-8789/com.goach.client D/zgx: OkHttp====message Content-Length: 30
06-11 22:16:11.068 31186-8789/com.goach.client D/zgx: OkHttp====message
06-11 22:16:11.068 31186-8789/com.goach.client D/zgx: OkHttp====message username=Goach&password=123456
06-11 22:16:11.068 31186-8789/com.goach.client D/zgx: OkHttp====message --> END POST (30-byte body)
06-11 22:16:12.376 31186-8789/com.goach.client D/zgx: OkHttp====message <-- 200 OK http://192.168.1.102:8080/GoachWeb/LoginDataServlet (1308ms)
06-11 22:16:12.376 31186-8789/com.goach.client D/zgx: OkHttp====message Server: Apache-Coyote/1.1
06-11 22:16:12.376 31186-8789/com.goach.client D/zgx: OkHttp====message Content-Type: text/plain;charset=UTF-8
06-11 22:16:12.376 31186-8789/com.goach.client D/zgx: OkHttp====message Transfer-Encoding: chunked
06-11 22:16:12.376 31186-8789/com.goach.client D/zgx: OkHttp====message Date: Sat, 11 Jun 2016 14:15:19 GMT
06-11 22:16:12.384 31186-8789/com.goach.client D/zgx: OkHttp====message
06-11 22:16:12.384 31186-8789/com.goach.client D/zgx: OkHttp====message {"errorCode":1,"userId":1000000,"responseTime":"2016-06-11 22:15:19","resultCode":200}
06-11 22:16:12.384 31186-8789/com.goach.client D/zgx: OkHttp====message <-- END HTTP (86-byte body)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

既然能用OkHttp的拦截机制,那么我们就可以在RequestBody 里面添加基本参数

配置基本提交参数

我们可以再新建一个拦截器,这里我举例加些简单的系统参数,如下:

        class HttpBaseParamsLoggingInterceptor implements Interceptor{@Overridepublic Response intercept(Chain chain) throws IOException {Request request = chain.request();Request.Builder requestBuilder = request.newBuilder();RequestBody formBody = new FormBody.Builder().add("userId", "10000").add("sessionToken", "E34343RDFDRGRT43RFERGFRE").add("q_version", "1.1").add("device_id", "android-344365").add("device_os", "android").add("device_osversion","6.0").add("req_timestamp", System.currentTimeMillis() + "").add("app_name","forums").add("sign", "md5").build();String postBodyString = Utils.bodyToString(request.body());postBodyString += ((postBodyString.length() > 0) ? "&" : "") +  Utils.bodyToString(formBody);request = requestBuilder.post(RequestBody.create(MediaType.parse("application/x-www-form-urlencoded;charset=UTF-8"),postBodyString)).build();return chain.proceed(request);}}

上面Utils类是使用的okio.Buffer里面的工具类。通过RequestBody构建要上传的一些基本公共的参数,然后通过”&”符号在http 的body里面其他要提交参数拼接。然后再通过requestBuilder重新创建request对象,然后再通过chain.proceed(request)返回Response 。

接下来在创建OkHttpClient对象的时候修改为如下代码:

    mOkHttpClient = new OkHttpClient.Builder().addInterceptor(interceptor).addInterceptor(new HttpBaseParamsLoggingInterceptor()).build();
  • 1
  • 2
  • 3
  • 4

这样就添加好了一些基本的公共参数。

当然。我们也可以直接借助github 上的BasicParamsInterceptor。代码如下:

public class BasicParamsInterceptor implements Interceptor {Map<String, String> queryParamsMap = new HashMap<>();Map<String, String> paramsMap = new HashMap<>();Map<String, String> headerParamsMap = new HashMap<>();List<String> headerLinesList = new ArrayList<>();private BasicParamsInterceptor() {}@Overridepublic Response intercept(Chain chain) throws IOException {Request request = chain.request();Request.Builder requestBuilder = request.newBuilder();// process header params injectHeaders.Builder headerBuilder = request.headers().newBuilder();if (headerParamsMap.size() > 0) {Iterator iterator = headerParamsMap.entrySet().iterator();while (iterator.hasNext()) {Map.Entry entry = (Map.Entry) iterator.next();headerBuilder.add((String) entry.getKey(), (String) entry.getValue());}}if (headerLinesList.size() > 0) {for (String line: headerLinesList) {headerBuilder.add(line);}}requestBuilder.headers(headerBuilder.build());// process header params end// process queryParams inject whatever it's GET or POSTif (queryParamsMap.size() > 0) {injectParamsIntoUrl(request, requestBuilder, queryParamsMap);}// process header params end// process post body injectif (request.method().equals("POST") && request.body().contentType().subtype().equals("x-www-form-urlencoded")) {FormBody.Builder formBodyBuilder = new FormBody.Builder();if (paramsMap.size() > 0) {Iterator iterator = paramsMap.entrySet().iterator();while (iterator.hasNext()) {Map.Entry entry = (Map.Entry) iterator.next();formBodyBuilder.add((String) entry.getKey(), (String) entry.getValue());}}RequestBody formBody = formBodyBuilder.build();String postBodyString = bodyToString(request.body());postBodyString += ((postBodyString.length() > 0) ? "&" : "") +  bodyToString(formBody);requestBuilder.post(RequestBody.create(MediaType.parse("application/x-www-form-urlencoded;charset=UTF-8"), postBodyString));} else {    // can't inject into body, then inject into urlinjectParamsIntoUrl(request, requestBuilder, paramsMap);}request = requestBuilder.build();return chain.proceed(request);}// func to inject params into urlprivate void injectParamsIntoUrl(Request request, Request.Builder requestBuilder, Map<String, String> paramsMap) {HttpUrl.Builder httpUrlBuilder = request.url().newBuilder();if (paramsMap.size() > 0) {Iterator iterator = paramsMap.entrySet().iterator();while (iterator.hasNext()) {Map.Entry entry = (Map.Entry) iterator.next();httpUrlBuilder.addQueryParameter((String) entry.getKey(), (String) entry.getValue());}}requestBuilder.url(httpUrlBuilder.build());}private static String bodyToString(final RequestBody request){try {final RequestBody copy = request;final Buffer buffer = new Buffer();if(copy != null)copy.writeTo(buffer);elsereturn "";return buffer.readUtf8();}catch (final IOException e) {return "did not work";}}public static class Builder {BasicParamsInterceptor interceptor;public Builder() {interceptor = new BasicParamsInterceptor();}public Builder addParam(String key, String value) {interceptor.paramsMap.put(key, value);return this;}public Builder addParamsMap(Map<String, String> paramsMap) {interceptor.paramsMap.putAll(paramsMap);return this;}public Builder addHeaderParam(String key, String value) {interceptor.headerParamsMap.put(key, value);return this;}public Builder addHeaderParamsMap(Map<String, String> headerParamsMap) {interceptor.headerParamsMap.putAll(headerParamsMap);return this;}public Builder addHeaderLine(String headerLine) {int index = headerLine.indexOf(":");if (index == -1) {throw new IllegalArgumentException("Unexpected header: " + headerLine);}interceptor.headerLinesList.add(headerLine);return this;}public Builder addHeaderLinesList(List<String> headerLinesList) {for (String headerLine: headerLinesList) {int index = headerLine.indexOf(":");if (index == -1) {throw new IllegalArgumentException("Unexpected header: " + headerLine);}interceptor.headerLinesList.add(headerLine);}return this;}public Builder addQueryParam(String key, String value) {interceptor.queryParamsMap.put(key, value);return this;}public Builder addQueryParamsMap(Map<String, String> queryParamsMap) {interceptor.queryParamsMap.putAll(queryParamsMap);return this;}public BasicParamsInterceptor build() {return interceptor;}}
}

我们只要向上面一样配置就行了。

其实拦截器还能做很多事。比如在开发中,我们会遇到,我们去请求某些接口的时候,服务端会直接返回一个信息给客户端,让客户端去Toast提示。下面,我就以只要是请求登录接口就给个提示框为例

Rxandroid的使用和特殊Url请求拦截处理

还是会用到拦截器,要知道,拦截器接口实现的intercept这个方法可不是在ui线程里面执行的,所以这里弹Toast,我们用RxAndroid实现再好不过了。
既然要用到RxAndroid,那就需要再依赖两个包:

compile 'io.reactivex:rxandroid:1.1.0'
compile 'io.reactivex:rxjava:1.1.0'
  • 1
  • 2

依赖好了,下面就可以在OkHttpClient创建的时候再添加一个拦截器mUrlInterceptor,代码如下,

 @Overridepublic okhttp3.Response intercept(Chain chain) throws IOException {Request request = chain.request();okhttp3.Response response = chain.proceed(request);String requestUrl = response.request().url().uri().getPath();if(!TextUtils.isEmpty(requestUrl)){if(requestUrl.contains("LoginDataServlet")) {if (Looper.myLooper() == null) {Looper.prepare();}createObservable("现在请求的是登录接口");}}return response;}

然后再上面OkHttp创建的时候修改下:

mOkHttpClient = new OkHttpClient.Builder()//..前面两个拦截器省略.addInterceptor(mUrlInterceptor).build();
  • 1
  • 2
  • 3
  • 4

说下上面intercept里面的,注意在createObservable方法调用前,要先Looper.prepare()下,否则会报错提示你要先调用Looper.prepare()方法下。其他的代码应该理解没什么问题了。我们知道RxAndroid两个核心就是Observable事件被观察者,然后就是subscribe事件订阅者,可以理解为观察者模式,但是它和观察者模式又有不同的地方,就是当事件被观察者没有关注者的时候,事件不会发送出去。详细就不讲解了。我这里只是弹个Toast,不用那么复杂。代码如下:

private void createObservable(String msg){Observable.just(msg).map(new Func1<String, String>() {@Overridepublic String call(String s) {return s;}}).observeOn(AndroidSchedulers.mainThread()).subscribe(onNextAction);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

通过Func1,直接发送一条消息给订阅者,发送完后这个事件就结束了。

.observeOn(AndroidSchedulers.mainThread())
  • 1

的作用就是把订阅者处理事件发送给ui线程去处理。

接下来订阅者,就简单的用onNextAction实现了。

 private void createSubscriberByAction() {onNextAction = new Action1<String>() {@Overridepublic void call(String s) {Log.d("zgx","s=========="+s);Toast.makeText(mContext,s, Toast.LENGTH_SHORT).show();}};}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

createSubscriberByAction方法在HRetrofitNetHelper对象构造器里面调用就好了。

  private HRetrofitNetHelper(Context context){//...createSubscriberByAction();//...}
  • 1
  • 2
  • 3
  • 4
  • 5

这样就实现了,上面提的需求。

缓存

配置了这么多,接下来肯定又会想到缓存问题还没有处理呢。那么,接下来就来说下缓存处理了。

写之前,先看下源码里面注释的一段话

 if (!requestMethod.equals("GET")) {// Don't cache non-GET responses. We're technically allowed to cache// HEAD requests and some POST requests, but the complexity of doing// so is high and the benefit is low.return null;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

看懂了吧,OkHttp建议在不是Get请求的响应体不要缓存,因为如果缓存的话会提高它的复杂性而且好处不大。
没看到这段话之前。郁闷了很久为什么Post请求缓存生成不了,而且会报一个错

504 Unsatisfiable Request (only-if-cached)
  • 1

这个错的意思就是只去读缓存,但是缓存不存在,所以就会报错了。但是我觉得有时候Post请求缓存的需求还是会有的,比如有时候在应用中经常想在没网的情况下缓存这个页面,而这个页面的请求接口也是post请求。所以还是要有缓存更好,比如volley框架就可以缓存整个页面,但是也是要改下volley的代码。目前还不知道怎么去缓存post请求。目前github上有RxCache,或者是通过Sqlite自己实现缓存都有,没有仔细研究,后面有时间在看。
下面就来看下实现代码

  • 创建局部变量Cache,以及两个Get方法,一个获取Cache对象,一个清除Cache缓存。
private final Cache cache;public Cache getCache(){return cache;}public void clearCache() throws IOException {cache.delete();}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 配置OkHttp缓存
File cacheFile = new File(context.getCacheDir(), "HttpCache");
cache = new Cache(cacheFile, 1024 * 1024 * 100); //100Mb
mOkHttpClient = new OkHttpClient.Builder()//....cache(cache).build();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

官方建议缓存路径写在context.getCacheDir()里面,也就是在/data/data/com.goach.client/cache/HttpCache里面。这样配置好了,如果云端通过http的header里面Cache-Control做了缓存。那么这样就缓存完了。但是如果云端没有做了,那么我们客户端也可以自己通过Interceptor实现。这里我就把缓存逻辑写在上面的mUrlInterceptor拦截器里面了。修改如下

    @Overridepublic okhttp3.Response intercept(Chain chain) throws IOException {Request request = chain.request();//缓存if(NetUtil.checkNetwork(mContext)==NetUtil.NO_NETWORK){request = request.newBuilder().cacheControl(CacheControl.FORCE_CACHE).build();Log.d("zgx","no network");}okhttp3.Response response = chain.proceed(request);String requestUrl = response.request().url().uri().getPath();if(!TextUtils.isEmpty(requestUrl)){if(requestUrl.contains("LoginDataServlet")) {if (Looper.myLooper() == null) {Looper.prepare();}createObservable("现在请求的是登录接口");}}//缓存响应if(NetUtil.checkNetwork(mContext)!=NetUtil.NO_NETWORK){//有网的时候读接口上的@Headers里的配置,你可以在这里进行统一的设置String cacheControl = request.cacheControl().toString();Log.d("zgx","cacheControl====="+cacheControl);return response.newBuilder().header("Cache-Control", cacheControl)//http1.0的旧东西,优先级比Cache-Control低.removeHeader("Pragma").build();}else{return response.newBuilder().header("Cache-Control", "public, only-if-cached, max-stale=30*24*60*60").removeHeader("Pragma").build();}}

没网的情况下Request 直接从缓存里面读取,响应体增加header的Cache-Control,缓存30天,有网的情况下,Request 就会去请求服务器,然后响应体就会去都Retrofit框架里面的@Header配置,如果没有配置,就没不缓存,如果配置了就可以进行缓存。到这里,当我们去Get请求的时候,就会生成缓存

我这里是通过模拟器看到,真机里面是看不到的。打开可以看到我们请求信息。

超时

okhttp如果没有配置默认是10s,错误信息如下

onFailure======java.net.SocketTimeoutException: failed to connect to /192.168.1.101 (port 8080) after 10000ms
  • 1

配置

 mOkHttpClient = new OkHttpClient.Builder().connectTimeout(12, TimeUnit.SECONDS)//....build();
  • 1
  • 2
  • 3
  • 4

后,错误信息如下

 onFailure======java.net.SocketTimeoutException: failed to connect to /192.168.1.101 (port 8080) after 12000ms
  • 1

还可以配置

 .writeTimeout(20, TimeUnit.SECONDS).readTimeout(20, TimeUnit.SECONDS).retryOnConnectionFailure(true)
  • 1
  • 2
  • 3

没毛病,应该看的懂。
这样Retrofit创建基本的配置就完成了,最后结合上面总结后整个配置类的代码:

public class HRetrofitNetHelper implements HttpLoggingInterceptor.Logger,Interceptor {//HRetrofitNetHelper 实现单例public static HRetrofitNetHelper mInstance;//缓存对象private final Cache cache;public Retrofit mRetrofit;public OkHttpClient mOkHttpClient;//请求日志拦截器public HttpLoggingInterceptor mHttpLogInterceptor;//基本参数拦截器private BasicParamsInterceptor mBaseParamsInterceptor;//缓存和特殊Url拦截处理拦截器private Interceptor  mUrlInterceptor;private Context mContext;//Date对象传递public Gson mGson;//接口baseurlpublic static final String BASE_URL = "http://192.168.1.101:8080/GoachWeb/";private Action1<String> onNextAction;private HRetrofitNetHelper(Context context){this.mContext = context ;//提供Action,供特殊Url拦截然后ToastcreateSubscriberByAction();//yyyy-MM-dd HH:mm:ss的时间格式,可以转换为Date对象mGson = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create();mHttpLogInterceptor = new HttpLoggingInterceptor(this);//打印http的body体mHttpLogInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);//基本参数Map<String,String> tempParams = getBaseParams(context);mBaseParamsInterceptor = new BasicParamsInterceptor.Builder().addParamsMap(tempParams).build();mUrlInterceptor = this;//创建缓存路径File cacheFile = new File(context.getCacheDir(), "HttpCache");Log.d("zgx","cacheFile====="+cacheFile.getAbsolutePath());cache = new Cache(cacheFile, 1024 * 1024 * 100); //100MbmOkHttpClient = new OkHttpClient.Builder().connectTimeout(12, TimeUnit.SECONDS).writeTimeout(20, TimeUnit.SECONDS).readTimeout(20, TimeUnit.SECONDS).retryOnConnectionFailure(true).addInterceptor(mHttpLogInterceptor).addInterceptor(mBaseParamsInterceptor).addInterceptor(mUrlInterceptor).cache(cache).build();mRetrofit = new Retrofit.Builder().baseUrl(BASE_URL).addConverterFactory(GsonConverterFactory.create(mGson)).client(mOkHttpClient).build();}public static HRetrofitNetHelper getInstance(Context context){if(mInstance==null){synchronized (HRetrofitNetHelper.class){if(mInstance==null){mInstance =  new HRetrofitNetHelper(context);}}}return mInstance;}//获取相应的APIService对象public <T> T getAPIService(Class<T> service) {return mRetrofit.create(service);}//异步callback,对一些特殊response逻辑处理public <D> void enqueueCall(Call<BaseResp<D>> call,final RetrofitCallBack<D> retrofitCallBack){call.enqueue(new Callback<BaseResp<D>>() {@Overridepublic void onResponse(Call<BaseResp<D>> call, Response<BaseResp<D>> response) {BaseResp<D> resp = response.body() ;if (resp == null) {Toast.makeText(mContext, "暂时没有最新数据!", Toast.LENGTH_SHORT).show();return;}if (resp.getResultCode() == 2000 || resp.getResultCode() == 2001 || resp.getResultCode() == 2002) {Toast.makeText(mContext,"code====="+resp.getResultCode(),Toast.LENGTH_SHORT).show();}if (resp.getResultCode() == 200) {if(retrofitCallBack!=null)retrofitCallBack.onSuccess(resp);} else {// ToastMaker.makeToast(mContext, resp.errMsg, Toast.LENGTH_SHORT);if(retrofitCallBack!=null)retrofitCallBack.onFailure(resp.getErrMsg());}}@Overridepublic void onFailure(Call<BaseResp<D>> call, Throwable t) {//   ToastMaker.makeToast(mContext, "网络错误,请重试!", Toast.LENGTH_SHORT);if(retrofitCallBack!=null){retrofitCallBack.onFailure(t.toString());}}});}@Overridepublic void log(String message) {Log.d("zgx","OkHttp: " + message);}//提供一些常用的基本参数public Map<String,String> getBaseParams(Context context){Map<String,String> params = new HashMap<>();params.put("userId", "324353");params.put("sessionToken", "434334");params.put("q_version", "1.1");params.put("device_id", "android7.0");params.put("device_os", "android");params.put("device_type", "android");params.put("device_osversion", "android");params.put("req_timestamp", System.currentTimeMillis() + "");params.put("app_name","forums");String sign = makeSign(params);params.put("sign", sign);return params ;}public String makeSign(Map<String, String> params) {final String signSalt = "fe#%d8ec93a1159a2a3";TreeMap<String, Object> sorted = new TreeMap<String, Object>();for (Map.Entry<String, String> kv : params.entrySet()) {sorted.put(kv.getKey(), kv.getValue());}StringBuilder sb = new StringBuilder(signSalt);for (String key : sorted.keySet()) {if (!"sign".equals(key) && !key.startsWith("file_")) {sb.append(key).append(sorted.get(key));}}sb.append(signSalt);return MD5.md5(sb.toString()).toUpperCase();}@Overridepublic okhttp3.Response intercept(Chain chain) throws IOException {Request request = chain.request();//缓存if(NetUtil.checkNetwork(mContext)==NetUtil.NO_NETWORK){request = request.newBuilder().cacheControl(CacheControl.FORCE_CACHE).build();Log.d("zgx","no network");}okhttp3.Response response = chain.proceed(request);String requestUrl = response.request().url().uri().getPath();if(!TextUtils.isEmpty(requestUrl)){if(requestUrl.contains("LoginDataServlet")) {if (Looper.myLooper() == null) {Looper.prepare();}createObservable("现在请求的是登录接口");}}//缓存响应if(NetUtil.checkNetwork(mContext)!=NetUtil.NO_NETWORK){//有网的时候读接口上的@Headers里的配置,你可以在这里进行统一的设置String cacheControl = request.cacheControl().toString();Log.d("zgx","cacheControl====="+cacheControl);return response.newBuilder().header("Cache-Control", cacheControl).removeHeader("Pragma").build();}else{return response.newBuilder().header("Cache-Control", "public, only-if-cached, max-stale=120").removeHeader("Pragma").build();}}//异步特殊处理后回调public interface RetrofitCallBack<D>{void onSuccess(BaseResp<D> baseResp);void onFailure(String error);}private void createSubscriberByAction() {onNextAction = new Action1<String>() {@Overridepublic void call(String s) {Log.d("zgx","s=========="+s);Toast.makeText(mContext,s, Toast.LENGTH_SHORT).show();}};}//创建事件源private void createObservable(String msg){Observable.just(msg).map(new Func1<String, String>() {@Overridepublic String call(String s) {return s;}}).observeOn(AndroidSchedulers.mainThread()).subscribe(onNextAction);}public Cache getCache(){return cache;}public void clearCache() throws IOException {cache.delete();}
}

Service之请求API

定义上面的3个请求接口API,为了验证缓存,都是Get请求。

public interface ILoginService {@GET("LoginDataServlet")@Headers("Cache-Control: public, max-age=30")Call<BaseResp<RegisterBean>> userLogin(@Query("username") String username, @Query("password") String password);
}
  • 1
  • 2
  • 3
  • 4
  • 5
public interface INewsService {@GET("NewsDataServlet")@Headers("Cache-Control: public, max-age=30")Call<BaseResp<News>> userNews(@Query("userId") String userId);
}
  • 1
  • 2
  • 3
  • 4
  • 5
public interface IRegisterService {@FormUrlEncoded@POST("RegisterDataServlet")Call<RegisterBean> createUser(@FieldMap Map<String ,String> params);
}
  • 1
  • 2
  • 3
  • 4
  • 5

其中Get请求,使用@GET和@Query或者@QueryMap的结合,Post请求@FormUrlEncoded、@POST和@Field或者@FieldMap的结合。又或者url中通过@Path动态添加参数。比如

public interface INewsService
{  @GET("NewsDataServlet/currentPage={currentPage}")  Call<BaseResp<News>> getUser(@Path("currentPage") String currentPage);
}  
  • 1
  • 2
  • 3
  • 4
  • 5

还有通过@Multipart 实现文件上传等等,详细可以看鸿洋大神的 Retrofit2 完全解析 探索与okhttp之间的关系

HRetrofitNetHelper的使用以及Activity相关代码

BaseActivity

public abstract class BaseActivity extends AppCompatActivity{public HRetrofitNetHelper retrofitNetHelper;public LayoutInflater mInflater;public ProgressDialog mDialog;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);}@Overridepublic void setContentView(@LayoutRes int layoutResID) {super.setContentView(layoutResID);mInflater = LayoutInflater.from(this);setContentView(mInflater.inflate(layoutResID,null));}@Overridepublic void setContentView(View view) {super.setContentView(view);retrofitNetHelper = HRetrofitNetHelper.getInstance(BaseActivity.this);mDialog = new ProgressDialog(BaseActivity.this);}
}

LonigActicity

public class LoginActivity extends BaseActivity implements View.OnClickListener,HRetrofitNetHelper.RetrofitCallBack<RegisterBean> {private AutoCompleteTextView mEmailView;private EditText mPasswordView;private View mLoginFormView;private Button mSignInButton;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_login);mEmailView = (AutoCompleteTextView) findViewById(R.id.email);mPasswordView = (EditText) findViewById(R.id.password);mPasswordView.setOnEditorActionListener(new TextView.OnEditorActionListener() {@Overridepublic boolean onEditorAction(TextView textView, int id, KeyEvent keyEvent) {if (id == R.id.login || id == EditorInfo.IME_NULL) {return true;}return false;}});mSignInButton = (Button) findViewById(R.id.sign_in_button);mSignInButton.setOnClickListener(this);mLoginFormView = findViewById(R.id.login_form);}public void startRegister(View view){Intent intent = new Intent(LoginActivity.this,RegisterActivity.class);startActivity(intent);}@Overridepublic void onClick(View v) {switch (v.getId()){case R.id.sign_in_button:mDialog.setMessage("正在登录中,请稍后...");mDialog.show();ILoginService loginService = retrofitNetHelper.getAPIService(ILoginService.class);String username = mEmailView.getText().toString();String password = mPasswordView.getText().toString();if(!TextUtils.isEmpty(username)&&!TextUtils.isEmpty(password)){final Call<BaseResp<RegisterBean>> repos = loginService.userLogin(username,password);retrofitNetHelper.enqueueCall(repos,this);}break;}}@Overridepublic void onSuccess(BaseResp<RegisterBean> baseResp) {Log.d("zgx","onResponse======"+baseResp.getData().getErrorCode());Date date = baseResp.getResponseTime();Log.d("zgx","RegisterBean======"+date);if(baseResp.getData().getErrorCode()==1){Intent intent = new Intent(LoginActivity.this, NewsActivity.class);intent.putExtra("intent_user_id",String.valueOf(baseResp.getData().getUserId()));startActivity(intent);Toast.makeText(getBaseContext(),"登录成功",Toast.LENGTH_SHORT).show();}else {Toast.makeText(getBaseContext(),"用户不存在",Toast.LENGTH_SHORT).show();}mDialog.dismiss();}@Overridepublic void onFailure(String error) {Log.d("zgx","onFailure======"+error);mDialog.dismiss();}
}

RegisterActivity

public class RegisterActivity extends BaseActivity implements Callback<RegisterBean> {private AutoCompleteTextView mUserName;private EditText mPasswordEditText;private EditText mConfirmationEditText;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_register);mUserName = (AutoCompleteTextView) findViewById(R.id.id_username);mPasswordEditText = (EditText)findViewById(R.id.password);mConfirmationEditText = (EditText)findViewById(R.id.confirmation_password);}public void startRegister(View view){String userName = mUserName.getText().toString();String password = mPasswordEditText.getText().toString();String mConfirmation = mConfirmationEditText.getText().toString();if(!TextUtils.isEmpty(userName)&&!TextUtils.isEmpty(password)&&!TextUtils.isEmpty(mConfirmation)){if(password.equals(mConfirmation)){IRegisterService loginService = retrofitNetHelper.getAPIService(IRegisterService.class);Map<String,String> mParamsMap = new HashMap<>();mParamsMap.put("username",userName);mParamsMap.put("password",password);Call<RegisterBean> call =  loginService.createUser(mParamsMap);call.enqueue(this);}else {Toast.makeText(getBaseContext(),"密码不一致",Toast.LENGTH_SHORT).show();}}else {Toast.makeText(getBaseContext(),"请填写完整",Toast.LENGTH_SHORT).show();}}@Overridepublic void onResponse(Call<RegisterBean> call, Response<RegisterBean> response) {if(response.body().getErrorCode()==1){Intent intent = new Intent(RegisterActivity.this, LoginActivity.class);startActivity(intent);}else{Toast.makeText(getBaseContext(),"注册失败",Toast.LENGTH_SHORT).show();}}@Overridepublic void onFailure(Call<RegisterBean> call, Throwable t) {}
}

NewsActivity

public class NewsActivity extends BaseActivity implements HRetrofitNetHelper.RetrofitCallBack<News>{private String mUserId;private RecyclerView mRecyclerView;private NewsAdapter mNewsAdapter;private List<NewItem> mDataList;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_news);mUserId = getIntent().getStringExtra("intent_user_id");mDataList = new ArrayList<>();mRecyclerView = (RecyclerView)findViewById(R.id.id_news_recycler_view);LinearLayoutManager manager = new LinearLayoutManager(NewsActivity.this);mRecyclerView.setLayoutManager(manager);mNewsAdapter = new NewsAdapter(NewsActivity.this,mDataList);mRecyclerView.setAdapter(mNewsAdapter);loadData();}private void loadData(){mDialog.setMessage("正在加载中,请稍后...");mDialog.show();INewsService newService = retrofitNetHelper.getAPIService(INewsService.class);Log.d("zgx","mUserId====="+mUserId);final Call<BaseResp<News>> repos = newService.userNews(mUserId);retrofitNetHelper.enqueueCall(repos,this);}@Overridepublic void onSuccess(BaseResp<News> baseResp) {mDialog.dismiss();mDataList.clear();mDataList.addAll(baseResp.getData().getNewsItem());mNewsAdapter.notifyDataSetChanged();}@Overridepublic void onFailure(String error) {mDialog.dismiss();Toast.makeText(NewsActivity.this,"请求出现异常"+error,Toast.LENGTH_SHORT).show();}
}

其他一些帮助类,后面提供源码下载。

最后来看下实现的效果

缓存效果没有录制,博客上传文件有限。

参考博客

  1. okhttp-logging-interceptor
  2. BasicParamsInterceptor - 为 OkHttp 请求添加公共参数
  3. 使用Retrofit和Okhttp实现网络缓存。无网读缓存,有网根据过期时间重新请求
  4. Okhttp缓存浅析
  5. 用 Retrofit 2 简化 HTTP 请求
  6. Retrofit2 完全解析 探索与okhttp之间的关系

说到这里,其实还有很多地方还要去学习,比如RxCache框架,通过Retrofit和Rxandroid真正的结合实现缓存处理,比如文件的上传下载等等。这些后面有时间再学习了。

源码下载

只是提供服务端和客户端的源码,数据库表和环境搭建配置就不提供了。
使用的环境为:
Android studio 2.1.2
MyEclipse 2014GA
Tomcat8.0
JDK8.0
MySQL Server 5.7
Navicat for MySQL

转自: https://blog.csdn.net/iamzgx/article/details/51607387

学会Retrofit+OkHttp+RxAndroid三剑客的使用,让自己紧跟Android潮流的步伐相关推荐

  1. Retrofit+OkHttp+RxAndroid

    转载请标明出处: http://blog.csdn.net/iamzgx/article/details/51607387 本文出自:[iGoach的博客] 概括 在上一篇博客android网络框架O ...

  2. MVP+Dragger2+Rxjava2+Retrofit+OKhttp进行开发。

    MVP+Dragger2+Rxjava2+Retrofit+OKhttp框架已经流行很长时间,而且也必将成为未来android开发的趋势,在使用这个框架的过程中踩过很多坑, 所以想把我的经验告诉大家, ...

  3. Retrofit+OKHttp+RxJava的使用

    什么是响应式编程   响应式编程是一种基于异步数据 流概念的编程模式.数据流就像一条河:它可以被观测,被过滤,被操作,或者为新的消费者与另外一 条流合并为一条新的流. 什么是RxJava RxJava ...

  4. Android MVVM + Retrofit + OkHttp + Coroutine 协程 + Room + 组件化架构的Android应用开发规范化架构

    BaseDemo 介绍 BaseDemo 是Android MVVM + Retrofit + OkHttp + Coroutine 协程 + Room + 组件化架构的Android应用开发规范化架 ...

  5. Dagger2之应用篇(Dagger2+RxJava+Retrofit+OkHttp+MVP)-第7章

    介绍 前面介绍了Dagger2,今天尝试自己的想法去应用了下,说实话很别扭,晚上睡觉思前想后这个的好处.总是有一种似懂非懂,感觉就是,让对象与对象之间产生了一中关联,多个module的provides ...

  6. Android 基于ijkplayer+Rxjava+Rxandroid+Retrofit2.0+MVP+Material Design的android万能播放器aaa

    MDPlayer万能播放器 MDPlayer,基于ijkplayer+Rxjava+Rxandroid+Retrofit2.0+MVP+Material Design的android万能播放器,可以播 ...

  7. 基于ijkplayer+Rxjava+Rxandroid+Retrofit2.0+MVP+Material Design的android万能播放器

    MDPlayer万能播放器 MDPlayer,基于ijkplayer+Rxjava+Rxandroid+Retrofit2.0+MVP+Material Design的android万能播放器,可以播 ...

  8. java中使用okhttpsoap,Android okHttp网络请求之Retrofit+Okhttp+RxJava组合

    Retrofit介绍: Retrofit和okHttp师出同门,也是Square的开源库,它是一个类型安全的网络请求库,Retrofit简化了网络请求流程,基于OkHtttp做了封装,解耦的更彻底:比 ...

  9. android http常用配置,Android中Retrofit+OkHttp进行HTTP网络编程的使用指南

    Retrofit介绍:Retrofit(GitHub主页https://github.com/square/okhttp)和OkHttp师出同门,也是Square的开源库,它是一个类型安全的网络请求库 ...

最新文章

  1. OpenShift — 核心概念
  2. js进阶 13-6 jquery动画效果相关常用函数有哪些
  3. 多分辨率图像的快速查询
  4. 将tensor张量转换成图片格式并保存
  5. Linux系统文件有三个主要的时间属性,分别是ctime(change time), atime(access time), mtime(modify time)...
  6. 帆软报表(finereport)单元格函数,OP参数
  7. Linux 进程概念
  8. 【原创】线上环境 SYN flooding 问题排查
  9. 用户生命周期常用指标_生命周期管理工具如何使用指标
  10. Eclipse 启动项目错误:class not found
  11. 【翻译】YARN Architecture
  12. fortan dll在本地可以运行成功,移植到其他电脑上报错Exception in thread main java.lang.UnsatisfiedLinkError: 找不到指定的模块。
  13. 虚拟内存与物理内存与内存碎片-杂谈
  14. 暑期训练第四次团队赛
  15. 实现酒店无线覆盖和无线漫游
  16. Docker启动报错“Job for docker.service failed because a configured resource limit was exceeded.
  17. Laplance算子(二阶导数)
  18. android 断点下载的实现,Android实现断点下载的方法
  19. auther tonyxiao
  20. vue element 表格增加删除修改数据

热门文章

  1. office无法显示联机服务器,Microsoft Office Outlook出现无法打开服务器的解决方法
  2. 杰理之认识芯片规格书【篇】
  3. Git查看分支从哪个分支创建的
  4. SQL新增、修改和删除数据
  5. sql添加、修改、删除数据,修改删除数据库表。
  6. 通过B-S公式计算期权隐含波动率
  7. 1、Oracle主键自增、清0,与用户授权
  8. google运行速度最近突然变慢了!!怎么回事!!!完美解决!!
  9. 高碳醇类乳状液消泡剂还是一如既往让泡沫感觉到绝望
  10. wincc的画面怎么用博图打开_博图v13如何打开winccflexible sp4文件