有疑必看

功能支持

支持 SSL(HTTPS)吗?

答:支持,比如以下请求百度的带 https 的网址,不需要任何配置就可以正常运行:

HTTP http = HTTP.builder().build();
String baidu = http.sync("https://www.baidu.com")
    .get()
    .getBody()
    .toString();
System.out.println(baidu);
1
2
3
4
5
6

当然这有一个前提就是是服务器配置的 SSL 证书是值得信任并且有效的,这也是我们推荐的一种方式。

如果服务器的 SSL 证书不是在权威机构购买而是自己生成的(不推荐这种做法),则需要配置sslSocketFactoryhostnameVerifier即可:

HTTP http = HTTP.builder()
    .config(b -> {
        b.sslSocketFactory(mySSLSocketFactory, myTrustManager);
        b.hostnameVerifier(myHostnameVerifier);
    })
    .build();
1
2
3
4
5
6

例如,让 OkHttps 信任所有,上述代码中的mySSLSocketFactorymyTrustManagermyHostnameVerifier可通过如下方式生成:

X509TrustManager myTrustManager = new X509TrustManager() {

    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {}

    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {}

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return new X509Certificate[0];
    }
};

HostnameVerifier myHostnameVerifier = new HostnameVerifier() {
    @Override
    public boolean verify(String hostname, SSLSession session) {
        return true;
    }
};

SSLContext sslCtx = SSLContext.getInstance("TLS");
sslCtx.init(null, new TrustManager[] { myTrustManager }, new SecureRandom());

SSLSocketFactory mySSLSocketFactory = sslCtx.getSocketFactory();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

答:支持,配置方和 OkHttp 完全一样,只需要配置一个 CookieJar 即可:

CookieJar myCookieJar = new CookieJar() {

    @Override
    public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
        // TODO: 保存 cookies
    }

    @Override
    public List<Cookie> loadForRequest(HttpUrl url) {
        // TODO: 读取 cookies
        return null;
    }

};

HTTP http = HTTP.builder()
    .config(b -> {
        b.cookieJar(myCookieJar);
    })
    .build();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

以上配置后,OkHttps 便具有了自动管理 Cookie 的功能,具体请求用户便不用操心 Cookie 了,但如果想手动自己添加一个 Cookie 的话,那只需要添加一个请求头即可,如下:

http.async("https://...")
    // 添加两个 Cookie
    .addHeader("Cookie", "cname1=cvalue1; cname2=cvalue2")
    // ...
    .post();
1
2
3
4
5

支持代理(Proxy)吗?

答:支持,只需配置 Proxy 即可,例如:

HTTP http = HTTP.builder()
    .config(b -> {
        b.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("www.your-proxy.com", 8080)));
    })
    .build();
1
2
3
4
5

支持缓存(Cache)吗?

答:支持,只需配置 Cache 即可,例如:

HTTP http = HTTP.builder()
    .config(b -> {
        b.cache(new Cache("/path-to-cache", 10 * 1024 * 1024));
    })
    .build();
1
2
3
4
5

有失败重试机制吗?

答:很简单,比如以下配置就可实现请求超时重试三次:

HTTP http = HTTP.builder()
    .config(b -> {
        b.addInterceptor(chain -> {
            int retryTimes = 0;	
            while (true) {
                try {
                    return chain.proceed(chain.request());
                } catch (SocketTimeoutException e) {
                    if (retryTimes >= 3) {
                        throw e;
                    }
                    System.out.println("超时重试第" + retryTimes + "次!");
                    retryTimes++;
                }
            }
        });
    }).build();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

以下代码实现服务器状态码为 500 时,自动重试三次:

HTTP http = HTTP.builder()
    .config(b -> {
        b.addInterceptor(chain -> {
            int retryTimes = 0;	
            while (true) {
                Response response = chain.proceed(chain.request());
                if (response.code() == 500 && retryTimes < 3) {
                    System.out.println("失败重试第" + retryTimes + "次!");
                    // 注意,这里一定要 close 掉失败的 Response
                    response.close();
                    retryTimes++;
                    continue;
                }
                return response;
            }
        });
    }).build();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

当然也可以把两者结合起来:

HTTP http = HTTP.builder()
    .config(b -> {
        b.addInterceptor(chain -> {
            int retryTimes = 0;	
            while (true) {
                Response response = null;
                Exception exception = null;
                try {
                    response = chain.proceed(chain.request());
                } catch (Exception e) {
                    exception = e;
                }
                if ((exception != null || response.code() == 500) && retryTimes < 3) {
                    System.out.println("失败重试第" + retryTimes + "次!");
                    if (response != null) {
                        // 注意,这里一定要 close 掉失败的 Response
                        response.close();
                    }
                    retryTimes++;
                    continue;
                } else if (exception != null) {
                    throw exception;
                }
                return response;
            }
        });
    }).build();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

常见问题

HttpException: 没有匹配[null/json]类型的转换器!

当出现这个异常时,一般是让 OkHttps 去自动解析 JSON 却没有给它配置MsgConvertor导致的,当遇到这个异常,可按如下步骤检查:

1、 项目依赖中是否添加了 json 扩展包:okhttps-fastjsonokhttps-gsonokhttps-jackson,添加一个即可;

2、 发起请求时,使用的是 OkHttps 提供的工具类(OkHttpsHttpUtils)还是 自己构建的HTTP实例,如果是前者,框架会自动配置MsgConvertor,若是后者,得自己手动配置MsgConvertor

HTTP http = HTTP.builder()
    .addMsgConvertor(new GsonMsgConvertor());       // okhttps-gson
    .addMsgConvertor(new JacksonMsgConvertor());    // okhttps-jackson
    .addMsgConvertor(new FastjsonMsgConvertor());   // okhttps-fastjson
    .build();
1
2
3
4
5

3、 项目依赖中已经添加了 json 扩展包,并且使用的是 OkHttps 提供的工具类(OkHttpsHttpUtils),但还是有这个异常(罕见),这个时候一般是 IDE 的编译器的 BUG 导致的,请 clean 一下项目,重新运行即可。

HttpException: 转换失败 Caused by IOException: closed

当出现这个异常时,很可能是对报文体重复消费(多次调用 toXXX 方法)造成的,类似以下代码:

Body body = OkHttps.sync("/api/users/1").get().getBody();

log.info("body = " + body);             // 这里隐式的调用了 body 的 toString 消费方法

User user = body.toBean(User.class);    // 这里又调用了一次 toBean,将会抛出异常
1
2
3
4
5

以上代码,由于多次调用报文体的消费方法,则会导致此异常,如果确实需要多次消费时,可以先使用cache方法,如下:

Body body = OkHttps.sync("/api/users/1").get().getBody()
        .cache();                       // 先调用 cache 方法,就可以多次消费了

log.info("body = " + body);             // 这里隐式的调用了 body 的 toString 消费方法

User user = body.toBean(User.class);    // 又调用了一次 toBean,则不会再有问题
Mapper mapper = body.toMapper();        // 再调用一次,依然没问题
1
2
3
4
5
6
7

HttpException: 报文体转换字符串出错 Caused by IOException: Content-Length (xxx) and stream length (0) disagree

当出现这个异常,同样很可能是多次消费报文体的问题(同上),再类似以下的代码:

HttpResult.Body body1 = OkHttps.async("/api/...")
        .setOnResponse(res -> {
            HttpResult.Body body2 = res.getBody();
            String str2 = body2.toString();     // 这里消费了一次报文体
            // ...
        })
        .get()
        .getResult()
        .getBody();

String str1 = body1.toString();                 // 这里又消费了一次报文体
1
2
3
4
5
6
7
8
9
10
11

以上的代码,在第 4、11 行都消费了报文体,但是没有提前使用cache()方法,所以会报错,如下修改即可:

HttpResult.Body body1 = OkHttps.async("/api/...")
        .setOnResponse(res -> {
            HttpResult.Body body2 = res.getBody()
                    .cache();                   // 使用 cache
            String str2 = body2.toString();
            // ...
        })
        .get()
        .getResult()
        .getBody()
        .cache();                               // 使用 cache

String str1 = body1.toString();
1
2
3
4
5
6
7
8
9
10
11
12
13

若使用的 OkHttps 版本是 v2.4.2 及以前版本,上面的代码还得考虑线程安全问题,加一个锁即可:

Object lock = new Object();

HttpResult res1 = OkHttps.async("/api/...")
        .setOnResponse(res2 -> {

            synchronized(lock) {
            	String str2 = res2.getBody().cache().toString();
            	// ...
            }

        })
        .get()
        .getResult()

synchronized(lock) {
	String str1 = res1.getBody().cache().toString();
	// ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

OkHttps v2.4.3 及以后版本则不必如此。

NoSuchMethodError: kotlin.collections.ArraysKt.copyInto([B[BIII)[B

一般出现这个问题,是因为依赖了 v4.x 的 OkHttp, 而你的项目是纯 Java 项目,没有添加 kotlin 的依赖导致(由于 OkHttp v4.x 是用 kotlin 重写)。

解决办法:依赖换成 v3.x 的 OkHttp 即可。

JSON 请求后端收不到数据,JSON 被加上双引号当做字符串了?

List<String> values = new ArrayList<>();
values.add("value1");
values.add("value2");

OkHttps.sync("/api/...")
    .bodyType(OkHttps.JSON)
    .addBodyPara("name", "Test")
    .addBodyPara("values", values)
    .post();
1
2
3
4
5
6
7
8
9

如上,用户可能期望发送这样的 JSON 给服务器:

{
    "name": "Test",
    "values": [ "value1", "value2" ]
}
1
2
3
4

但实际上服务器收到的却是这样:

{
    "name": "Test",
    "values": "[\"value1\", \"value2\"]"
}
1
2
3
4

这是因为addBodyPara方法添加的参数只支持单层数据结构(v3.4 开始支持多层结构),若要支持多层数据结构,必须使用setBodyPara方法(或者使用 v3.4 及以上版本),如下:

List<String> values = new ArrayList<>();
values.add("value1");
values.add("value2");

Map<String, Object> paraMap = new HashMap<>();
paraMap.put("name", "Test");
paraMap.put("values", values);

OkHttps.sync("/api/...")
    .bodyType(OkHttps.JSON)
    .setBodyPara(paraMap)
    .post();
1
2
3
4
5
6
7
8
9
10
11
12

A connection to http://xxx.xx/ was leaked. Did you forget to close a response body?

这个问题,提示已经非常明确:一个连接泄漏,你是否忘记了关闭响应报文体?。遇到此问题时,可按一下步骤来检查:

1、 确认每一个请求都 消费了关闭了 响应报文(体),例如:

HttpResult res = OkHttps.sync("/api/...").get();
res.close();        // 关闭报文
1
2

或者

HttpResult.Body body = OkHttps.sync("/api/...").get().getBody();
body.close();       // 关闭报文体
1
2

或者

HttpResult.Body body = OkHttps.sync("/api/...").get().getBody();
body.toString();    // 消费报文体:toXXX() 系列方法
1
2

2、 已确认每一个使用 OkHttps 的地方都按照以上要求编写请求代码,但仍然出现这个问题,那么请检查项目中除了使用 OkHttps 框架,是否还存在直接使用 OkHttp 进行请求的代码,是否还有使用其它的基于 OkHttp 的框架。

同一个接口,Postman 可以请求通,但 OkHttps 请求不通,怎么回事呢?

当出现这个情况时,按照以下四步要诀检查,都可以找到问题原因:

  1. 检查 请求方法 是否一致
  2. 检查 请求路径(包括查询参数) 是否一致
  3. 检查 报文体参数 是否一致
  4. 检查 请求头 是否一致

以上 四步要诀,在遇到此类问题时绝对百试百灵,但我们在 OkHttps 交流群中注意到,最困扰同学们的,常有两种情况:

后端的接口 需要客户端携带 Cookie 才能访问,而 Postman 会自动记录服务器下发的 Cookie 并下次请求时 自动携带,即相当于 OkHttps 配置了 CookieJar 的效果。

2、服务端对 OkHttp 做了特殊处理

由于 OkHttp 几乎是最流行的 Java HTTP 客户端,黑客 爬虫们 也都非常喜欢使用它,所以很多网站为了防爬虫、防黑客,都对 User-Agent 头中携带 OkHttp 字样的请求做了特殊处理。

所以这种情况,我们只需修改 User-Agent 值即可,例如:

String html = OkHttps.sync("http://www.baidu.com/")
        .addHeader("User-Agent", "xxx")
        .get()
        .getBody()
        .toString();
System.out.println(html);
1
2
3
4
5
6

这里我还是要劝劝各位同行,学习交流可以,但不要恶意去爬别人的网站,如果你坚持这么做,只会导致行业内卷更加严重(道高一尺魔高一丈)。

java.lang.NoSuchFieldError: Companion

Exception in thread "main" java.lang.NoSuchFieldError: Companion
	at okhttp3.internal.Util.<clinit>(Util.kt:70)
	at okhttp3.internal.concurrent.TaskRunner.<clinit>(TaskRunner.kt:309)
	at okhttp3.ConnectionPool.<init>(ConnectionPool.kt:41)
	at okhttp3.ConnectionPool.<init>(ConnectionPool.kt:47)
	at okhttp3.OkHttpClient$Builder.<init>(OkHttpClient.kt:471)
	at cn.zhxu.okhttps.okhttp.OkHttpBuilderImpl.build(OkHttpBuilderImpl.java:241)
	at cn.zhxu.okhttps.OkHttps.getHttp(OkHttps.java:36)
1
2
3
4
5
6
7
8

当出现如上问题时,一般是底层依赖 okhttpokio 的版本不兼容导致,例如下图中 okhttpv4.9.3,而 okiov1.17.5 就会出现上述问题:

知道原因后,问题就很好解决了,只需将 okio 调整为与 okhttp 相匹配的版本即可。

okio 的哪个版本才能与 okhttp 匹配呢?

简单,咱们只需到 Maven 中央仓库去查看即可,例如(okhttp v4.9.3 对应的 okio v2.8.0):

然后我们在 Maven 或 Gradle 配置文件里更正 okio 的版本即可(如果没有显示引用,就添加一个):

<dependency>
    <groupId>com.squareup.okio</groupId>
    <artifactId>okio</artifactId>
    <version>2.8.0</version>
</dependency>
1
2
3
4
5

java.net.ProtocolException: unexpected end of stream

这是因为服务器返会的数据格式错误导致的(没有满足 HTTP 协议的要求)。这个问题,一般常在下载大文件时会出现。

解决思路

遇到这个问题时,先检查一下这个请求在服务器端有没有走网关,这个网关是否对响应的报文体大小做了限制。

如果您的后端用了 Nginx,可以参考这篇文章:https://www.cnblogs.com/binghe001/p/13356662.html

还有其它问题,怎么解决?

在调试模式中

如果你是在调式模式下遇到问题,那可以先参考 调式注意事项 章节。

查看已有 ISSUE

  1. 到 GitHub 的 issue 里看看有没有人提过类似的问题:https://github.cn/zhxu/okhttps/issues?q=is%3Aissue+is%3Aclosed

  2. 到 Gitee 的 issue 里看看有没有人提过类似的问题:https://gitee.com/troyzhxu/okhttps/issues?assignee_id=&author_id=&branch=&issue_search=&label_name=&milestone_id=&program_id=&scope=&sort=&state=closed

利用搜索引擎

例如当遇到如下异常时:

检索关键字一定要输入上图中的 Cause by 部分,如下:

这样便很容易检索到问题的答案,其它异常也是类似。

进入有问必答群

若通过以上几步,问题还没有得到解决,可先加微信:18556739726(请备注 OkHttps)再入群交流,有问必答!!!