OkHttp 总结, 常见问题以及简单封装

这篇文章总结了 OKHttp 的一些知识点, 以及使用过程中容易出现的问题, 我对 OKHttp 的封装.

OKHttp 的特点: 使用简单方便, 设计非常巧妙, 使用了很多设计模式. 支持 https, 支持 websocket, 管理cookie 方便, 缓存机制, 拦截器.

文章结构

一. 简单使用

1 构建一个 OKHttpClient

简单翻译即为 OKHttp 客户端, 在发送请求之前, 我们需要构建一个 OKHttpClient 对象, 该对象用于发送我们构建的请求, 并管理一些请求所需要的配置, 例如如何处理 Cookies, 统一 Headers, 如何配置 SSL/TSL 协议.
在一般情况下, 我们只在全局构建一个 OKHttpClient 对象, 因为这样我们不需要为每个请求都配置一些共有的参数.

构建该对象使用了构建者模式, OkHttp 中大量使用构建者模式以及其他一些设计模式. 这里我们构建了一个最简单的 client 对象.

OKHttpClient client = new OkHttpClient.Builder().build();

2 Request 类, 构建 HTTP 请求

OKHttp 将所有类型的请求都封装为一个 Request, 一个 Request 中主要的参数有 url, method, 请求参数, header, 支持的请求方法有 GET, HEAD, POST, DELETE, PUT, PATCH.

其中协议类型不能像浏览器一样省略.

2.1 构建 GET 请求

Request request = new Request.Builder().url("https://www.baidu.com").get().build();

这里我们构建了一个请求, 请求 url 为百度首页, 方法为 get, 其中 get 方法可以省略, 默认方法就是 get.

2.2 构建 POST 请求

FormBody formBody = new FormBody.Builder()
        .add("name1", "value1")
        .add("name2", "value2")
        .addEncoded("name3",new String("value3".getBytes(), Charset.forName("utf-8")))
        .build();
Request request = new Request.Builder()
        .url("http://example.com")
        .post(formBody)
        .build();

post 请求与 get 不同的是多了一个 FormBody 请求表单. 在一些特殊情况下可以用 addEncoded 对部分字段设置编码方式, 那么将不会再次对应字段进行编码.

3 发送请求

发送请求前需要先用之前创建的 client 以及 request 创建一个 call. Call 类似于在浏览器点击一个链接开启的一个新页面, 我们可以查看该页面的状态,是否请求完成, 或者关闭该页面取消这次请求.

这个步骤的意义在于我可以在他返回请求结果前取消他, 为什么要取消, 例如用户打开一个页面我们发起一个网络请求在结果没有返回之前用户又关闭了, 而请求结果返回到页面时页面已经销毁, 将引起一个 NullPointException.

Call call = client.newCall(request);

3.1 同步请求

Response response = call.execute();

调用 call 的 execute 方法即可执行该请求, 同步请求将阻塞当前线程, 在 Android 主线程中调用这个方法将引发 ANR(Application Not Response) 错误. 直到请求结果返回给 response. 所有的关于这次请求的内容都可以在该 response 中得到.

3.2 异步请求

call.enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {

    }
    @Override
    public void onResponse(Call call, Response response) throws IOException {

    });

调用 enqueue 方法并传入一个用于监听请求结果的 CallBack 接口实例即可异步执行该请求. CallBack 中 onFailure 表示服务器响应失败, 多半是网络问题, onResponse 表示收到服务器响应.

4 请求结果, Response

所有关于这次请求的内容, 都封装在这个对象中, 比如请求 Request, 响应 Header. 响应时长, 使用协议, 是否重定向, 等等.

4.1 响应主体 ResponseBody

在请求结果 response 中包含了我们需要的响应主体 responseBody, 在 responseBody 中我们可以获取更多我们直接想要的类型的结果, 比如 String, InputStream, bytes.

同时还可以通过该对象获取到响应主体的 MIME 类型, 响应主体大小. 其中应注意的是, string() 方法只能调用一次.

ResponseBody responseBody = response.boody()
responseBody.string();
responseBody.contentType();
responseBody.byteStream();

4.2 Response 比较重要的几个方法

  • isSuccessful() 请求是否返回 200 OK, 表示这次请求是否成功.
  • code() 响应 HTTP 状态码, 用于判断这次响应的状态.
  • headers() 包含了本次响应的所有 header.
  • request() 这个响应的请求对象
  • close() 关闭响应流并释放系统资源

二. 如何管理 Cookies 以及 Headers

Cookie 和 Header 是我们经常需要设置的, 而 OkHttp 对这方面提供的方法和接口非常方便使用.

1 Cookies

cookie 的属性介绍

  • name cookie 的名称
  • value 值
  • domain 该 cookie 的使用域名
  • path 路径
  • secure 是否只在使用 https 时传输该 cookie
  • expires/Max-Age 过期时间, 默认为直到浏览器关闭

在构建 OkHttpClient 对象的时候, 其中有一个 cookieJar 方法, 这个方法中接受一个 CookieJar 接口, 我们只需传入一个实现了该接口的对象即可.

从该接口方法名即可看出. saveFromResponse 方法即但响应中有 cookie 时调用, 用于保存从响应中返回的 cookie. 而 loadForRequest 则是每次请求都将调用, 我们可以根据传入的 url 返回相应的 cookie.

这里我实现了一个简单管理 cookie 的 CookieManager.

OkHttpClient client = new OkHttpClient.Builder()
        .cookieJar(new CookieManager())
        .build();
...
class CookieManager implements CookieJar{

    private Map小于String, ConcurrentHashMap小于String, Cookie大于大于 cookies = new HashMap小于大于();

    @Override
    public void saveFromResponse(@NonNull HttpUrl httpUrl, @NonNull List小于Cookie大于 list) {
        if (list.size() 大于 0) {
            for (Cookie item : list) {
                add(httpUrl, item);
            }
        }
    }
    @Override
    public List小于Cookie大于 loadForRequest(@NonNull HttpUrl url) {

        ArrayList小于Cookie大于 ret = new ArrayList小于大于();
        if (cookies.containsKey(url.host()))
            ret.addAll(cookies.get(url.host()).values());
        return ret;
    }
    private void add(HttpUrl url, Cookie cookie){

        String name = cookie.name() + "@" + cookie.domain();
        if (cookie.persistent()) {
            if (!cookies.containsKey(url.host())) {
                cookies.put(url.host(), new ConcurrentHashMap小于大于());
            }
            cookies.get(url.host()).put(name, cookie);
        } else {
            if (cookies.containsKey(url.host())) {
                cookies.get(url.host()).remove(name);
            }
        }
    }
}

我在这里遇到一个问题, 在 saveFromResponse 中的 cookie 如果没有设置 expired 属性, cookie 的过期时间值则 persistent 会为 false, expiredAt() 返回一个错误的时间.

在这一点需要注意一下.

2. Headers

比较低级的方法就是直接给 Request 设置 headers, 比较好的方法是在构建 OkHttpClient 对象时通过 addInterceptor 方法添加一个拦截器给所有请求添加header.
拦截器将在后面介绍, 这里先用第一种方法.

    Headers headers = new Headers.Builder()
            .add("User-Agent","okhttpclient")
            .add("Host","baidu.com")
            .build();
    Request request = new Request.Builder().url("https://baidu.com").headers(headers).build();

非常简单.但是如果每个请求都要这样设置就不简单了.

三. Multipart 类型表单, 上传文件

上传文件是非常常用的, 使用也与普通请求差不了多少, 只是 RequestBody 变成了 MultipartBody, 这与 html 中的表单一致了, 可以添加
普通的字段, 也可以添加文件;

例如:

File file = new File("/a.txt");
MultipartBody.Builder builder =new MultipartBody.Builder()
        .addFormDataPart("field","value")
        .addFormDataPart("file", file.getName(), RequestBody.create(MediaType.parse("text/plain"), file));
Request request = new Request().url("http://example.com").post(builder.build()).build();

这里上传了一个文件, 和一个字段. 其中 RequestBody.create() 可以传入多种参数, 比如 byte[], ByteString;

四. Interceptor, 拦截器

拦截器用于拦截所有请求并统一做出处理, 比如添加一个请求头, 在请求响应后作出预先处理, 比如简单的判断请求是否成功, 从请求头中获取 cookies 并保存,
或者记录每次请求, 花费时间, 以及相关信息;

灵活地运用拦截器可以更好的管理项目中的请求, 使代码更加精炼无冗余;

Interceptor 是一个接口, 只需实现该接口并传入构建 okHttpClient 的 Builder.addInterceptor(Interceptor) 方法中即可添加一个拦截器;

class HeaderInterceptor implements Interceptor{
    @Override
    public Response intercept(Chain chain) throws IOException {

        Request request = chain.request();
        request.newBuilder().addHeader("Host", "example.com");
        Response response = chain.proceed(request);
        String contentType = response.header("contentType");
        // do something else
        return response;
    }
}
// ...
OkHttpClient client = new OkHttpClient.Builder()
                .addInterceptor(new HeaderInterceptor())
                //...
                .build();

这个拦截器给所有请求进行前添加了一个 Header 请求头 Host, 并且在所有响应返回前获取了响应的类型, 当然, 这里可以做更多巧妙的事情.

比如如果不用 OkHttp 自带的 CookieJar, 则可以在这做出相应的处理, 获取所有的 Cookie 并管理;

五. 使用 HTTPS, 设置 X509TrustManager 和 HostNameVerifier

现在已经2018 年了, 基本都使用 HTTPS 协议进行传输, 因为他才能保证安全;

OkHttp 支持 CA 认证 HTTPS 请求, 非 CA 证书需要自行配置, 网站证书可以在浏览器中导出, 导出格式需为 .cer, 然后将证书放在 app/src/main/assets/ 文件夹中,
该文件夹不存在则自行创建, 这个例子文件名为 cert.cer 并且协议为 TLS;

class HttpsUtil {

    public static SSLSocketFactory getSslSocketFactory(X509TrustManager trustManager){

        SSLSocketFactory sslSocketFactory = null;

        try {
            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, new TrustManager[]{trustManager}, null);
            sslSocketFactory = sslContext.getSocketFactory();
        } catch (NoSuchAlgorithmException | KeyManagementException e) {
            e.printStackTrace();
        }
        return sslSocketFactory;
    }

    public static X509TrustManager getX509TrustManager(Context context){

        X509TrustManager x509TrustManager = null;
        InputStream inputStream = null;

        try {
            inputStream = context.getAssets().open("cert.cer");
            x509TrustManager = trustManagerForCertificates(inputStream);
        } catch (IOException | GeneralSecurityException e) {
            e.printStackTrace();
        }finally {
            if (null != inputStream){
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return x509TrustManager;
    }

    private static X509TrustManager trustManagerForCertificates(InputStream in)
            throws GeneralSecurityException {

        CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
        Collection小于? extends Certificate大于 certificates = certificateFactory.generateCertificates(in);
        if (certificates.isEmpty()) {
            throw new IllegalArgumentException("expected non-empty set of trusted certificates");
        }
        char[] password = "password".toCharArray();
        KeyStore keyStore = newEmptyKeyStore(password);
        int index = 0;
        for (Certificate certificate : certificates) {
            String certificateAlias = Integer.toString(index++);
            keyStore.setCertificateEntry(certificateAlias, certificate);
        }

        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(
                KeyManagerFactory.getDefaultAlgorithm());
        keyManagerFactory.init(keyStore, password);
        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
                TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(keyStore);
        TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
        if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
            throw new IllegalStateException("Unexpected default trust managers:"
                    + Arrays.toString(trustManagers));
        }
        return (X509TrustManager) trustManagers[0];
    }

    private static KeyStore newEmptyKeyStore(char[] password) throws GeneralSecurityException {
        try {
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            InputStream in = null; // By convention, 'null' creates an empty key store.
            keyStore.load(in, password);
            return keyStore;
        } catch (IOException e) {
            throw new AssertionError(e);
        }
    }
}

这是一个工具类, 可以方便获取 X509TrustManager 和 SslSocketFactory 关于 X509 是啥, 这是传输协议相关的东西.

如何设置, SslSocketFactory 和 HostnameVerifier

List小于String大于 hosts = new ArrayList小于String大于(){{
        add("example.com}");
        add("www.example.com");
        add("static.example.com");
 }};
X509TrustManager trustManager = HttpsUtil.getX509TrustManager(context);
OkHttpClient client = new OkHttpClient.Builder()
        .hostnameVerifier(verifier)
        .hostnameVerifier(new HostnameVerifier() {
                                @Override
                                public boolean verify(String hostname, SSLSession session) {
                                    return hosts.contains(hostname);
                                }
                            })
        .sslSocketFactory(HttpsUtil.getSslSocketFactory(trustManager), trustManager)
        .build();

HostnameVerifier 是在每次需要验证远程主机名的时候调用, 返回true 则表示验证通过, 返回 false 则这次请求会报错.
验证的域名则是需要用到的域名, 但需与证书颁发的域名匹配;

六. 简单的封装

为啥要封装, 为了可复用, 解耦合, 封装以后用起来特别爽, 例如

对 Request 进行封装

public final class MRequest {

    private Request.Builder requestBuilder;

    private MRequest(Request.Builder requestBuilder){
        this.requestBuilder = requestBuilder;
    }

    public Request getRequest() {
        return requestBuilder.build();
    }

    public Request.Builder getRequestBuilder(){
        return requestBuilder;
    }

    public MRequest addHeader(String key, String value){
        requestBuilder.addHeader(key, value);
        return this;
    }

    public MRequest addHeaders(Map小于String, String大于 headers){

        for (Map.Entry小于String, String大于 entry:
                headers.entrySet()){
            requestBuilder.addHeader(entry.getKey(), entry.getValue());
        }
        return this;
    }

    public static MRequest get(String url){

        Request.Builder mRequestBuilder = new Request.Builder().get().url(url);
        return new MRequest(mRequestBuilder);
    }

    public static MRequest get(String url, Map小于String, String大于 params){

        if (( null == params) || (params.size() == 0)){
            return get(url);
        }
        Request.Builder mRequestBuilder = new Request.Builder();
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(url)
                    .append("?");

        for (Map.Entry小于String, String大于 entry:
                params.entrySet()){
            stringBuilder.append(entry.getKey())
                        .append("=")
                        .append(entry.getValue())
                        .append("&");
        }
        mRequestBuilder.url(stringBuilder.substring(0, stringBuilder.length()-1));
        return new MRequest(mRequestBuilder);
    }

    public static MRequest post(String url, Map小于String, String大于 params){

        Request.Builder mRequestBuilder = new Request.Builder();
        FormBody.Builder formBodyBuilder = new FormBody.Builder();

        for (Map.Entry小于String,String大于 entry:
             params.entrySet()) {
            formBodyBuilder.add(entry.getKey(), entry.getValue());
        }

        mRequestBuilder
                .url(url)
                .post(formBodyBuilder.build());

        return new MRequest(mRequestBuilder);
    }
}

(等待更新....)
个人博客链接 https://djh.red/blog/article/20/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容