v1.0 2nd Edition
自从Android将SDK中的HttpClient去除之后,我开始找替代的HTTP请求类库,推荐的也比较多,其中我对于okhttp比较感兴趣。当时也没有深究只是把它当做Httpclient替代品来用,后来看到越来越多的地方开始谈论Retrofit。渐渐的开始对其有一定的了解并尝试使用,在我开始使用的时候其版本已经到了Retrofit2,它也变得更加强大和易用。
Retrofit2给我最直观的感觉是漂亮。
似乎是在我刚开始学程序设计的时候我看到一句话,Java代码一定要优雅。之前看到Httpclient
在做请求时写的乱糟糟的代码,我曾经做过多次的重构,而每次重构之后虽然解决了问题但是依旧谈不上优雅。之后看到Retrofit用代理的方式做请求,真的是非常喜欢。
TL;DR
一、Httpclient兼容
Httpclient
虽说问题很多安全是一回事,比较烦恼的是用起来比较繁琐。但是无奈之前由于Android内置,它也成了Http请求的首选。再加上某些项目对类库的使用耦合性比较高,这样造成了在替换Httpclient
的时候有很多难以预料的因素。再加上工作日程的安排也没有足够的时间去重构这些,那么在短时间内继续使用Httpclient
也是非常必要的。
这里以Android Studio为例。
1 | android { |
在Module的build.gradle
文件中引用org.apache.http.legacy
这个库,这个库在Android SDK中自带。之后在编译的时候对jar文件进行合并可能会出现文件相同无法执行,这里主要指的是jar文件中的NOTICE
、LICENSE
等文件,使用packagingOptions
指定打包时排除这些文件。而有些时候这个设置不知为何不起作用,那么就只能手动删除gradle
缓存好的jar文件中的对应项了。
二、Retrofit配置
1 | //module/build.gradle |
首先给出gradle
配置。这里需要依赖基本的Retrofit
库,以及相对应的日志拦截器logging-interceptor
。同时本项目要使用到RxJava
那么需要一个retrofit针对RxJava的回调适配器adapter-rxjava
。
1 | private void build(){ |
这是一个针对RxJava的基本配置信息。如果不需要输出日志的话,OkHttpClient.Builder
和后面的Retrofit#client(OkHttpClient)
也就不需要了。后面的initHttps()
和initHeader()
稍后说明,这里值得注意的是在请求回调适配器中使用了RxJava的RxJavaCallAdapterFactory
,这点也在稍后的请求章节中进行说明。
1. HTTPS
1 | private void initHttps(OkHttpClient.Builder builder){ |
这是最基本的忽略掉所有https
请求的验证,也可以根据实际情况针对特定URL地址进行处理。这里可以看到SSL实现的本质就是构建SSLSocketFactory
,因此其它的几种验证可以从这里入手。
2. HTTPS认证
单向认证
基本单个证书
1 | KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); |
没有特殊之处的加载本地证书构建SSLSocketFactory
的方法,像TrusManager
和KeyManager
的生成根据实际情况可能会有所不同。实际上之前用HttpClient的时候由于有SchemeRegistry
的存在所以写了一堆的自定义SocketFactory
,记得在1.0的时候似乎Retrofit#Builder#client()
方法是支持不同的实现的,进而可以复用之前的代码可以将OkHttpClient
替换为ApacheClient
。但是现在的源码中似乎将其固定为了okHttpclient。
多证书
1 | int[] rawArrays = new int[]{};//R.raw.xxx, R.raw.yyy ... |
// TODO 这里有几种情况没有考虑完善,之后经过测试后再仔细补充
这里很直观的将多个证书文件加载到KeyStore
中,其他的部分和之前的情况类似。
双向认证
1 | KeyStore trustKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()); |
双向认证即服务器和客户端都需要验证对方的身份,因此每一方都要持有对方的可信任证书。
服务器:kserver.keystore -> server.crt -> tclient.keystore
客户端:kclient.keystore -> client.crt -> tserver.keystore
过程都是生成自己的证书,导出,导入到自己的信任库中并生成可信任证书。双方交换最后生成的可信任证书,在之后双方的通信中互相提交此证书供对方认证。
生成证书的过程可以使用java自带的keytool工具,或者使用openssl。
但是不论结果如何对于android来说,其持有自己的kclient.keystore和服务器交换而来的tclient.keystore。另外这两个证书都必须转换为bks格式才能使用,而生成bks所需的BC库在JDK中也是自带的,但是可能存在版本问题导致生成的证书存在一些问题。
所以上述代码中R.raw.server
实际上应为tclient.bks
,其添加到客户端的信任管理中TrustManager
。而对应的客户端证书文件R.raw.client
应为kclient.bks
,其添加到KeyManager
中。如果想要带文件名以便区分,这两个文件可以放在assets
文件夹中并使用AssetManager
读取。
到这里感觉设置上和HttpClient有很多相似的地方,并且很多关键的代码都是可以直接迁移而来的,毕竟使用的Java的公有库。
3. Base Authentication
Base Authentication,服务器返回
401 Unauthozied
并在response头里增加WWW-Authentication
字段,内容为Base Realm="xxx"
(xx为提示内容)。客户端使用Base64将用户名和密码编码之后在request头中增加字段Authorization
,内容为Basic yyy
(yyy为Base64编码之后的内容)。
之所以要把Base验证写在这里,是因为Retrofit处理此类验证是可以当做配置项来实现的。
1 | private Authenticator authenticator = new Authenticator() { |
如上述代码在接收到服务器的验证请求后,在请求头中增加认证内容。这样避免了将此功能和服务器多次交互的过程引入到业务逻辑代码中,并且是统一配置非常省心。这里可以想到类似的比如token过期等问题也可以使用同样的思路,但是在实际应用中总是会出现那么不尽如人意之处。Authenticator
只有当服务器返回401的时候才会被调用,但是实际情况是目前大部分服务器API将逻辑上的状态信息封装到通信协议中,这些协议一般是JOSN。当然HTTP状态码也是要考虑的,因为在进入服务器处理逻辑之前,可能会产生HTTP状态直接返回。对于这种情况,就需要使用到下面提到的拦截器。
4、拦截器
1 | private Interceptor interceptor = new Interceptor() { |
以上为拦截器的基本使用方式。其内部的Interceptor#Chain
正如命名是一个链条结构,相应的添加的所有拦截器在使用中都是有序的被依次调用。Chain#request()
方法获取发送前的http请求,这里我们可以做一些发送前的工作,比如增加验证消息。而Chain#process(Request)
方法的返回就是Http请求的返回值。在return
之前我们同样可以对数据做一定的处理,进而方便外部逻辑使用。
正因为拦截器可以控制发送前的请求和服务器返回,因此一些常用的如日志拦截器、token
验证和一些header验证都可以放在这里去实现。
请求头
对于最开始给出的代码中的initHeader()
方法如下。
1 | private void initHeader(OkHttpClient.Builder builder){ |
对于在每个Http请求的header中加入公用内容,很多时候也是非常必要的需求,比如增加服务器认证等等。使用Request.Builder#header(String, String)
(这里的两个参数分别是name和value)设置请求头。另外在每次请求的时候增加请求头,也可以在每个请求的方法上增加@Header
注解(在请求部分进行说明)。
这里
header(String, String)
和addHeader(String, String)
的区别在于,head方法是覆盖式的,而addHeader
是追加方式。所以header
叫做setHeader
才是比较直观的。
同样的针对上一章节所提到的401相关认证也可以使用拦截器来实现。
1 |
|
正是由于拦截器这种控制出(request)入(response)的特点,就如官方所说的它可以实现诸如数据压缩和数据校验等功能。
而针对token
这种要再进行一次服务器通信操作的一样可以写在这里。
1 |
|
这里没有比较正规的代码,仅仅从原理上说明,request和response我们可以拦截掉并再次进行http请求,然后覆盖掉原始的请求逻辑。这个过程外部没有察觉。
Intercept和NetworkIntercept
拦截器有应用拦截器和网络拦截器之分。
由于设计上的原因每次http请求都是通过okhttp执行,所以在和okhttp内核通信的时候以及okhttp内核真正请求网络的时候有两对request和response。从简单的理解上来说前者是逻辑上的请求后者是实际的请求,那么相应的我们可以在这两个位置都设置拦截器,这时候前者被叫做应用拦截器,后者被叫做网络拦截器。
应用拦截器符合我们的逻辑认知发送request和接收response,这是个一来一回的过程。而真正的网络请求情况是十分复杂的,一般来说网络重定向、各类的超时重试等,在实际的操作中是要多次进行http请求的,但是对于逻辑代码来说根本不需要关注这些。在使用中表现出来的现象是对于一次请求应用拦截器只调用一次,网络拦截器可能根据实际的网络请求被多次调用,甚至不调用(使用缓存的情况下)。
1 | OkHttpClient.Builder#addInterceptor(Interceptor); |
使用上也仅仅是调用#addInterceptor
方法和#addNetworkInterceptor
方法的区别。
三、请求
Retrofit吸引人之处,在数据请求上绝对是令人难忘
每次网络请求的调用体现在对应方法的编写上,而这些方法仅仅有一个声明就足够了。于是对于实际http请求的使用体现在接口的编写上,我们只需要声明一个方法,这个方法对应某个功能的具体http请求就足够了。
1 | public interface AccountService { |
实际代码详见本项目在GitHub中的“AccountService.java”文件。这里由于使用了RxJava所以在此请先忽略掉其返回值,对于其他类型的返回值在下面的数据处理和线程控制相关章节进行详细说明。
对于这些注解在retrofit2.http.*
这个包下面可以比较直观的看到。这其中URL相关的注解如@POST
、@GET
、@PUT
等在方法前提供一个url片段,这个片段会连接在retrofit初始化的时候设置的那个BASE_URL
之后。听到有其他朋友说这个url片段不能以斜杠/
结尾,但是我在测试的后发现即便写一个斜杠也是可以的。另外这里的ActionPath
为自定义的一个注解在之后的数据解析章节进行说明。
之后是一些参数注解如@Query(value)
、@Field(value)
其指定后面传入的参数在http请求中对应的参数名。还有比如@Path(value)
这种参数是用作动态替换url片段使用的,如/repos/{owner}/{repo}/contributors
这种,如果使用@Path("user"), @Path("repo")
参数注解,则会将相应的内容替换到url片段当中。这种情况比较常用于那种二级域名或者个性化域名以及多目录的url中。但是这里依旧无法处理动态url,所以之后又引入了@Url(value)
参数注解。使用此注解的时候前面的@Query
等注解可以不写url,如此处方法registerParameter(@Url String url)
。而另外的比如map参数注解@QueryMap
、@FieldMap
等,以及表单@FormUrlEncoded
都是比较直观的。而比如像看起来比较麻烦但是又非常有用的@Multipart
文件上传之后再进行补充。
Header
请求头在前面部分有过说明,但是总觉得使用起来非常繁琐,并且很多时候都是全局设置。如果想针对单个请求设置请求头的话,这里就需要使用到@Header
和@Headers
注解了。
1 | "user") ( |
以上列举的三种情况。第一种为参数注解,此时动态向本次请求头中增加一个Authoriztion
信息,并且这个值是动态传入的。第二种使用方法注解,将这个请求参数固定下,第三种为固定的传入一个数组。这点在看源代码之后就很容易理解了,@Header
中接收的是一个字符串,而@Headers
中接收的是一个字符数组。
四、数据转换器Converter
1.协议与数据结构
在说明如何将http返回的原始数据转换成自定义的实体类之前,先定义一些数据结构实体以便规范之后的使用。
1 | { |
首先假设服务器API接口返回是数据遵循以上格式,每次必有一个整型的code
值用于标识此次请求的状态,message
为针当前code
的简要说明。data
为实际的返回数据内容其包括各种基本数据类型,或者是一个JSON对象、数组等,当然也可以不存在(比如仅仅用作成功失败的接口就不需要额外的数据返回)。
1 | public class HttpResponse<T> { |
相关源代码请参见GitHub项目中的model目录
。
首先定义一个HttpResponse
用于代表整个HTTP返回信息,其结构与JSON协议类似。同时这样设计处理兼容API中的协议之外,还兼顾到在进入服务器逻辑代码之前的各种HTTP操作情况。比如各种IO和网络异常等,这样为了便于统一处理。之后的抽象类ResponseEntity
在实际使用中其作用就是泛型的上限,即HttpResponse
。其内部也设计的code
和message
两个属性,这个目的是有些API设计外部的HttpResponse#code
为整体的请求状态码,而内部针对各个业务逻辑又有单独的状态码这种情况,比如返回的是一个列表的情况下一部分数据正常一部分不正常,此时就需要ResponseEntity#code
进行判断。
HttpResponse
之所以设计成泛型T
而非T extend ResponseEntity
。这里有以下几种考虑。
T
可能是基本数据类型。最常见的情况就是一个字符串HttpResponse
。T
可能是更在复杂的复合型类型。如HttpResponse
。参见上文代码中AccountService#getCountryRegionList():Observable<httpresponse<list<countryregionentity>>></httpresponse<list<countryregionentity>
部分。
2. Converter和Factory
interface retrofit2.Converter <f, t=””>与 abstract retrofit2.Converter.Factory</f,>
使用方法为Retrofit.Builder#addConverterFactory(Converter.Factory)
,这里提供的源代码有两个实现,标准的实现参见retrofit目录下
的类JsonConverterFactory.java
和类AccountConverter.java
。可运行版本参见JUnit目录
中的类ExampleJsonConverterFactory.java
和类ExampleJsonHelper.java
。后者可以通过三个测试同步异步调用的类ExampleRetrofitAsyncTest
、ExampleRetrofitRxTest
和ExampleRetrofitSyncTest
通过JUnit直接运行,这方面在后文同步异步请求章节进行详细说明。
1 | public interface Converter<F, T> { |
以上为Converter
源代码,为了便于展示说明简化了参数命名和省略掉了一些类的结构。
首先这里的Converter
看起来其泛型要表达的意思就是from
和to
了,其实际是由内部分工厂类Factory
来实例化的。其工厂提供三个方法,分别是接收到http response之后将ResponseBody
转换成我们定义的数据类型?
,第二个为在发送http request时将我们的自定义类型转换为RequestBody
,第三个没有实际使用过稍后测试。
requestBodyConverter
方法很多时候和@Body
注解配合使用,很多时候直接使用Type
参数做类型判断,将结果直接转换成ResquestBody
。值得一提是这里不仅可以获得方法注解,还可以获得参数注解,所以这里从编程上看就有很大发挥空间了。详情请参见官方自带的Gson
转换器实现,更多实例稍后补充。
responseBodyConverter
是必须要实现的方法,毕竟返回是一定要解析的。这里的参数分别是之前编写的请求接口返回的参数泛型对应的类型,方法注解和Retrofit实例。在实际使用中每个接口返回的数据是不同的,而这里可以用的参数只有类型和注解,对于JSON数据结构Gson
转换器是直接根据Type
将ResponseBody
的内容填充到实例中。接下来提供一种一般的JSON转换器的实现。
1 | class ExampleJsonConverterFactory extends Converter.Factory{ |
此转换器将ResponseBody
转换成HttpResponse
,当然由于返回泛型为?
所以这里的JsonConverter
也可以动态的设置泛型。那么剩下的问题就是如何知道当前服务器返回的数据要转换成什么类型呢?这里就是要如何确定HttpResponse
中的泛型。查看官方的几个实现,比如GsonConverterFactory
、JacksonConverterFactory
和ProtoConverterFactory
都是使用Type
判断返回的泛型类型,这样做都是直接把返回值的字段和给出的泛型对象字段进行一一对应的。我从很早就开始比较反感这类写法,把服务器返回的字段和本地实体成员变量进行对应。所以这里设计另一套解析方式。
3、使用自定义注解>
1 | (METHOD) |
从Converter.Factory
的方法中我们只能获得Type
和方法注解,Type
个人感觉灵活性不足那么剩下的方法就是使用方法注解了。这里定义一个@ActionPath
的注解,其作用是传入一个字符串。将这个注解写在之前定义的请求接口上,随后这个注解就会传入Converter.Factory#responseBodyConverter
方法中。注解传值非常的灵活,甚至你可以直接传一个Class
进来直接实例化,这时的Factory
就完全不需要管内容了。
参见上文的ExampleJsonConverterFactory#responseBodyConverter
方法@GET
、@POST
等方法的url片段判断当前请求要返回的数据类型,但是遇到动态url@Url
的时候就无法使用了,这是就使用@ActionPath
做判断。这里定义的ModelAction.java
类,其提供一个功能列表表示请求的Url片段地址并规定此请求对应的功能列表,这里的值传入转换器供其处理。
另外这里可以看到我们处理的仅仅为ResponseBody
对象,对于外部Http对应的Response这里是无法获得。所以这种写法并不能完全控制整个Http请求的返回(关于这点请参见《Retrofit 适配器与转换器》中的“调用适配器”章节)。
五、同步与异步请求
1 | public interface ExampleServiceTest { |
上述源码参见GitHubExampleServiceTest.java
其中定义了几种常用的请求返回调用方法。
1. Retrofit Call
首先的官方的一般方法返回Call
这里实际是包装了okHttp
,值得注意的是返回的Call
并没有真正开始http请求。
1 | RetrofitHelperTest helperTest = RetrofitHelperTest.getInstance(RetrofitHelperTest.TYPE_ASYNC); |
以上两种方法源码参见GitHub同步请求ExampleRetrofitSyncTest.java
和异步请求ExampleRetrofitAsyncTest.java
。这里的同步异步是Call
原生支持的也不必多说,源码已经打印线程信息使用Junit运行即可查看两者不同。
2. Rxjava
Retrofit.Builder#addCallAdapterFactory(RxJavaCallAdapterFactory.create())
对于RxJava的支持需要添加RxJava回调适配器RxJavaCallAdapterFactory.create()
,这样在接口返回即可支持Observable
。
1 | RetrofitHelperTest helperTest = RetrofitHelperTest.getInstance(RetrofitHelperTest.TYPE_RX); |
而RxJava的同步异步控制就更加方便,只需要控制Observable#subscribeOn(Schedulers)
和Observable#observeOn(Schedulers)
即可。以上代码参见GiuHub类ExampleRetrofitRxTest.java
。
3. 同步返回对象
1 | RetrofitHelperTest helperTest = RetrofitHelperTest.getInstance(RetrofitHelperTest.TYPE_SYNC); |
以上这种直接返回所需的对象应该是最开始想到的,前面对RxJava支持需要添加RxJava的回调适配器,那么这种没有默认支持的方法很显然也需要实现一个回调适配器。
1 | class ExampleCallAdapterFactoryTest extends CallAdapter.Factory{ |
这里可以看出其结构和之前的Converter非常相似,这里是直接将同步请求封装在内部。从内部外部看都觉得非常简洁直观,但是这有个非常严重的问题那么就是我们无法控制Http请求,最简单的场景就是无法取消Http请求。而对取消请求Call#ancel()
和Subscription#unsubscribe()
都非常方便。
最后在使用上也没有过多要说明的地方,但是个人觉得这里的ConverterFactory
和CallAdapterFactory
非常有趣。并且由于Retrofit源代码其实非常少并且代码注释写的非常好,各种请求逻辑实际上在okhttp
中也不需要过多的关注,因此针对数据转换与回调在稍后会专门写一篇文进行详细说明。
六、与MVP配合使用>
这个系列的本质是MVP + Retrofit + RxJava的实例,所以到最后还是要归于如何使用上来。关于MVP
请参考《Android MVP模式》,这里就继续。
首先是之前设计的类HttpResponse
,这里的泛型T
才是我们需要的实际数据。因此Http返回是数据传回到Presenter
层,并进行数据解封将T
传回UI层。
1 | public void login(String mobile, String vCode, String area){ |
源代码参见GitHubDefaultLoginPresenter.java
。由于这里使用RxJava的Observable
,其在开始执行订阅操作的时候才进行Http请求并在UI线程回调。这里使用Subscription
的作用仅仅是做自动取消绑定,减少资源消耗和避免由于线程未及时结束可能造成内存泄露问题。
这里的逻辑是为了获得登录信息,因此仅仅是将LogInActionEntity
这个对象传回到BaseView
中。而所有的异常信息都被Subscriber#onError(Throwable)
捕获,如果愿意可以将Subscriber
再次进行封装,将所有的情况全部归于HtpResponse
进行管理。
最后。由于Retrofit的简洁与强大的功能,如果使用一定会喜欢的吧。