简介
Retrofit
是由Square
公司提供的开源产品,为Android
平台的应用提供一个类型安全的REST
客户端。其实质上是对OkHttp
的封装,使用面向接口的方式进行网络请求,利用动态生成的代理类封装了网络接口请求的底层,将REST API
返回的数据转化为Java
对象方便操作,可以进行GET
、POST
、PUT
、DELETE
等请求,极大的提高了应用的网络体验。
官方文档(英文)
更新日志
用Retrofit 2简化HTTP请求
给Android开发者的RxJava详解
Android文件存储使用参考
1、REST
REST(REpresentational State Transfer)
指的是一组架构约束条件和原则。
RESTful
架构都满足以下规则:
(1) 每一个URI
代表一种资源;
(2) 客户端和服务器之间,传递这种资源的某种表现层;
(3) 客户端通过四个HTTP
动词,对服务器端资源进行操作,实现”表现层状态转化”。
2、2.0与1.9使用比较
(1) 创建实例:Retrofit 1.9
中使用的是RestAdapter
,而Retrofit 2.0
中使用的是Retrofit
;
(2) 加载URL:Retrofit 1.9
中使用的是setEndpoint
,而Retrofit 2.0
中使用的是baseUrl
;
(3) 拦截器:Retrofit 1.9
中使用setRequestInterceptor
方法设置拦截器对Http
请求进行相应处理,而Retrofit 2.0
中通过OKHttp
的拦截器拦截Http
请求进行监控,可以用于重写、重试、日志打印等;
(4) 转换器:Retrofit 1.9
中使用的是setConverter
,而Retrofit 2.0
中使用的是addConverterFactory
用于支持Gson
转换。
3、Retrofit 1.9体验不好的地方
(1) 不能同时操作response
返回数据(比如返回的Header
部分或者URL
)和序列化后的数据(JavaBean
);
(2) 同步和异步方式执行同一个方法需要分别定义接口;
(3) 对正在进行的网络任务无法取消。
常用配置
1、依赖配置
compile 'com.squareup.retrofit2:retrofit:2.3.0'
compile 'com.squareup.retrofit2:converter-gson:2.0.2'
compile 'com.squareup.retrofit2:adapter-rxjava:2.0.2'
compile 'com.squareup.okhttp3:logging-interceptor:3.2.0'
2、网络权限
<uses-permission android:name="android.permission.INTERNET" />
3、 混淆配置
-dontwarn retrofit2.**
-keep class retrofit2.** { *; }
-keepattributes Signature
-keepattributes Exceptions
4、解析方式
Retrofit 2
支持多种解析方式来解析响应数据,有以下解析库可以选择:
Gson: com.squareup.retrofit:converter-gson:2.0.0-beta2
Jackson: com.squareup.retrofit:converter-jackson:2.0.0-beta1
Moshi: com.squareup.retrofit:converter-moshi:2.0.0-beta1
Protobuf: com.squareup.retrofit:converter-protobuf:2.0.0-beta1
Wire: com.squareup.retrofit:converter-wire:2.0.0-beta1
Simple XML: com.squareup.retrofit:converter-simplexml:2.0.0-beta1
基本用法
//定以接口
public interface GitHubApi {
@GET("repos/{owner}/{repo}/contributors")
Call<ResponseBody> contributorsBySimpleGetCall(@Path("owner") String owner, @Path("repo") String repo);
}
//获取实例
Retrofit retrofit = new Retrofit.Builder()
//设置OkHttpClient,如果不设置会提供一个默认的
.client(new OkHttpClient())
//设置baseUrl
.baseUrl("https://api.github.com/")
//添加Gson转换器
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
GitHubApi repo = retrofit.create(GitHubApi.class);
//请求完整地址:https://api.github.com/repos/square/retrofit/contributors
//同步请求
Call<ResponseBody> call = repo.contributorsBySimpleGetCall(mUserName, mRepo);
try {
Response<ResponseBody> repos = call.execute();
} catch (IOException e) {
e.printStackTrace();
}
//Call只能调用一次,否则会抛IllegalStateException异常
Call<ResponseBody> clone = call.clone();
//异步请求
clone.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
try {
// Get result bean from response.body().string()
String repos = response.body().string();
} catch (IOException e) {
e.printStackTrace();
}
// Get header item from response
String links = response.headers().get("Link");
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
}
});
//取消请求
call.cancel();
使用方式
1、创建Retrofit实例
如果要向一个API
发送我们的网络请求,我们需要使用Retrofit.Builder()
并指定Service
的baseUrl
(通常情况下指的是域名)。还需注意我们要指定一个Factory
来对响应进行反序列化,可以添加多种序列化Factory
,其被添加的顺序将是它们被Retrofit
尝试解析的顺序。但是GsonConverterFactory
必须放在最后,否则会抛出异常。如果我们希望传入一个自定义的Gson
解析实例,也是可以自定义的。一般地baseUrl
是在实例化Retrofit
的时候定义的,我们也可以在API
接口中定义完整的Url
。
注意:Retrofit 2.0
后建议baseUrl
中要以“/”结尾,在API中不要以“/”开头和结尾。
Gson gson = new GsonBuilder()
.setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
.create();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
2、创建API接口
在Retrofit 2
中通过一个Java
接口作为HTTP
请求的API
接口,使用特殊的Retrofit
注解来映射参数以及请求方法这些细节。另外返回值始终是一个参数化了的Call<T>
对象,比如Call<User>
。如果你不需要任何类型安全的响应,你可以把返回值指定为Call<ResponseBody>
。
public interface GitHubApi {
@GET("repos/{owner}/{repo}/contributors")
Call<ResponseBody> contributorsBySimpleGetCall(@Path("owner") String owner, @Path("repo") String repo);
@POST("/users/new")
Call<User> createUser(@Body User user);
}
每个API
接口都指定了一个关于HTTP
(GET
、POST
等)方法的注解以及用于分发网络调用的方法。
3、调用API接口
GitHubApi repo = retrofit.create(GitHubApi.class);
Call<ResponseBody> call = repo.contributorsBySimpleGetCall(mUserName, mRepo);
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
try {
Gson gson = new Gson();
ArrayList<Contributor> contributors = gson.fromJson(response.body().string(),
new TypeToken<List<Contributor>>() {
}.getType());
LogUtil.e("contributors-->" + contributors.toString());
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
LogUtil.e("onFailure");
}
});
完整Url
地址为:https://api.github.com/repos/square/retrofit/contributors
4、取消请求
call.cancel();
我们可以终止一个请求。终止操作是对底层的HttpClient
执行cancel
操作,即使是正在执行的请求,也能够立即终止。
5、clone
无论是同步操作还是异步操作每一个call
对象实例只能被执行一次,多次执行会抛出IllegalStateException
异常。通过clone
方法可以创建一个一模一样的实例,并且它的开销也是很小的。
Call<List<Contributor>> cloneCall = call.clone();
转换器
在上面的例子中通过获取ResponseBody
自己使用Gson
来解析接收到的Json
格式数据。在Retrofit
中当创建一个Retrofit
实例的时候可以为其添加一个Json
转换器,这样就会自动将Json
格式的响应体转换为所需要的Java
对象。下面看一下如何根据已有的Json
格式数据生成Java
对象,我们可以根据已知的数据手动创建Java
对象,也可以通过工具或插件将Json
格式的数据为我们自动生成Java
对象。
自动生成Java
对象
在这里介绍两种根据已有Json
数据自动生成Java
对象的工具。
1、jsonschema2pojo
可以通过访问jsonschema2pojo网站来自动生成,先来看一下它的使用方法。
上面配置中所选注解也可以选择Gson
,对于@Generated
注解若是需要保留的话可以添加以下依赖方式,也可以直接删除@Generated
注解,没有任何影响。
compile 'org.glassfish:javax.annotation:10.0-b28'
2、GsonFormat
GsonFormat
是Android Studio
中的一个插件,在Android Studio
的插件选项中直接搜索安装这个插件即可。在这里我们看一下是如何使用这个插件的。
3、添加转换器
在这里我们需要为Retrofit
添加Gson
转换器的依赖,如果已经添加过converter-gson
那么就不用再添加Gson
库,因为在converter-gson
中已经包含了Gson
。
compile 'com.squareup.retrofit2:converter-gson:2.0.2'
在这里先创建一个Java
类Contributor
来保存接收到的数据。
public class Contributor {
private String login;
private Integer contributions;
public String getLogin() {
return login;
}
public void setLogin(String login) {
this.login = login;
}
public Integer getContributions() {
return contributions;
}
public void setContributions(Integer contributions) {
this.contributions = contributions;
}
@Override
public String toString() {
return "Contributor{" +
"login='" + login + '\'' +
", contributions=" + contributions +
'}';
}
}
然后修改API
接口。
public interface GitHubApi {
@GET("repos/{owner}/{repo}/contributors")
Call<List<Contributor>> contributorsBySimpleGetCall(@Path("owner") String owner, @Path("repo") String repo);
}
创建Retrofit
实例,通过addConverterFactory
指定一个Factory
来对响应进行反序列化,在这里converters
被添加的顺序将是它们被Retrofit
尝试解析的顺序。
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
调用上面所修改的API
接口。
GitHubApi repo = retrofit.create(GitHubApi.class);
Call<List<Contributor>> call = repo.contributorsByAddConverterGetCall(mUserName, mRepo);
call.enqueue(new Callback<List<Contributor>>() {
@Override
public void onResponse(Call<List<Contributor>> call, Response<List<Contributor>> response) {
List<Contributor> contributors = response.body();
LogUtil.e("contributors-->" + contributors.toString());
}
@Override
public void onFailure(Call<List<Contributor>> call, Throwable t) {
LogUtil.e("onFailure");
}
});
在这里我们也可以通过call.execute()
执行一个同步请求,由于不允许在主线程中进行网络请求操作,所以我们需要在子线程中进行执行。
GitHubApi repo = retrofit.create(GitHubApi.class);
final Call<List<Contributor>> call = repo.contributorsByAddConverterGetCall(mUserName, mRepo);
new Thread(new Runnable() {
@Override
public void run() {
try {
Response<List<Contributor>> response = call.execute();
List<Contributor> contributors = response.body();
LogUtil.e("contributors-->" + contributors.toString());
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
添加日志信息
在Retrofit 2.0
中是没有日志功能的,但是Retrofit 2.0
依赖OkHttp
,所以也就能够通过OkHttp
中的addInterceptor
来实现实际的底层请求和响应日志。
HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor();
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(httpLoggingInterceptor)
.build();
Retrofit retrofit = new Retrofit.Builder()
.client(okHttpClient)
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
需要添加如下依赖:
compile 'com.squareup.okhttp3:logging-interceptor:3.1.2'
参数注解
@HTTP:可以代替其他方法的任意一种。
/**
* method 表示请求方法,不区分大小写
* path 表示路径
* hasBody 表示是否有请求体
*/
@HTTP(method = "get", path = "user/{userName}", hasBody = false)
Call<User> getUser(@Path("userName") String userName);
@Header:添加请求头,不能被互相覆盖,用于修饰参数。
//动态设置Header值
@GET("user")
Call<User> getUser(@Header("Authorization") String authorization);
等同于:
//静态设置Header值
@Headers("Authorization: authorization")//这里的authorization相当于上面方法中传进来的变量值
@GET("user")
Call<User> getUser();
@Headers:设置多个Header
值,用于修饰方法。
@Headers({
"Accept: application/vnd.github.v3.full+json",
"User-Agent: Retrofit-Sample-App"
})
@GET("user")
Call<User> getUser();
@Url:使用全路径复写baseUrl
,适用于非统一baseUrl
的场景。
@GET
Call<User> getUser(@Url String url);
@Path:URL
占位符,用于替换和动态更新,相应的参数必须使用相同的字符串被@Path
进行注释。
@GET("user/{userName}")
Call<User> getUser(@Path("userName") String userName);
//--> http://baseUrl/user/userName
等同于:
@GET
Call<User> getUser(@Url String url);
@Body:用于POST
请求体,将实例对象根据转换方式转换为对应的json
字符串参数,这个转化方式是GsonConverterFactory
定义的。
@POST("add")
Call<List<User>> addUser(@Body User user);
这个参数对象会被Retrofit
实例中的Converter
进行转化,如果没有给Retrofit
实例添加任何Converter
的话则只有ResponseBody
可以作为参数使用。
@Query:为URL
指定查询参数。
@GET("user")
Call<User> getUser(@Query("userName") String userName);
//--> http://baseUrl/user?userName=userName
传数组:
@GET("v1/enterprise/find")
Call<ResponseBody> getData(@Query("id") String id, @Query("linked[]") String... linked);
String id = "retrofit";
String[] str = new String[]{"retrofit"};
GitHubApi repo = retrofit.create(GitHubApi.class);
Call<ResponseBody> call = repo.getData(id, str);
@QueryMap:当参数过多的时候使用它来指定每个表单项的key
和value
值。
@GET("user")
Call<User> getUser(@QueryMap(encoded = true) Map<String, String> map);
可以约定是否需要encode
。
@Field:使用@Field
注解和参数来指定每个表单项的key
、value
为参数的值,以表单的方式传递简单的键值对;使用@FormUrlEncoded
注解来发送表单数据,表示表单提交Content-Type:application/x-www-form-urlencoded
。
@FormUrlEncoded
@POST("user/edit")
Call<User> updateUser(@Field("first_name") String first, @Field("last_name") String last);
@FieldMap:当我们有很多个表单参数时可以通过@FieldMap
注解和Map
对象参数来指定每个表单项的key
、value
值。
@FormUrlEncoded
@POST("user/edit")
Call<User> updateUser(@FieldMap Map<String, String> fieldMap);
@Part:用于单文件上传,以Post
表单的方式上传文件可以携带参数,其中@Part MultipartBody.Part
代表文件,@Part("key") RequestBody
代表参数;需要添加@Multipart
表示支持文件上传的表单Content-Type: multipart/form-data
。
@Multipart
@POST("upload")
Call<ResponseBody> upload(@Part("description") RequestBody description, @Part MultipartBody.Part file);
File file = new File(Environment.getExternalStorageDirectory(), "image.png");
// create RequestBody instance from file
RequestBody requestFile = RequestBody.create(MediaType.parse("multipart/form-data"), file);
// MultipartBody.Part is used to send also the actual file name
MultipartBody.Part body = MultipartBody.Part.createFormData("picture", file.getName(), requestFile);
// add another part within the multipart request
String descriptionString = "hello, this is description speaking";
RequestBody description = RequestBody.create(MediaType.parse("multipart/form-data"), descriptionString);
GitHubApi repo = retrofit.create(GitHubApi.class);
// finally, execute the request
Call<ResponseBody> call = repo.upload(description, body);
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
LogUtil.e("success");
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
LogUtil.e("error:" + t.getMessage());
}
});
@PartMap:用于多文件上传。
@Multipart
@POST("upload")
Call<ResponseBody> upload(@PartMap Map<String, RequestBody> params);
File file = new File(Environment.getExternalStorageDirectory(), "image.png");
RequestBody requestFile = RequestBody.create(MediaType.parse("multipart/form-data"), file);
Map<String, RequestBody> params = new HashMap<>();
params.put("picture\"; filename=\"" + file.getName() + "", requestFile);
@Streaming:用于下载大文件。
@Streaming
@GET
Call<ResponseBody> downLoadFile(@Url String fileUrl);
与RxJava结合
添加如下依赖:
compile 'com.squareup.retrofit2:adapter-rxjava:2.0.1'
compile 'io.reactivex:rxandroid:1.1.0'
创建Retrofit
对象实例时,通过addCallAdapterFactory
来添加对RxJava
的支持。
Retrofit retrofit = new Retrofit.Builder()
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.baseUrl("https://api.github.com/")
.build();
使用Observable
创建一个API
接口。
@GET("repos/{owner}/{repo}/contributors")
Observable<List<Contributor>> contributorsByRxJava(@Path("owner") String owner, @Path("repo") String repo);
下面来调用这个API
接口。
private CompositeSubscription mSubscriptions = new CompositeSubscription();
mSubscriptions.add(
mGitHubService.contributorsByRxJava(mUserName, mRepo)
//设置事件触发在非主线程
.subscribeOn(Schedulers.io())
//设置事件接受在UI线程以达到UI显示的目的
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<List<Contributor>>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
}
@Override
public void onNext(List<Contributor> contributors) {
LogUtil.e("contributors-->" + contributors.toString());
}
}));
如果我们想要查看所有Contributor
的信息,首先我们需要向GitHub
请求获取到所有Contributor
,然后再通过获得的Contributor
进行依次向GitHub
请求获取Contributor
的信息,在这时候我们使用RxJava
也就非常方便了。
mSubscriptions.add(mGitHubService.contributorsByRxJava(mUserName, mRepo)
.flatMap(new Func1<List<Contributor>, Observable<Contributor>>() {
@Override
public Observable<Contributor> call(List<Contributor> contributors) {
return Observable.from(contributors);
}
})
.flatMap(new Func1<Contributor, Observable<Pair<User, Contributor>>>() {
@Override
public Observable<Pair<User, Contributor>> call(Contributor contributor) {
Observable<User> userObservable = mGitHubService.userByRxJava(contributor.getLogin())
.filter(new Func1<User, Boolean>() {
@Override
public Boolean call(User user) {
return !isEmpty(user.getName()) && !isEmpty(user.getEmail());
}
});
return Observable.zip(userObservable,
Observable.just(contributor),
new Func2<User, Contributor, Pair<User, Contributor>>() {
@Override
public Pair<User, Contributor> call(User user, Contributor contributor) {
return new Pair<>(user, contributor);
}
});
}
})
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<Pair<User, Contributor>>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
}
@Override
public void onNext(Pair<User, Contributor> pair) {
User user = pair.first;
Contributor contributor = pair.second;
LogUtil.e("name:" + user.getName());
LogUtil.e("contributions:" + contributor.getContributions());
LogUtil.e("email:" + user.getEmail());
}
}));
设置缓存
1、由于Retrofit
是对OkHttp
的封装,所以可以直接为OkHttp
设置缓存,以下我们设置了离线读取本地缓存,在线获取最新数据。
OkHttpClient builder = new OkHttpClient.Builder()
//设置Cache目录
.cache(cache())
//设置缓存
.addInterceptor(cacheInterceptor)
.addNetworkInterceptor(cacheInterceptor)
//设置超时
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
//错误重连
.retryOnConnectionFailure(true)
.build();
Retrofit retrofit = new Retrofit.Builder()
//设置baseUrl
.baseUrl(Constant.baseUrl)
//设置OkHttpClient,如果不设置会提供一个默认的
.client(builder)
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
//添加Gson转换器
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
private static Cache cache() {
//设置缓存路径
File cacheDir = new File(MyApplication.getContext().getExternalCacheDir(), "HttpResponseCache");
//设置缓存大小为10M
return new Cache(cacheDir, 10 * 1024 * 1024);
}
private static Interceptor cacheInterceptor = new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
//在每个请求发出前,判断一下网络状况,如果没问题继续访问,如果有问题,则设置从本地缓存中读取
if (!NetworkUtils.isNetworkAvailable()) {
LogUtil.i("no network");
request = request.newBuilder()
//强制使用缓存
.cacheControl(CacheControl.FORCE_CACHE)
.build();
}
Response response = chain.proceed(request);
//先判断网络,网络好的时候,移除header后添加cache失效时间为0小时,网络未连接的情况下设置缓存时间为4周
if (NetworkUtils.isNetworkAvailable()) {
LogUtil.i("has network");
// 有网络时 设置缓存超时时间0个小时
int maxAge = 0;// 在线缓存0个小时
response.newBuilder()
.removeHeader("Pragma")// 清除头信息,因为服务器如果不支持,会返回一些干扰信息,不清除下面无法生效
.removeHeader("Cache-Control")
.header("Cache-Control", "public, max-age=" + maxAge)
.build();
} else {
LogUtil.i("network error");
// 无网络时,设置超时为4周
int maxStale = 60 * 60 * 24 * 4 * 7;// 离线缓存4周
response.newBuilder()
.removeHeader("Pragma")
.removeHeader("Cache-Control")
.header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)
.build();
}
return response;
}
};
2、有网和没网都先读缓存,统一缓存策略,降低服务器压力。
private static Interceptor cacheInterceptor = new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
String cacheControl = request.cacheControl().toString();
if (TextUtils.isEmpty(cacheControl)) {
cacheControl = "public, max-age=60";
}
return response.newBuilder()
.header("Cache-Control", cacheControl)
.removeHeader("Pragma")
.build();
}
};
3、配置单个请求的@Headers
,设置此请求的缓存策略,不影响其他请求的缓存策略,不设置则没有缓存。
//设置单个请求的缓存时间
@Headers("Cache-Control: max-age=640000")
@GET("user")
Call<User> getUser();
使用证书锁定
Retrofit
中的证书锁定是借助OkHttpClient
实现的,通过为OkHttpClient
添加certificatePinner
即可。CertificatePinner
对象以构建器的方式创建,可以通过其add()
方法来锁定多个证书。
OkHttpClient client = new OkHttpClient.Builder()
.certificatePinner(new CertificatePinner.Builder()
.add("YOU API..com", "sha1/blhOM3W9V/bVQhsWAcLYwPU6n24=")
.add("YOU API..com", "sha1/T5x9IXmcrQ7YuQxXnxoCmeeQ84c=")
.build())
.build();
自签名证书
由于我们使用的是自签名的证书,因此客户端不信任服务器,会抛出异常javax.NET.ssl.SSLHandshakeException
。为此,我们需要自定义信任处理器(TrustManager
)来替代系统默认的信任处理器,这样我们才能正常的使用自定义的证书或者非Android
认可的证书颁发机构颁发的证书。
针对使用场景又分为以下两种情况:一种是安全性要求不高的情况下,客户端无需内置证书;另外一种则是客户端内置证书。
客户端不内置证书
public static SSLSocketFactory getSSLSocketFactory() throws Exception {
// Create a trust manager that does not validate certificate chains
final TrustManager[] trustAllCerts = new TrustManager[]{
new X509TrustManager() {
//证书中的公钥
public static final String PUB_KEY = "------";
@Override
public void checkClientTrusted(
java.security.cert.X509Certificate[] chain,
String authType) throws CertificateException {
}
//客户端并为对ssl证书的有效性进行校验
@Override
public void checkServerTrusted(
java.security.cert.X509Certificate[] chain,
String authType) throws CertificateException {
if (chain == null) {
throw new IllegalArgumentException("checkServerTrusted:x509Certificate array isnull");
}
if (!(chain.length > 0)) {
throw new IllegalArgumentException("checkServerTrusted: X509Certificate is empty");
}
if (!(null != authType && authType.equalsIgnoreCase("RSA"))) {
throw new CertificateException("checkServerTrusted: AuthType is not RSA");
}
// Perform customary SSL/TLS checks
try {
TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
tmf.init((KeyStore) null);
for (TrustManager trustManager : tmf.getTrustManagers()) {
((X509TrustManager) trustManager).checkServerTrusted(chain, authType);
}
} catch (Exception e) {
throw new CertificateException(e);
}
// Hack ahead: BigInteger and toString(). We know a DER encoded Public Key begins
// with 0×30 (ASN.1 SEQUENCE and CONSTRUCTED), so there is no leading 0×00 to drop.
RSAPublicKey pubkey = (RSAPublicKey) chain[0].getPublicKey();
String encoded = new BigInteger(1 /* positive */, pubkey.getEncoded()).toString(16);
// Pin it!
final boolean expected = PUB_KEY.equalsIgnoreCase(encoded);
if (!expected) {
throw new CertificateException("checkServerTrusted: Expected public key: "
+ PUB_KEY + ", got public key:" + encoded);
}
}
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return new java.security.cert.X509Certificate[0];
}
}};
// Install the all-trusting trust manager
final SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
// Create an ssl socket factory with our all-trusting manager
return sslContext.getSocketFactory();
}
其中PUB_KEY
是我们证书中的公钥,你可以自行从自己的证书中提取。我们看到,在checkServerTrusted()
方法中,我们通过证书的公钥信息来确认证书的真伪,如果验证失败,则中断请求。
客户端内置证书
在Retrofit
中使用自签名证书大致要经过以下几步:
- 将证书添加到工程中;
- 自定义信任管理器
TrustManager
; - 用自定义
TrustManager
代替系统默认的信任管理器;
1、添加证书到工程
比如现在我们有个证书myssl.cer
,首先需要将其放在res/raw
目录下,当然你也可以放在assets
目录下。
2、自定义TrustManager
public static SSLSocketFactory getSSLSocketFactory(Context context, int[] certificates) {
if (context == null) {
throw new NullPointerException("context == null");
}
//CertificateFactory用来证书生成
CertificateFactory certificateFactory;
try {
certificateFactory = CertificateFactory.getInstance("X.509");
//Create a KeyStore containing our trusted CAs
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
for (int i = 0; i < certificates.length; i++) {
//读取本地证书
InputStream is = context.getResources().openRawResource(certificates[i]);
keyStore.setCertificateEntry(String.valueOf(i), certificateFactory.generateCertificate(is));
if (is != null) {
is.close();
}
}
//Create a TrustManager that trusts the CAs in our keyStore
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
//Create an SSLContext that uses our TrustManager
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());
return sslContext.getSocketFactory();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
3、用自定义TrustManager
代替系统默认的信任管理器
int[] certificates = new int[]{R.raw.myssl};
OkHttpClient build = new OkHttpClient.Builder()
.socketFactory(HttpsFactroy.getSSLSocketFactory(context, certificates))
.build();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.client(build)
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
这样我们的客户端就可以使用自签名的证书了。
公共参数
Interceptor addQueryParameterInterceptor = new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request originalRequest = chain.request();
Request request;
String method = originalRequest.method();
Headers headers = originalRequest.headers();
HttpUrl modifiedUrl = originalRequest.url().newBuilder()
// Provide your custom parameter here
.addQueryParameter("platform", "android")
.addQueryParameter("version", "1.0.0")
.build();
request = originalRequest.newBuilder().url(modifiedUrl).build();
return chain.proceed(request);
}
};
OkHttpClient builder = new OkHttpClient.Builder()
.addInterceptor(addQueryParameterInterceptor)
.build();
Retrofit retrofit = new Retrofit.Builder()
.client(builder)
.addConverterFactory(GsonConverterFactory.create())
.build();
设置通用头
Interceptor headerInterceptor = new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request originalRequest = chain.request();
Request.Builder requestBuilder = originalRequest.newBuilder()
.header("AppType", "TPOS")
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.method(originalRequest.method(), originalRequest.body());
Request request = requestBuilder.build();
return chain.proceed(request);
}
};
OkHttpClient builder = new OkHttpClient.Builder()
.addInterceptor(headerInterceptor)
.build();
Retrofit retrofit = new Retrofit.Builder()
.client(builder)
.addConverterFactory(GsonConverterFactory.create())
.build();
设置Cookie
服务端可能需要保持请求是同一个Cookie
。
1、添加依赖
compile 'com.squareup.okhttp3:okhttp-urlconnection:3.2.0'
2、设置Cookie
CookieManager cookieManager = new CookieManager();
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL);
OkHttpClient builder = new OkHttpClient.Builder()
.cookieJar(new JavaNetCookieJar(cookieManager))
.build();
Retrofit retrofit = new Retrofit.Builder()
.client(builder)
.addConverterFactory(GsonConverterFactory.create())
.build();
文件上传下载进度显示
在Retrofit
中我们可以通过ResponseBody
对文件进行下载。但是在Retrofit
中并没有为我们提供显示下载进度的接口。在项目开发中,如果用户下载一个文件,无法实时给用户显示下载进度,那么这样的用户体验是非常差的。下面我们简单介绍一下在Retrofit
中是如何对文件的上传下载进度进行实时的更新显示。
首先定义一个用于监听上传下载进度的接口,其中包含上传或下载进度、文件总大小以及是否操作完成。
public interface ProgressListener {
/**
* @param progress 已经下载或上传字节数
* @param total 总字节数
* @param done 是否完成
*/
void onProgress(long progress, long total, boolean done);
}
对于文件的下载我们需要重写ResponseBody
类中的一些方法,用于下载进度监听。
public class ProgressResponseBody extends ResponseBody {
//实际的待包装响应体
private final ResponseBody responseBody;
//进度回调接口
private final ProgressListener progressListener;
//包装完成的BufferedSource
private BufferedSource bufferedSource;
/**
* 构造函数赋值
*
* @param responseBody 待包装的响应体
* @param progressListener 回调接口
*/
public ProgressResponseBody(ResponseBody responseBody, ProgressListener progressListener) {
this.responseBody = responseBody;
this.progressListener = progressListener;
}
/**
* 重写实际响应体的contentType
*
* @return MediaType
*/
@Override
public MediaType contentType() {
return responseBody.contentType();
}
/**
* 重写实际响应体的contentLength
*
* @return contentLength
* @throws IOException 异常
*/
@Override
public long contentLength() {
return responseBody.contentLength();
}
/**
* 重写包装source
*
* @return BufferedSource
*/
@Override
public BufferedSource source() {
if (bufferedSource == null) {
//包装
bufferedSource = Okio.buffer(source(responseBody.source()));
}
return bufferedSource;
}
/**
* 读取操作回调进度接口
*
* @param source Source
* @return Source
*/
private Source source(Source source) {
return new ForwardingSource(source) {
//当前读取字节数
long totalBytesRead = 0L;
@Override
public long read(Buffer sink, long byteCount) throws IOException {
long bytesRead = super.read(sink, byteCount);
//增加当前读取的字节数,如果读取完成了bytesRead会返回-1
totalBytesRead += bytesRead != -1 ? bytesRead : 0;
//回调:如果contentLength()不知道长度则返回-1
progressListener.onProgress(totalBytesRead, responseBody.contentLength(), bytesRead == -1);
return bytesRead;
}
};
}
}
对于文件的上传我们需要重写RequestBody
类中的一些方法,用于上传进度监听。
public class ProgressRequestBody extends RequestBody {
//实际的待包装请求体
private final RequestBody requestBody;
//进度回调接口
private final ProgressListener progressListener;
//包装完成的BufferedSink
private BufferedSink bufferedSink;
/**
* 构造函数赋值
*
* @param requestBody 待包装的请求体
* @param progressListener 回调接口
*/
public ProgressRequestBody(RequestBody requestBody, ProgressListener progressListener) {
this.requestBody = requestBody;
this.progressListener = progressListener;
}
/**
* 重写实际响应体的contentType
*
* @return MediaType
*/
@Override
public MediaType contentType() {
return requestBody.contentType();
}
/**
* 重写实际响应体的contentLength
*
* @return contentLength
* @throws IOException 异常
*/
@Override
public long contentLength() throws IOException {
return requestBody.contentLength();
}
/**
* 重写写入操作
*
* @param sink BufferedSink
* @throws IOException 异常
*/
@Override
public void writeTo(BufferedSink sink) throws IOException {
if (bufferedSink == null) {
//包装
bufferedSink = Okio.buffer(sink(sink));
}
//写入
requestBody.writeTo(bufferedSink);
//必须调用flush,否则最后一部分数据可能不会被写入
bufferedSink.flush();
}
/**
* 写入操作回调进度接口
*
* @param sink Sink
* @return Sink
*/
private Sink sink(Sink sink) {
return new ForwardingSink(sink) {
//当前写入字节数
long bytesWritten = 0L;
//总字节长度,避免多次调用contentLength()方法
long contentLength = 0L;
@Override
public void write(Buffer source, long byteCount) throws IOException {
super.write(source, byteCount);
if (contentLength == 0) {
//获得contentLength的值,后续不再调用
contentLength = contentLength();
}
//增加当前写入的字节数
bytesWritten += byteCount;
//回调
progressListener.onProgress(bytesWritten, contentLength, bytesWritten == contentLength);
}
};
}
}
上面类中计算已经读取文件的字节数,并且调用了ProgressListener
接口,因此这个接口是在子线程中运行的。下面我们创建ProgressHelper
帮助类,用于对OkHttpClient
添加拦截事件,将RequestBody
和ResponseBody
替换成我们自己实现的ProgressRequestBody
和ProgressResponseBody
。
public class ProgressHelper {
private static ProgressBean progressBean = new ProgressBean();
private static ProgressHandler mProgressHandler;
public static void setProgressHandler(ProgressHandler progressHandler) {
mProgressHandler = progressHandler;
}
/**
* 包装OkHttpClient,用于下载文件的回调
*
* @return 包装后的OkHttpClient
*/
public static OkHttpClient addProgressDownLoadBuilder(OkHttpClient.Builder builder) {
if (builder == null) {
builder = new OkHttpClient.Builder();
}
//进度回调接口
final ProgressListener progressListener = new ProgressListener() {
// 该方法在子线程中运行
@Override
public void onProgress(long progress, long total, boolean done) {
LogUtil.e("progress: " + String.format("%d%% \n", (100 * progress) / total));
if (mProgressHandler == null) {
return;
}
progressBean.setBytesRead(progress);
progressBean.setContentLength(total);
progressBean.setDone(done);
mProgressHandler.sendMessage(progressBean);
}
};
//增加拦截器
builder.addInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
//拦截
Response originalResponse = chain.proceed(chain.request());
//包装响应体并返回
return originalResponse.newBuilder()
.body(new ProgressResponseBody(originalResponse.body(), progressListener))
.build();
}
});
return builder.build();
}
/**
* 包装OkHttpClient,用于上传文件的回调
*
* @return 包装后的OkHttpClient
*/
public static OkHttpClient addProgressUpLoadBuilder(OkHttpClient.Builder builder) {
if (builder == null) {
builder = new OkHttpClient.Builder();
}
//进度回调接口
final ProgressListener progressListener = new ProgressListener() {
// 该方法在子线程中运行
@Override
public void onProgress(long progress, long total, boolean done) {
LogUtil.e("progress: " + String.format("%d%% \n", (100 * progress) / total));
if (mProgressHandler == null) {
return;
}
progressBean.setBytesRead(progress);
progressBean.setContentLength(total);
progressBean.setDone(done);
mProgressHandler.sendMessage(progressBean);
}
};
//增加拦截器
builder.addInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request original = chain.request();
Request request = original.newBuilder()
.method(original.method(), new ProgressRequestBody(original.body(), progressListener))
.build();
return chain.proceed(request);
}
});
return builder.build();
}
}
通过实现ProgressListener
接口来获取下载进度,但是由于ProgressListener
接口运行在子线程中,无法在其中进行UI
操作,因此需要通过Handler
将子线程中ProgressListener
的数据发送到UI线程中进行处理。
创建一个用于存放ProgressListener
中参数的对象。
public class ProgressBean {
private long bytesRead;
private long contentLength;
private boolean done;
public long getBytesRead() {
return bytesRead;
}
public void setBytesRead(long bytesRead) {
this.bytesRead = bytesRead;
}
public long getContentLength() {
return contentLength;
}
public void setContentLength(long contentLength) {
this.contentLength = contentLength;
}
public boolean isDone() {
return done;
}
public void setDone(boolean done) {
this.done = done;
}
@Override
public String toString() {
return "ProgressBean{" +
"bytesRead=" + bytesRead +
", contentLength=" + contentLength +
", done=" + done +
'}';
}
}
然后在创建一个ProgressHandler
类。
public abstract class ProgressHandler {
protected abstract void sendMessage(ProgressBean progressBean);
protected abstract void handleMessage(Message message);
protected abstract void onProgress(long progress, long total, boolean done);
protected static class ResponseHandler extends Handler {
private ProgressHandler mProgressHandler;
public ResponseHandler(ProgressHandler mProgressHandler, Looper looper) {
super(looper);
this.mProgressHandler = mProgressHandler;
}
@Override
public void handleMessage(Message msg) {
mProgressHandler.handleMessage(msg);
}
}
}
上面的ProgressHandler
是一个抽象类,通过Handler
对象进行发送和处理消息。于是定义了两个抽象方法sendMessage
和handleMessage
,之后又定义了一个抽象方法onProgress
来处理下载进度的显示,而这个onProgress
则是需要我们在UI
线程内进行调用。最后创建了一个继承自Handler
的ResponseHandler
内部类,为了避免内存泄露我们使用static
关键字。
下面来创建一个DownloadProgressHandler
类,继承于ProgressHandler
,用来发送和处理下载消息。
public abstract class DownloadProgressHandler extends ProgressHandler {
private static final int DOWNLOAD_PROGRESS = 1;
protected ResponseHandler mHandler = new ResponseHandler(this, Looper.getMainLooper());
@Override
protected void sendMessage(ProgressBean progressBean) {
mHandler.obtainMessage(DOWNLOAD_PROGRESS, progressBean).sendToTarget();
}
@Override
protected void handleMessage(Message message) {
switch (message.what) {
case DOWNLOAD_PROGRESS:
ProgressBean progressBean = (ProgressBean) message.obj;
onProgress(progressBean.getBytesRead(), progressBean.getContentLength(), progressBean.isDone());
}
}
}
在创建一个UploadProgressHandler
类,继承于ProgressHandler
,用来发送和处理上传消息。
public abstract class UploadProgressHandler extends ProgressHandler {
private static final int UPLOAD_PROGRESS = 0;
protected ResponseHandler mHandler = new ResponseHandler(this, Looper.getMainLooper());
@Override
protected void sendMessage(ProgressBean progressBean) {
mHandler.obtainMessage(UPLOAD_PROGRESS, progressBean).sendToTarget();
}
@Override
protected void handleMessage(Message message) {
switch (message.what) {
case UPLOAD_PROGRESS:
ProgressBean progressBean = (ProgressBean) message.obj;
onProgress(progressBean.getBytesRead(), progressBean.getContentLength(), progressBean.isDone());
}
}
}
定义上传下载回调Service
/**
* 创建带响应进度(下载进度)回调的Service
*/
public static <T> T getDownLoadService(Class<T> tClass) {
return builder
.client(ProgressHelper.addProgressDownLoadBuilder(OkHttpClientBuilder()))
.build()
.create(tClass);
}
/**
* 创建带请求进度(上传进度)回调的Service
*/
public static <T> T getUpLoadService(Class<T> tClass) {
return builder
.client(ProgressHelper.addProgressUpLoadBuilder(OkHttpClientBuilder()))
.build()
.create(tClass);
}
定义上传下载API
接口
@GET
Call<ResponseBody> requestByDownload(@Url String url);
@Multipart
@POST("{url}")
Call<ResponseBody> requestByUpload(@Path("url") String url, @PartMap Map<String, RequestBody> params);
使用方式
/**
* 下载文件
*/
private void requestByDownload() {
final ProgressDialog dialog = new ProgressDialog(this);
dialog.setProgressNumberFormat("%1d KB/%2d KB");
dialog.setTitle("下载");
dialog.setMessage("正在下载,请稍后...");
dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
dialog.setCancelable(false);
dialog.show();
ProgressHelper.setProgressHandler(new DownloadProgressHandler() {
@Override
protected void onProgress(long bytesRead, long contentLength, boolean done) {
LogUtil.e("是否在主线程中运行: " + String.valueOf(Looper.getMainLooper() == Looper.myLooper()));
LogUtil.e("onProgress: " + String.format("%d%% \n", (100 * bytesRead) / contentLength));
LogUtil.e("done: " + String.valueOf(done));
dialog.setMax((int) (contentLength / 1024));
dialog.setProgress((int) (bytesRead / 1024));
if (done) {
dialog.dismiss();
}
}
});
HttpApi mDownLoadHttpApi = HttpUtil.getDownLoadService(HttpApi.class);
Call<ResponseBody> call = mDownLoadHttpApi.requestByDownload(Constant.mobileSafe);
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
InputStream is = null;
FileOutputStream fos = null;
BufferedInputStream bis = null;
try {
//获取存储文件夹
String dirName = Environment.getExternalStorageDirectory().getAbsolutePath() + "/RetrofitDownload/";
File file = new File(dirName);
//如果目录不存在则创建
if (!file.exists()) {
file.mkdir();
}
File fileName = new File(dirName + "mobileSafe.apk");
if (fileName.exists()) {
fileName.delete();
fileName.createNewFile();
} else {
fileName.createNewFile();
}
is = response.body().byteStream();
fos = new FileOutputStream(fileName);
bis = new BufferedInputStream(is);
byte[] buffer = new byte[1024];
int len;
while ((len = bis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
fos.flush();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (bis != null) {
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
ToastUtil.showText(t.getMessage());
}
});
}
/**
* 上传文件
*/
private void requestByUpload() {
final ProgressDialog dialog = new ProgressDialog(this);
dialog.setProgressNumberFormat("%1d KB/%2d KB");
dialog.setTitle("上传");
dialog.setMessage("正在上传,请稍后...");
dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
dialog.setCancelable(false);
dialog.show();
ProgressHelper.setProgressHandler(new UploadProgressHandler() {
@Override
protected void onProgress(long bytesRead, long contentLength, boolean done) {
LogUtil.e("是否在主线程中运行: " + String.valueOf(Looper.getMainLooper() == Looper.myLooper()));
LogUtil.e("onProgress: " + String.format("%d%% \n", (100 * bytesRead) / contentLength));
LogUtil.e("done: " + String.valueOf(done));
dialog.setMax((int) (contentLength / 1024));
dialog.setProgress((int) (bytesRead / 1024));
if (done) {
dialog.dismiss();
}
}
});
HttpApi mUpLoadHttpApi = HttpUtil.getUpLoadService(HttpApi.class);
Map<String, RequestBody> params = new HashMap<>();
Call<ResponseBody> call = mUpLoadHttpApi.requestByUpload("", params);
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
ToastUtil.showText(t.getMessage());
}
});
}
项目地址 ☞ 传送门