v1.0 2nd Edition

自从Android将SDK中的HttpClient去除之后,我开始找替代的HTTP请求类库,推荐的也比较多,其中我对于okhttp比较感兴趣。当时也没有深究只是把它当做Httpclient替代品来用,后来看到越来越多的地方开始谈论Retrofit。渐渐的开始对其有一定的了解并尝试使用,在我开始使用的时候其版本已经到了Retrofit2,它也变得更加强大和易用。

Retrofit2给我最直观的感觉是漂亮。

似乎是在我刚开始学程序设计的时候我看到一句话,Java代码一定要优雅。之前看到Httpclient在做请求时写的乱糟糟的代码,我曾经做过多次的重构,而每次重构之后虽然解决了问题但是依旧谈不上优雅。之后看到Retrofit用代理的方式做请求,真的是非常喜欢。

TL;DR

  1. Httpclient兼容
  2. Retrofit配置
  3. 请求
  4. 数据转换器Converter
  5. 同步与异步请求
  6. 与MVP配合使用

一、Httpclient兼容

Httpclient虽说问题很多安全是一回事,比较烦恼的是用起来比较繁琐。但是无奈之前由于Android内置,它也成了Http请求的首选。再加上某些项目对类库的使用耦合性比较高,这样造成了在替换Httpclient的时候有很多难以预料的因素。再加上工作日程的安排也没有足够的时间去重构这些,那么在短时间内继续使用Httpclient也是非常必要的。

这里以Android Studio为例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
android {  
...
useLibrary 'org.apache.http.legacy'
....

dependencies {
...
compile ('org.apache.httpcomponents:httpcore:4.4.4'){
exclude group: 'org.apache.httpcomponents', module: 'httpclient'
}
compile ('org.apache.httpcomponents:httpmime:4.5.2'){
exclude group: 'org.apache.httpcomponents', module: 'httpclient'
}
...
}

packagingOptions{
exclude `META-INFO/NOTICE`
...
}
}

在Module的build.gradle文件中引用org.apache.http.legacy这个库,这个库在Android SDK中自带。之后在编译的时候对jar文件进行合并可能会出现文件相同无法执行,这里主要指的是jar文件中的NOTICELICENSE等文件,使用packagingOptions指定打包时排除这些文件。而有些时候这个设置不知为何不起作用,那么就只能手动删除gradle缓存好的jar文件中的对应项了。

二、Retrofit配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//module/build.gradle
android{
...

dependencies{
...
compile 'com.squareup.retrofit2:retrofit:2.1.0'
compile 'io.reactivex:rxjava:1.2.0'
compile 'io.reactivex:rxandroid:1.2.1'
compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0'
compile 'com.squareup.okhttp3:logging-interceptor:3.4.1'
...
}
}

首先给出gradle配置。这里需要依赖基本的Retrofit库,以及相对应的日志拦截器logging-interceptor。同时本项目要使用到RxJava那么需要一个retrofit针对RxJava的回调适配器adapter-rxjava

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void build(){
OkHttpClient.Builder builder = new OkHttpClient.Builder();
if (BuildConfig.DEBUG){
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
builder.addInterceptor(interceptor);
}
builder.connectTimeout(5, TimeUnit.SECONDS);
initHttps(builder);
initHeader(builder);

retrofit = new Retrofit.Builder().client(builder.build())
.addConverterFactory(JsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.baseUrl(BASE_URL).build();
}

这是一个针对RxJava的基本配置信息。如果不需要输出日志的话,OkHttpClient.Builder和后面的Retrofit#client(OkHttpClient)也就不需要了。后面的initHttps()initHeader()稍后说明,这里值得注意的是在请求回调适配器中使用了RxJava的RxJavaCallAdapterFactory,这点也在稍后的请求章节中进行说明。

1. HTTPS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
private void initHttps(OkHttpClient.Builder builder){
SSLContext sslContext = null;
try {
sslContext = SSLContext.getInstance("SSL");
}catch (Exception ex){
LL.w(TAG, "", ex);
}
if (sslContext == null){
return;
}

X509TrustManager x509TrustManager = new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
// ignored
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
// ignored
}

@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
};
try {
sslContext.init(null, new TrustManager[]{x509TrustManager}, new SecureRandom());
}catch (Exception ex){
LL.w(TAG, "", ex);
}
builder.sslSocketFactory(sslContext.getSocketFactory());
builder.hostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
});
}

这是最基本的忽略掉所有https请求的验证,也可以根据实际情况针对特定URL地址进行处理。这里可以看到SSL实现的本质就是构建SSLSocketFactory,因此其它的几种验证可以从这里入手。

2. HTTPS认证

单向认证

基本单个证书

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
char[] password = "".toCharArray(); // password
InputStream keyStoreFile = context.getResources().openRawResource(0);// R.raw.xxx
keyStore.load(keyStoreFile, password);

SSLContext sslContext = SSLContext.getInstance("SSL");
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);

KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, password);

sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom());

SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();

没有特殊之处的加载本地证书构建SSLSocketFactory的方法,像TrusManagerKeyManager的生成根据实际情况可能会有所不同。实际上之前用HttpClient的时候由于有SchemeRegistry的存在所以写了一堆的自定义SocketFactory,记得在1.0的时候似乎Retrofit#Builder#client()方法是支持不同的实现的,进而可以复用之前的代码可以将OkHttpClient替换为ApacheClient。但是现在的源码中似乎将其固定为了okHttpclient。

多证书

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int[] rawArrays = new int[]{};//R.raw.xxx, R.raw.yyy ...

CertificateFactory certificateFactory = CertificateFactory.getInstance("x.509");
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);

InputStream inputStream;
for (int index = 0; index < rawArrays.length; index ++){
inputStream = context.getResources().openRawResource(rawArrays[index]);
keyStore.setCertificateEntry(String.valueOf(index), certificateFactory.generateCertificate(inputStream));
// close input stream
}

SSLContext sslContext = SSLContext.getInstance("SSL");
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());

SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();

// TODO 这里有几种情况没有考虑完善,之后经过测试后再仔细补充

这里很直观的将多个证书文件加载到KeyStore中,其他的部分和之前的情况类似。

双向认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
KeyStore trustKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());

InputStream serverFileStream = context.getResources().openRawResource(0);// R.raw.server
trustKeyStore.load(serverFileStream, "".toCharArray());//trust keystore password
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());

KeyStore clientKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
InputStream clientFileStream = context.getResources().openRawResource(0);// R.raw.client
clientKeyStore.load(clientFileStream,"".toCharArray());//client keystore password
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(clientKeyStore, "".toCharArray()); //client keystore password

SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom());

SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();

双向认证即服务器和客户端都需要验证对方的身份,因此每一方都要持有对方的可信任证书。

服务器: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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private Authenticator authenticator = new Authenticator() {
@Override
public Request authenticate(Route route, Response response) throws IOException {
String basic = "Basic " + "xxxx"; //Base64 encode string
Request.Builder builder = response.request().newBuilder();
builder.header("Authorization", basic);
...
return builder.build();
}
};

private void buildRetrofit(){
OkHttpClient.Builder httpBuilder = new OkHttpClient.Builder();

Retrofit.Builder retrofitBuilder = new Retrofit.Builder();
httpBuilder.authenticator(authenticator);
retrofitBuilder.client(httpBuilder.build());
...
}

如上述代码在接收到服务器的验证请求后,在请求头中增加认证内容。这样避免了将此功能和服务器多次交互的过程引入到业务逻辑代码中,并且是统一配置非常省心。这里可以想到类似的比如token过期等问题也可以使用同样的思路,但是在实际应用中总是会出现那么不尽如人意之处。Authenticator只有当服务器返回401的时候才会被调用,但是实际情况是目前大部分服务器API将逻辑上的状态信息封装到通信协议中,这些协议一般是JOSN。当然HTTP状态码也是要考虑的,因为在进入服务器处理逻辑之前,可能会产生HTTP状态直接返回。对于这种情况,就需要使用到下面提到的拦截器。

4、拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
private Interceptor interceptor  = new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
...
Response response = chain.proceed(request);
...
return response;
}
};

OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.addInterceptor(interceptor);

以上为拦截器的基本使用方式。其内部的Interceptor#Chain正如命名是一个链条结构,相应的添加的所有拦截器在使用中都是有序的被依次调用。Chain#request()方法获取发送前的http请求,这里我们可以做一些发送前的工作,比如增加验证消息。而Chain#process(Request)方法的返回就是Http请求的返回值。在return之前我们同样可以对数据做一定的处理,进而方便外部逻辑使用。

正因为拦截器可以控制发送前的请求和服务器返回,因此一些常用的如日志拦截器、token验证和一些header验证都可以放在这里去实现。

请求头

对于最开始给出的代码中的initHeader()方法如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
private void initHeader(OkHttpClient.Builder builder){
builder.addInterceptor(new Interceptor() {
@Override
public Response intercept(Interceptor.Chain chain) throws IOException {
Request original = chain.request();
Request.Builder requestBuilder = original.newBuilder();
// add request header
// 可以增加针对服务端的特定请求头
Request request = requestBuilder.build();
return chain.proceed(request);
}
});
}

对于在每个Http请求的header中加入公用内容,很多时候也是非常必要的需求,比如增加服务器认证等等。使用Request.Builder#header(String, String)(这里的两个参数分别是name和value)设置请求头。另外在每次请求的时候增加请求头,也可以在每个请求的方法上增加@Header注解(在请求部分进行说明)。

这里header(String, String)addHeader(String, String)的区别在于,head方法是覆盖式的,而addHeader是追加方式。所以header叫做setHeader才是比较直观的。

同样的针对上一章节所提到的401相关认证也可以使用拦截器来实现。

1
2
3
4
5
6
7
8
9
10
@Override
public Response intercept(Chain chain) throws IOException {
Request original = chain.request();
String basic = "Basic " + "xxxx"; //Base64 encode string
Request.Builder builder = original.newBuilder();
builder.header("Authorization", basic);
builder.method(original.method(), original.body());
Response response = chain.proceed(builder.build());;
return response;
}

正是由于拦截器这种控制出(request)入(response)的特点,就如官方所说的它可以实现诸如数据压缩和数据校验等功能。

而针对token这种要再进行一次服务器通信操作的一样可以写在这里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
public Response intercept(Chain chain) throws IOException {
Request originalRequest = chain.request();
Response originalResponse = chain.proceed(originalRequest);
ResponseBody responseBody = originalResponse.body();
BufferedSource source = responseBody.source();
source.request(Long.MAX_VALUE);
Buffer buffer = source.buffer();

MediaType mediaType = responseBody.contentType();
// test MIME is text
String stringBody = buffer.clone().readString(Charset.defaultCharset());// android UTF-8

//if token out of date
ExampleModelServiceTest serviceTest = null; // get Service
Call<String> call = serviceTest.refreshToken();
String token = call.execute().body();

Request request = originalRequest.newBuilder().header("token", token).build();
originalResponse.close();
return chain.proceed(request);
}

这里没有比较正规的代码,仅仅从原理上说明,request和response我们可以拦截掉并再次进行http请求,然后覆盖掉原始的请求逻辑。这个过程外部没有察觉。

Intercept和NetworkIntercept

拦截器有应用拦截器和网络拦截器之分。

由于设计上的原因每次http请求都是通过okhttp执行,所以在和okhttp内核通信的时候以及okhttp内核真正请求网络的时候有两对request和response。从简单的理解上来说前者是逻辑上的请求后者是实际的请求,那么相应的我们可以在这两个位置都设置拦截器,这时候前者被叫做应用拦截器,后者被叫做网络拦截器。

应用拦截器符合我们的逻辑认知发送request和接收response,这是个一来一回的过程。而真正的网络请求情况是十分复杂的,一般来说网络重定向、各类的超时重试等,在实际的操作中是要多次进行http请求的,但是对于逻辑代码来说根本不需要关注这些。在使用中表现出来的现象是对于一次请求应用拦截器只调用一次,网络拦截器可能根据实际的网络请求被多次调用,甚至不调用(使用缓存的情况下)。

1
2
OkHttpClient.Builder#addInterceptor(Interceptor);
OkHttpClient.Builder#addNetworkInterceptor(Interceptor);

使用上也仅仅是调用#addInterceptor方法和#addNetworkInterceptor方法的区别。

三、请求

Retrofit吸引人之处,在数据请求上绝对是令人难忘

每次网络请求的调用体现在对应方法的编写上,而这些方法仅仅有一个声明就足够了。于是对于实际http请求的使用体现在接口的编写上,我们只需要声明一个方法,这个方法对应某个功能的具体http请求就足够了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public interface AccountService {

@GET(ModelAction.PATH_GET_AUTH_CODE)
Observable<HttpResponse<AuthCodeEntity>> getAuthCode(@Query("mobile") String mobile,
@Query("clientType") String clientType,
@Query("sendType") String sendType);

@POST(ModelAction.PATH_LOGIN)
Observable<HttpResponse<LogInActionEntity>> login(@Query("mobile") String mobile,
@Query("vCode") String vCode,
@Query("area") String area);

@FormUrlEncoded
@POST(ModelAction.PATH_REGISTER)
Observable<HttpResponse<RegisterActionEntity>> register1(@Field("mobile") String mobile,
@Field("captcha") String captcha,
@Field("password") String password,
@Field("area") String area);

@POST(ModelAction.PATH_REGISTER)
Observable<HttpResponse<RegisterActionEntity>> register(@Query("mobile") String mobile,
@Query("captcha") String captcha,
@Query("password") String password,
@Query("area") String area);

@ActionPath(ModelAction.PATH_REGISTER)
@POST
Observable<HttpResponse<RegisterActionEntity>> registerParameter(@Url String url);

@GET(ModelAction.PATH_COUNTRY_REGION)
Observable<HttpResponse<List<CountryRegionEntity>>> getCountryRegionList();
}

实际代码详见本项目在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@Headers注解了。

1
2
3
4
5
6
7
8
9
10
11
12
13
@GET("user")
Call<User> getUser(@Header("Authoriztion") String authoriztion);

@Headers("Authoriztion: xxxxxxxxx")
@GET("user")
Call<User> getUser();

@Headers({
"Authoriztion: xxxxxxxxx",
"User-Agent: UA"
})
@GET("user")
Call<User> getUser();

以上列举的三种情况。第一种为参数注解,此时动态向本次请求头中增加一个Authoriztion信息,并且这个值是动态传入的。第二种使用方法注解,将这个请求参数固定下,第三种为固定的传入一个数组。这点在看源代码之后就很容易理解了,@Header中接收的是一个字符串,而@Headers中接收的是一个字符数组。

四、数据转换器Converter

1.协议与数据结构

在说明如何将http返回的原始数据转换成自定义的实体类之前,先定义一些数据结构实体以便规范之后的使用。

1
2
3
4
5
{
code: 200,
message: "",
data : {}
}

首先假设服务器API接口返回是数据遵循以上格式,每次必有一个整型的code值用于标识此次请求的状态,message为针当前code的简要说明。data为实际的返回数据内容其包括各种基本数据类型,或者是一个JSON对象、数组等,当然也可以不存在(比如仅仅用作成功失败的接口就不需要额外的数据返回)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class HttpResponse<T> {

private long code;
private String message;

private T data;

...
// getter and setter
}

public abstract class ResponseEntity {

protected long code;
protected String message;

...
//getter and setter
}

相关源代码请参见GitHub项目中的model目录

首先定义一个HttpResponse用于代表整个HTTP返回信息,其结构与JSON协议类似。同时这样设计处理兼容API中的协议之外,还兼顾到在进入服务器逻辑代码之前的各种HTTP操作情况。比如各种IO和网络异常等,这样为了便于统一处理。之后的抽象类ResponseEntity在实际使用中其作用就是泛型的上限,即HttpResponse。其内部也设计的codemessage两个属性,这个目的是有些API设计外部的HttpResponse#code为整体的请求状态码,而内部针对各个业务逻辑又有单独的状态码这种情况,比如返回的是一个列表的情况下一部分数据正常一部分不正常,此时就需要ResponseEntity#code进行判断。

HttpResponse之所以设计成泛型T而非T extend ResponseEntity。这里有以下几种考虑。

  1. T可能是基本数据类型。最常见的情况就是一个字符串HttpResponse
  2. 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。后者可以通过三个测试同步异步调用的类ExampleRetrofitAsyncTestExampleRetrofitRxTestExampleRetrofitSyncTest通过JUnit直接运行,这方面在后文同步异步请求章节进行详细说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface Converter<F, T> {

T convert(F value);

abstract class Factory {

public Converter<ResponseBody, ?> responseBodyConverter(Type t, Annotation[] a,
Retrofit r)...

public Converter<?, RequestBody> requestBodyConverter(Type t, Annotation[] pa, Annotation[] ma, Retrofit r)...

public Converter<?, String> stringConverter(Type t, Annotation[] a,
Retrofit r)...
}
}

以上为Converter源代码,为了便于展示说明简化了参数命名和省略掉了一些类的结构。

首先这里的Converter看起来其泛型要表达的意思就是fromto了,其实际是由内部分工厂类Factory来实例化的。其工厂提供三个方法,分别是接收到http response之后将ResponseBody转换成我们定义的数据类型?,第二个为在发送http request时将我们的自定义类型转换为RequestBody,第三个没有实际使用过稍后测试。

requestBodyConverter方法很多时候和@Body注解配合使用,很多时候直接使用Type参数做类型判断,将结果直接转换成ResquestBody。值得一提是这里不仅可以获得方法注解,还可以获得参数注解,所以这里从编程上看就有很大发挥空间了。详情请参见官方自带的Gson转换器实现,更多实例稍后补充。

responseBodyConverter是必须要实现的方法,毕竟返回是一定要解析的。这里的参数分别是之前编写的请求接口返回的参数泛型对应的类型,方法注解和Retrofit实例。在实际使用中每个接口返回的数据是不同的,而这里可以用的参数只有类型和注解,对于JSON数据结构Gson转换器是直接根据TypeResponseBody的内容填充到实例中。接下来提供一种一般的JSON转换器的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class ExampleJsonConverterFactory extends Converter.Factory{

private static ExampleJsonConverterFactory INSTANCE= new ExampleJsonConverterFactory();

static ExampleJsonConverterFactory create(){
return INSTANCE;
}

@Override
public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
for (Annotation annotation : annotations){
if (annotation instanceof GET){
GET getMethod = (GET)annotation;
if (JUnitUtils.isEmpty(getMethod.value())) continue;
return new JsonConverter(getMethod.value());
}else if (annotation instanceof POST){
POST postMethod = (POST)annotation;
if (JUnitUtils.isEmpty(postMethod.value())) continue;
return new JsonConverter(postMethod.value());
}else if (annotation instanceof ActionPath){
ActionPath actionPath = (ActionPath)annotation;
if (JUnitUtils.isEmpty(actionPath.value())) continue;
return new JsonConverter(actionPath.value());
}
}
return new JsonConverter("");
}

private class JsonConverter implements Converter<ResponseBody, HttpResponse<?>> {

private String path;
JsonConverter(String path){
this.path = path;
}
@Override
public HttpResponse<?> convert(ResponseBody value) throws IOException {
return ExampleJsonHelper.parse(path, value.string());
}
}
}

此转换器将ResponseBody转换成HttpResponse,当然由于返回泛型为?所以这里的JsonConverter也可以动态的设置泛型。那么剩下的问题就是如何知道当前服务器返回的数据要转换成什么类型呢?这里就是要如何确定HttpResponse中的泛型。查看官方的几个实现,比如GsonConverterFactoryJacksonConverterFactoryProtoConverterFactory都是使用Type判断返回的泛型类型,这样做都是直接把返回值的字段和给出的泛型对象字段进行一一对应的。我从很早就开始比较反感这类写法,把服务器返回的字段和本地实体成员变量进行对应。所以这里设计另一套解析方式。

3、使用自定义注解>

1
2
3
4
5
@Target(METHOD)
@Retention(RUNTIME)
public @interface ActionPath {
String value() default "";
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface ExampleServiceTest {

@GET(ModelAction.TEST_PATH_APIS)
HttpResponse<Map<String, String>> queryAPIS();

/***** retrofit async *****/

@GET(ModelAction.TEST_PATH_APIS)
Call<HttpResponse<Map<String, String>>> asyncQueryAPIS();

@GET("/repos/{owner}/{repo}/contributors")
Call<HttpResponse<Map<String, String>>> asyncRepoContributors(@Path("owner") String owner, @Path("repo") String repo);

/***** rxJava *******/
@GET(ModelAction.TEST_PATH_APIS)
Observable<HttpResponse<Map<String, String>>> rxQueryAPIS();

Observable<HttpResponse<Map<String, String>>> rxrRpoContributors(@Path("owner") String owner, @Path("repo") String repo);
}

上述源码参见GitHubExampleServiceTest.java其中定义了几种常用的请求返回调用方法。

1. Retrofit Call

首先的官方的一般方法返回Call这里实际是包装了okHttp,值得注意的是返回的Call并没有真正开始http请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
RetrofitHelperTest helperTest = RetrofitHelperTest.getInstance(RetrofitHelperTest.TYPE_ASYNC);
ExampleServiceTest serviceTest = helperTest.getExampleServiceTest();

...
// Call sync
Call<HttpResponse<Map<String, String>>> call = serviceTest.asyncQueryAPIS();
Response<HttpResponse<Map<String, String>>> response = call.execute();
HttpResponse<Map<String, String>> httpResponse = response.body();

...

// Call Async
call.enqueue(new Callback<HttpResponse<Map<String, String>>>() {
@Override
public void onResponse(Call<HttpResponse<Map<String, String>>> call,
Response<HttpResponse<Map<String, String>>> response) {
HttpResponse<Map<String, String>> httpResponse = response.body();
}
@Override
public void onFailure(Call<HttpResponse<Map<String, String>>> call, Throwable t) {
...
}
})

以上两种方法源码参见GitHub同步请求ExampleRetrofitSyncTest.java和异步请求ExampleRetrofitAsyncTest.java。这里的同步异步是Call原生支持的也不必多说,源码已经打印线程信息使用Junit运行即可查看两者不同。

2. Rxjava

Retrofit.Builder#addCallAdapterFactory(RxJavaCallAdapterFactory.create())

对于RxJava的支持需要添加RxJava回调适配器RxJavaCallAdapterFactory.create(),这样在接口返回即可支持Observable

1
2
3
4
5
6
7
RetrofitHelperTest helperTest = RetrofitHelperTest.getInstance(RetrofitHelperTest.TYPE_RX);
ExampleServiceTest serviceTest = helperTest.getExampleServiceTest();
Observable<HttpResponse<Map<String, String>>> observable = serviceTest.rxQueryAPIS();

observable.subscribeOn(Schedulers.immediate())
.observeOn(Schedulers.newThread())
.subscribe(Subscriber);

而RxJava的同步异步控制就更加方便,只需要控制Observable#subscribeOn(Schedulers)Observable#observeOn(Schedulers)即可。以上代码参见GiuHub类ExampleRetrofitRxTest.java

3. 同步返回对象

1
2
3
RetrofitHelperTest helperTest = RetrofitHelperTest.getInstance(RetrofitHelperTest.TYPE_SYNC);
ExampleServiceTest serviceTest = helperTest.getExampleServiceTest();
HttpResponse<Map<String, String>> response = serviceTest.queryAPIS();

以上这种直接返回所需的对象应该是最开始想到的,前面对RxJava支持需要添加RxJava的回调适配器,那么这种没有默认支持的方法很显然也需要实现一个回调适配器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ExampleCallAdapterFactoryTest extends CallAdapter.Factory{

@Override
public CallAdapter<?> get(final Type returnType, Annotation[] annotations, Retrofit retrofit) {
return new CallAdapter<Object>() {
@Override
public Type responseType() {
return returnType;
}

@Override
public <R> Object adapt(Call<R> call) {
try {
return call.execute().body();
}catch (Exception ex){
throw new RuntimeException();
}
}
};
}
}

这里可以看出其结构和之前的Converter非常相似,这里是直接将同步请求封装在内部。从内部外部看都觉得非常简洁直观,但是这有个非常严重的问题那么就是我们无法控制Http请求,最简单的场景就是无法取消Http请求。而对取消请求Call#ancel()Subscription#unsubscribe()都非常方便。

最后在使用上也没有过多要说明的地方,但是个人觉得这里的ConverterFactoryCallAdapterFactory非常有趣。并且由于Retrofit源代码其实非常少并且代码注释写的非常好,各种请求逻辑实际上在okhttp中也不需要过多的关注,因此针对数据转换与回调在稍后会专门写一篇文进行详细说明。

六、与MVP配合使用>

这个系列的本质是MVP + Retrofit + RxJava的实例,所以到最后还是要归于如何使用上来。关于MVP请参考《Android MVP模式》,这里就继续。

首先是之前设计的类HttpResponse,这里的泛型T才是我们需要的实际数据。因此Http返回是数据传回到Presenter层,并进行数据解封将T传回UI层。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void login(String mobile, String vCode, String area){
baseView.onLoginLoading();
Observable<HttpResponse<LogInActionEntity>> observable = accountService.login(mobile, vCode, area);
addSubscription(observable, new Subscriber<HttpResponse<LogInActionEntity>>() {
@Override
public void onCompleted() {
baseView.overLoading();
}
@Override
public void onError(Throwable e) {
baseView.onFail(e.getMessage());
}
@Override
public void onNext(HttpResponse<LogInActionEntity> response) {
if (response.getCode() != HttpResponse.CODE_SUCCESS){
baseView.onFail(response.getMessage());
}else {
baseView.onLogInSuccess(response.getData());
}
}
});
}

源代码参见GitHubDefaultLoginPresenter.java。由于这里使用RxJava的Observable,其在开始执行订阅操作的时候才进行Http请求并在UI线程回调。这里使用Subscription的作用仅仅是做自动取消绑定,减少资源消耗和避免由于线程未及时结束可能造成内存泄露问题。

这里的逻辑是为了获得登录信息,因此仅仅是将LogInActionEntity这个对象传回到BaseView中。而所有的异常信息都被Subscriber#onError(Throwable)捕获,如果愿意可以将Subscriber再次进行封装,将所有的情况全部归于HtpResponse进行管理。

最后。由于Retrofit的简洁与强大的功能,如果使用一定会喜欢的吧。