Retrofit2.0使用详解

来源:互联网 发布:三十后换工作 知乎 编辑:程序博客网 时间:2024/06/05 11:36

简介

Retrofit是由Square公司提供的开源产品,为Android平台的应用提供一个类型安全的REST客户端。其实质上是对OkHttp的封装,使用面向接口的方式进行网络请求,利用动态生成的代理类封装了网络接口请求的底层,将REST API返回的数据转化为Java对象方便操作,可以进行GETPOSTPUTDELETE等请求,极大的提高了应用的网络体验。

官方文档(英文)
更新日志
用Retrofit 2简化HTTP请求
给Android开发者的RxJava详解
Android文件存储使用参考

1、REST

REST(REpresentational State Transfer)指的是一组架构约束条件和原则。
RESTful架构都满足以下规则:

(1) 每一个URI代表一种资源;
(2) 客户端和服务器之间,传递这种资源的某种表现层;
(3) 客户端通过四个HTTP动词,对服务器端资源进行操作,实现”表现层状态转化”。

什么是REST

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-beta2Jackson:     com.squareup.retrofit:converter-jackson:2.0.0-beta1Moshi:       com.squareup.retrofit:converter-moshi:2.0.0-beta1Protobuf:    com.squareup.retrofit:converter-protobuf:2.0.0-beta1Wire:        com.squareup.retrofit:converter-wire:2.0.0-beta1Simple 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()并指定ServicebaseUrl(通常情况下指的是域名)。还需注意我们要指定一个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(GETPOST等)方法的注解以及用于分发网络调用的方法。

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网站来自动生成,先来看一下它的使用方法。

jsonschema2pojo

上面配置中所选注解也可以选择Gson,对于@Generated注解若是需要保留的话可以添加以下依赖方式,也可以直接删除@Generated注解,没有任何影响。

compile 'org.glassfish:javax.annotation:10.0-b28'

2、GsonFormat

GsonFormatAndroid Studio中的一个插件,在Android Studio的插件选项中直接搜索安装这个插件即可。在这里我们看一下是如何使用这个插件的。

GsonFormat

3、添加转换器

在这里我们需要为Retrofit添加Gson转换器的依赖,如果已经添加过converter-gson那么就不用再添加Gson库,因为在converter-gson中已经包含了Gson

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

在这里先创建一个JavaContributor来保存接收到的数据。

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的场景。

@GETCall<User> getUser(@Url String url);

@Path:URL占位符,用于替换和动态更新,相应的参数必须使用相同的字符串被@Path进行注释。

@GET("user/{userName}")Call<User> getUser(@Path("userName") String userName);//--> http://baseUrl/user/userName

等同于:

@GETCall<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:当参数过多的时候使用它来指定每个表单项的keyvalue值。

@GET("user")Call<User> getUser(@QueryMap(encoded = true) Map<String, String> map);

可以约定是否需要encode

@Field:使用@Field注解和参数来指定每个表单项的keyvalue为参数的值,以表单的方式传递简单的键值对;使用@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对象参数来指定每个表单项的keyvalue值。

@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 fileRequestBody requestFile = RequestBody.create(MediaType.parse("multipart/form-data"), file);// MultipartBody.Part is used to send also the actual file nameMultipartBody.Part body = MultipartBody.Part.createFormData("picture", file.getName(), requestFile);// add another part within the multipart requestString 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 requestCall<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@GETCall<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中使用自签名证书大致要经过以下几步:

  1. 将证书添加到工程中;
  2. 自定义信任管理器TrustManager
  3. 用自定义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添加拦截事件,将RequestBodyResponseBody替换成我们自己实现的ProgressRequestBodyProgressResponseBody

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对象进行发送和处理消息。于是定义了两个抽象方法sendMessagehandleMessage,之后又定义了一个抽象方法onProgress来处理下载进度的显示,而这个onProgress则是需要我们在UI线程内进行调用。最后创建了一个继承自HandlerResponseHandler内部类,为了避免内存泄露我们使用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接口

@GETCall<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());        }    });}

项目地址 ☞ 传送门

原创粉丝点击