Retrofit 2.0 超能实践(一)完美支持加密Https传输

RxJava+Retrofit 系列文章

Posted by Tamic on 2016-06-07

前阵子看到圈子里Retrofit 2.0,RxJava(Android), OkHttp3.3 ,加之支持Android和 iOS 的React Native 热更新技术, 火的不要不要的, 2015年新技术一大波来袭 ,看着自己项目还在用httpClient, asyncTask的原生开发 感觉自己已成火星人,实在顶不住内心强烈的自卑感,加之对新技术的追求,入手移动开发新三剑客,运用在目前的项目中,虽然目前关于他们的介绍资料网上一大把,但是自己亲自实践后,发现坑不少,为了能方便其他人安全顺利入坑,今天就先从Retrofit说起,前方高能,准备躲避。

Retrofit 2.0

Retrofit是SQUARE美国一家移动支付公司最近新发布的在Android平台上http访问的开源项目


一 什么Retrofit

官方标语;A type-safe HTTP client for Android and Java
语意很明显一款android安全类型的http客户端, 那么怎么样才算安全?支持https?支持本地线程安全?
发现Rertofit其内部都是支持lambda语法(国内称只链式语法),内部支持okhttp, 并且支持响应式RxJAava,当然jdk1.8 和android studio工具也支持lambda。带着这些疑问 我开始探究一下。

在此之前准备入手资料:

国外博客
https://inthecheesefactory.com/blog/retrofit-2.0/en

官方github
http://square.github.io/retrofit/

二 Retrofit怎么使用

下文之前先给大家看下传统的httpclient(urlConnection) + AsyncTask实现的登录功能,这样我们才能发现Retrofit的优雅之处.

传统方式:

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
/**
* Represents an asynchronous login/registration task used to authenticate
* the user.
*/
public class UserLoginTask extends AsyncTask<Void, Void, Boolean> {
private final String mEmail;
private final String mPassword;
UserLoginTask(String email, String password) {
mEmail = email;
mPassword = password;
}
@Override
protected Boolean doInBackground(Void... params) {
// TODO: attempt authentication against a network service.
try {
// Simulate network access.
String result = "";
BufferedReader in = null;
String path ="http://localhost:8080/login/?" +"email =" + mEmail + "& password =" + mPassword;
URL url =new URL(path);
HttpURLConnection conn = (HttpURLConnection)url.openConnection();
conn.setConnectTimeout(5 * 1000);
conn.setRequestMethod("GET");
InputStream inStream = conn.getInputStream();
in = new BufferedReader(
new InputStreamReader(conn.getInputStream()));
String line;
while ((line = in.readLine()) != null)
{
result += "\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n" + line;
}
}catch (MalformedURLException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (ProtocolException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
//在这里我们还要对返回的json数据进行 要主动映射到modle上
………
for (String credential : DUMMY_CREDENTIALS) {
String[] pieces = credential.split(":");
if (pieces[0].equals(mEmail)) {
// Account exists, return true if the password matches.
return pieces[1].equals(mPassword);
}
}
// TODO: register the new account here.
return true;
}
@Override
protected void onPostExecute(final Boolean success) {
mAuthTask = null;
if (success) {
// do SomeThing
} else {
mPasswordView.setError(getString(R.string.error_incorrect_password));
mPasswordView.requestFocus();
}
}
@Override
protected void onCancelled() {
mAuthTask = null;
showProgress(false);
}
}
private void enterhome() {
Intent intent = new Intent(LoginActivity.this, MainListActivity.class);
startActivity(intent);
}

发现原理也很简单,点击loginbtn开启一个异步线程 在AsyncTaskdoInBackground中访问登录API,在onPostExecute中进行UI更新;也能很简单流畅的解决UI线程请求网络 非UI线程更新UI的问题, 但是AsyncTask 处理大数据耗时就会有弊端,况且他默认线程也是5个,容易造成泄漏,接下来介绍用Retrofit实现以上相同的功能的方式

2 Retrofit

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
/**
* 登录!
*/
private void getLogin() {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://localhost:8080/")
.addConverterFactory(GsonConverterFactory.create())
.build();
ApiManager apiService = retrofit.create(ApiManager.class);
Call<LoginResult> call = apiService.getData("lyk", "1234");
call.enqueue(new Callback<LoginResult>() {
@Override
public void onResponse(Call<LoginResult> call, Response<LoginResult> response) {
if (response.isSuccess()) {
// do SomeThing
} else {
//直接操作UI 返回的respone被直接解析成你指定的modle
}
}
@Override
public void onFailure(Call<LoginResult> call, Throwable t) {
// do onFailure代码
}
});
}

ApiManager接口

1
2
3
4
5
6
7
8
/**
* Created by LIUYONGKUI on 2016-05-03.
*/
public interface ApiManager {
@GET("login/")
Call<LoginResult> getData(@Query("name") String name, @Query("password") String pw);

好了 看了以上代码 或许你已经看到了他的链式优雅高大上的地方了,也许看不懂,有点蒙逼,但没关系我们继续入门。

1 配置gradle

compile 'com.squareup.retrofit2:retrofit:2.0.0-beta4'
compile 'com.squareup.retrofit2:converter-gson:2.0.0-beta4'  

com.squareup.retrofit2:converter-gson:2.0.0-beta4 此依赖非必须,只是方便我对http返回的数据进行解析。

2 定义实例化

1》初始化Retrofit

1
2
3
4
5
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://localhost:8080/")
.addConverterFactory(GsonConverterFactory.create())
.build();

通过 Retrofit.Builder 来创建一个retrofit客户端,接着添加host url, 然后制定数据解析器,上面依赖的gson就是用在这里做默认数据返回的, 之后通过build()创建出来
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

2》编写API

1
2
@GET("login/")
Call<LoginResult> getData(@Query("name") String name, @Query("password") String pw);

Call<T>是继承Cloneable的 并支持泛型,且此类是Retrofit统一返回对象,支持Callback<T>回调,在2.0上已支持RxJava观察者对象Observable<T>,此案例暂时用call ,后面入门了retrofit之后再接入RxJava,接着我们可以传入制定的解析Modle,就会在主线程里返回对应的model数据,无需开发者手动解析json数据,返回格式由开发者自己设置,这里主要用注解@get @post 设置请求方式,后面“login/”是方法Url, @Query(“name”)来设定body的parameters.

  • 如果想用表单 @FieldMap
1
2
3
4
@FormUrlEncoded
@POST("/url")
Call<T> postForm(
@FieldMap Map<String , Object> maps);
  • 如果直接用对象 @Body
1
2
3
@POST("url")
Call<T> PostBody(
@Body Objects objects);
  • 如果直接多参数 @QueryMap
1
2
3
4
@PUT("/url")
Call<T> queryMap(
@QueryMap Map<String, String> maps);
  • 如果上传文件 @Part
1
2
3
4
5
6
@Multipart
@POST("/url")
Call<ResponseBody> uploadFlie(
@Part("description") RequestBody description,
@Part("files") MultipartBody.Part file);
  • 如果多文件上传 @PartMap()
1
2
3
4
5
6
@Multipart
@POST("{url}")
Call<T> uploadFiles(
@Path("url") String url,
@PartMap() Map<String, RequestBody> maps);

3》 调用API
Retrofit支持异步和同步,案例中用call.enqueue(new Callback<LoginResult>)来采用异步请求,如果 调用call.execute() 则采用同步方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Call<LoginResult> call = apiService.getData("lyk", "1234");
call.enqueue(new Callback<LoginResult>() {
@Override
public void onResponse(Call<LoginResult> call, Response<LoginResult> response) {
}
@Override
public void onFailure(Call<LoginResult> call, Throwable t) {
}
});
}

取消请求

直接用call实例进行cancel即可

call.cancel(); 

优雅的取消请看Retrofit 2.0 超能实践(九)Rxjava结合Retrofit,如何优雅的取消请求!

如果还未理解请阅读参考入门资料:Retrofit 2.0:有史以来最大的改进

三 进阶拓展

通过以上的介绍和案列,我们了解了怎样运用Retrofit请求网络数据,展现数据更新UI,用什么数据模型接收 Retroifit就会返回什么类型的数据,我们也不用关心是否在主线程里访问网络 还是子线程更新ui的问题,但实际开发中会存在很多问题,很多同学会遇到:Retrofit的内部Log都无法输出 , header怎么加入,请求怎么支持https,包括怎么结合RxJava.? 不用担心,这些Retrofit 2.0 都提供了支持okhttp的自定义的Interceptor(拦截器),通过不同的Interceptor可以实现不同的自定义请求形式,比如统一加head,参数,加入证书(ssl)等,前提必须结合okhttp来实现 , 通过给OkHttpClient添加Interceptor·,然后给Retrofit设置http客户端即可.Retrofit提供了
.client()方法供我们传入自定义的网络客户端,当然默认请求客户端就是okhttps.

OkHttp入门请移步:
~https://github.com/square/okhttp
~ OKHttp源码解析

1 开启Log

可以用拦截器自己实现, retrofit已经提供了HttpLoggingInterceptor 里面有四种级别,输出的格式 可以看下面介绍。

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public enum Level {
/** No logs. */
NONE,
/**
* Logs request and response lines.
*
* <p>Example:
* <pre>{@code
* --> POST /greeting http/1.1 (3-byte body)
*
* <-- 200 OK (22ms, 6-byte body)
* }</pre>
*/
BASIC,
/**
* Logs request and response lines and their respective headers.
*
* <p>Example:
* <pre>{@code
* --> POST /greeting http/1.1
* Host: example.com
* Content-Type: plain/text
* Content-Length: 3
* --> END POST
*
* <-- 200 OK (22ms)
* Content-Type: plain/text
* Content-Length: 6
* <-- END HTTP
* }</pre>
*/
HEADERS,
/**
* Logs request and response lines and their respective headers and bodies (if present).
*
* <p>Example:
* <pre>{@code
* --> POST /greeting http/1.1
* Host: example.com
* Content-Type: plain/text
* Content-Length: 3
*
* Hi?
* --> END GET
*
* <-- 200 OK (22ms)
* Content-Type: plain/text
* Content-Length: 6
*
* Hello!
* <-- END HTTP
* }</pre>
*/
BODY
}

开启请求头

1
2
3
4
5
Retrofit retrofit = new Retrofit.Builder().client(new OkHttpClient.Builder()
.addNetworkInterceptor(
new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.HEADERS))
.build())

开启body日志

1
2
.addNetworkInterceptor(
new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))

基础输出

1
2
.addNetworkInterceptor(
new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC))

2 增加头部信息

通用请求头

new Retrofit.Builder()
          .addConverterFactory(GsonConverterFactory.create())

          .client(new OkHttpClient.Builder()
                  .addInterceptor(new Interceptor() {
                      @Override
                      public Response intercept(Chain chain) throws IOException {
                          Request request = chain.request()
                                  .newBuilder()
                                  .addHeader("mac", "f8:00:ea:10:45")
                                  .addHeader("uuid", "gdeflatfgfg5454545e")
                                  .addHeader("userId", "Fea2405144")
                                  .addHeader("netWork", "wifi")
                                  .build();
                          return chain.proceed(request);
                      }
                  })

                  .build()

单独加入

@Headers({ "Accept: application/vnd.github.v3.full+json", "User-Agent: Retrofit-your-App"})
@get("users/{username}")
Call<User>   getUser(@Path("username") String username);

3 添加证书Pinning

证书可以在自定义的OkHttpClient加入certificatePinner 实现

OkHttpClient client = new OkHttpClient.Builder()
    .certificatePinner(new CertificatePinner.Builder()
            .add("YOU API.com", "sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=")
            .add("YOU API..com", "sha1/SXxoaOSEzPC6BgGmxAt/EAcsajw=")
            .add("YOU API..com", "sha1/blhOM3W9V/bVQhsWAcLYwPU6n24=")
            .add("YOU API..com", "sha1/T5x9IXmcrQ7YuQxXnxoCmeeQ84c=")
            .build())

4 支持https

加密和普通http客户端请求支持https一样,步骤如下:

1 CertificateFactory 得到Context.getSocketFactory
2 添加证书源文件
3 绑定到okhttpClient
4设置okhttpClient到retrofit中

证书同样可以设置到okhttpclient中,我们可以把证书放到raw路径下

SLSocketFactory sslSocketFactory =getSSLSocketFactory_Certificate(context,"BKS", R.raw.XXX);

准备证书源文件

加入证书源文件,我的证书是放在Raw下面的: 证书

绑定证书

protected static SSLSocketFactory getSSLSocketFactory(Context context, int[] certificates) {

if (context == null) {
    throw new NullPointerException("context == null");
}

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

    for (int i = 0; i < certificates.length; i++) {
        InputStream certificate = context.getResources().openRawResource(certificates[i]);
        keyStore.setCertificateEntry(String.valueOf(i), certificateFactory.generateCertificate(certificate));

        if (certificate != null) {
            certificate.close();
        }
    }
    SSLContext sslContext = SSLContext.getInstance("TLS");
    TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    trustManagerFactory.init(keyStore);
    sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());
   return sslContext.getSocketFactory();   

构建HostnameVerifier

 protected static HostnameVerifier getHostnameVerifier(final String[] hostUrls) {

    HostnameVerifier TRUSTED_VERIFIER = new HostnameVerifier() {

        public boolean verify(String hostname, SSLSession session) {
            boolean ret = false;
            for (String host : hostUrls) {
                if (host.equalsIgnoreCase(hostname)) {
                    ret = true;
                }
            }
            return ret;
        }
    };

return TRUSTED_VERIFIER;

}

设置setSocketFactory

okhttpBuilder.socketFactory(HttpsFactroy.getSSLSocketFactory(context, certificates));

certificates 是你raw下证书源ID, int[] certificates = {R.raw.myssl}

设置setNameVerifie

okhttpBuilder.hostnameVerifier(HttpsFactroy.getHostnameVerifier(hosts));

hosts是你的host数据 列如 String hosts[]`= {“https//:aaaa,com”, “https//:bbb.com”}

实现自定义 添加到Retrofit

okHttpClient = okhttpBuilder.build(); 
Retrofit retrofit = new Retrofit.Builder() .client(okHttpClient) .build();

信任所有

如果忽略所有https请求,
可以直接将OkHttpClient的HostnameVerifier设置为false

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
OkHttpClient client = new OkHttpClient();
client.setHostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String s, SSLSession sslSession) {
return true;
}
});
TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
@Override
public void checkClientTrusted(
java.security.cert.X509Certificate[] x509Certificates,
String s) throws java.security.cert.CertificateException {
}
@Override
public void checkServerTrusted(
java.security.cert.X509Certificate[] x509Certificates,
String s) throws java.security.cert.CertificateException {
}
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return new java.security.cert.X509Certificate[] {};
}
} };
try {
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, trustAllCerts, new java.security.SecureRandom());
client.setSslSocketFactory(sc.getSocketFactory());
} catch (Exception e) {
e.printStackTrace();
}
clent.protocols(Collections.singletonList(Protocol.HTTP_1_1))
.build();

#常规问题归总

1 url被转义

http://api.myapi.com/http%3A%2F%2Fapi.mysite.com%2Fuser%2Flist

请将@path改成@url

public interface APIService { 
 @GET Call<Users> getUsers(@Url String url);}

或者:

  public interface APIService {
    @GET("{fullUrl}")
    Call<Users> getUsers(@Path(value = "fullUrl", encoded = true) String fullUrl);
}

2Method方法找不到

java.lang.IllegalArgumentException: Method must not be null

请指定具体请求类型@get @post等

   public interface APIService { 

   @GET Call<Users> getUsers(@Url String url);
}

3Url编码不对,@fieldMap parameters must be use FormUrlEncoded

如果用fieldMap加上FormUrlEncoded编码

@POST()
@FormUrlEncoded
Observable<ResponseBody> executePost(
        @FieldMap Map<String, Object> maps);

上层需要转换将自己的map转换为FieldMap

@FieldMap(encoded = true) Map<String, Object> parameters,

4 paht和url一起使用

Using @Path and @Url paramers together with retrofit2

java.lang.IllegalArgumentException: @Path parameters may not be used with @Url. (parameter #4

如果你是这样的:

 @GET
Call<DataResponse> getOrder(@Url String url,
 @Path("id") int id);

请在你的url指定占位符.url:

www.myAPi.com/{Id}

总结

看了以上的知识点你发现Retrofit同样支持RxJava,通过以下设置Call适配模式.就可以完美关联RxJava。

retrofit .addCallAdapterFactory(RxJavaCallAdapterFactory.create())    

关于 Retrofit+ RxJava的案列, 结尾源码已经结合,以及实际遇到的坑可以看本人的系列文章(Retrofit+Rxjava使用技巧一文)。RxJava也是一款强大的多线程通讯利器,也支持本地线程安全,从以前编程习惯迁移到这种链式风格, 估计入门会让你头痛,但会让你在实际应用开发中无时无刻,随心所欲进行多线程响应式编程开发。一句话 :谁用谁知道!

Retrofit 2.0系列请阅读

-Novate网络库:Retrofit2.0和RxJava1.x的又一次完美封装


参考文章:


第一时间获资讯请关注微信公众号!

开发者技术前线