开源库—HttpClient:过期时间的设置(结合源码分析)

Apache的HttpClient库是Java项目非常常用的一个开源库,用来在代码中发送http请求,并获取响应数据。
HttpClient有几个较大差异的版本,可以分为:

  • httpclient3.x
  • httpclient4.x到httpclient4.3以下
  • httpclient4.3以上

可以看到4.3版本是一个分水岭,和之前的版本在使用上会有较大差异,但其本质肯定还是一样的。

在HttpClient中有3个超时时间是需要我们设置的:

  • ConnectTimeout:客户端和服务器建立连接的超时时间。
  • ConnectionRequestTimeout:从HttpClient连接池获取连接的超时时间。
    在HttpClient4.3以下版本中是叫conn-manager.timeout,定义在org.apache.http.client.params.ClientPNames类中,该类在4.3版本以后被org.apache.http.client.config.RequestConfig所取代。
  • SocketTimeout:客户端从服务器读取数据的超时时间。

在 httpclient4.x到httpclient4.3以下版本中,我们可以这样设置这三个参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 设置组件参数, HTTP协议的版本,1.1/1.0/0.9
HttpParams params = new BasicHttpParams();
HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
HttpProtocolParams.setUserAgent(params, "HttpComponents/1.1");
HttpProtocolParams.setUseExpectContinue(params, true);

/*
* 建立Socket超时时间,10S
*/
params.setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 6000);

/**
* 读取内容超时时间,20秒
*/
params.setParameter(CoreConnectionPNames.SO_TIMEOUT, 20000);

/**
* 该值就是连接池满了,连接不够用的时候等待超时时间,一定要设置,而且不能太大 ,0.5秒
*/
params.setLongParameter(ClientPNames.CONN_MANAGER_TIMEOUT, 500L);

params.setBooleanParameter(CoreConnectionPNames.STALE_CONNECTION_CHECK, true);

DefaultHttpClient httpClient = new DefaultHttpClient(pccm, params);

还可以这样设置:

1
2
3
4

HttpClient httpClient = HttpConnectionManager.getHttpClient();
httpClient.getParams().setIntParameter(CoreConnectionPNames.SO_TIMEOUT, 5000);
httpClient.getParams().setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 10000);

在4.3+版本中,我们可以这样设置这三个参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//HttpClient线程安全,创建唯一的HttpClient实例
HttpClientBuilder httpClientBuilder = HttpClients.custom().setConnectionManager(cm);
httpClientBuilder.setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy() {
@Override
public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
long keepAlive = super.getKeepAliveDuration(response, context);
if (keepAlive == -1) {
// 如果keep-alive值没有由服务器明确设置,那么保持连接持续5秒。
keepAlive = 5000;
}
return keepAlive;
}
});

RequestConfig defaultRequestConfig = RequestConfig.custom()
.setSocketTimeout(SOCKET_TIMEOUT)
.setConnectTimeout(CONNECT_TIMEOUT)
.setConnectionRequestTimeout(CONNECTION_REQUESTT_IMEOUT)
.build();
httpClient = httpClientBuilder
.setDefaultRequestConfig(defaultRequestConfig)
.build();

发送请求的HttpRequest工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
package com.lzumetal.open.source.httpclient.utils;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


public class HttpRequest {

public static void main(String[] args) {
System.out.println(StandardCharsets.UTF_8.name());
}


private static final String DEFAULT_CHARSET_NAME = StandardCharsets.UTF_8.name();

/* 请求头 */
private Map<String, String> headers = new HashMap<>();

/* 参数 */
private Map<String, String> params = new HashMap<>();

/* 请求配置 */
private RequestConfig requestConfig;


public HttpRequest addHead(String key, String value) {
headers.put(key, value);
return this;
}

public HttpRequest addHeads(Map<String, String> headers) {
this.headers.putAll(headers);
return this;
}

public HttpRequest setHeads(Map<String, String> headers) {
this.headers = headers;
return this;
}

public HttpRequest addParam(String key, String value) {
params.put(key, value);
return this;
}

public HttpRequest addParams(Map<String, String> params) {
this.params.putAll(params);
return this;
}

public HttpRequest setParams(Map<String, String> params) {
this.params = params;
return this;
}


private List<NameValuePair> mapToNameValuePairList(Map<String, String> formParams) {
List<NameValuePair> nameValuePairs = new ArrayList<>();
for (Map.Entry<String, String> entry : formParams.entrySet()) {
nameValuePairs.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
}
return nameValuePairs;
}

public String get(String url, Map<String, String> params) throws IOException {
this.params.putAll(params);
return get(url);
}


public String get(String url) throws IOException {
String urlTarget = url;
//参数需要拼接
if (params != null && params.size() > 0) {
urlTarget = url + "?" + URLEncodedUtils.format(mapToNameValuePairList(params), DEFAULT_CHARSET_NAME);
}
HttpGet method = new HttpGet(urlTarget);
return executeMethod(method);
}

private RequestConfig buildRequestConfig(int connectTimeout, int socketTimeout) {
return RequestConfig.custom()
.setConnectTimeout(connectTimeout)
.setConnectionRequestTimeout(1000)
.setSocketTimeout(socketTimeout)
.build();

}


public String post(String url, Map<String, String> params, int connectTimeout, int socketTimeout) throws IOException {
this.params.putAll(params);
this.requestConfig = buildRequestConfig(connectTimeout, socketTimeout);
return post(url);
}


public String post(String url, Map<String, String> params) throws IOException {
this.params.putAll(params);
return post(url);
}

private String post(String url) throws IOException {
HttpPost method = new HttpPost(url);
if (params != null && params.size() > 0) {
UrlEncodedFormEntity uefEntity = new UrlEncodedFormEntity(mapToNameValuePairList(params), DEFAULT_CHARSET_NAME);
method.setEntity(uefEntity);
}
if (this.requestConfig != null) {
method.setConfig(this.requestConfig);
}
return executeMethod(method);
}

public String postJson(String url, String json) throws IOException {
HttpPost method = new HttpPost(url);
method.setEntity(new StringEntity(json, DEFAULT_CHARSET_NAME));
method.setHeader("content-type", ContentType.APPLICATION_JSON.getMimeType());
return executeMethod(method);
}


public String postMultipartFile(String url, String fileParamName, MultipartFile multipartFile, Map<String, String> otherParams) throws IOException {
HttpPost method = new HttpPost(url);
MultipartEntityBuilder builder = MultipartEntityBuilder.create()
.setCharset(StandardCharsets.UTF_8)
.setMode(HttpMultipartMode.BROWSER_COMPATIBLE)//加上此行代码解决返回中文乱码问题
.addBinaryBody(fileParamName, multipartFile.getBytes(), ContentType.MULTIPART_FORM_DATA, multipartFile.getOriginalFilename());// 文件流
for (Map.Entry<String, String> e : otherParams.entrySet()) {
builder.addTextBody(e.getKey(), e.getValue());//表单提交其他参数
}
HttpEntity entity = builder.build();
method.setEntity(entity);
return executeMethod(method);
}


private String executeMethod(HttpUriRequest request) throws IOException {
if (this.headers != null && this.headers.size() > 0) {
for (Map.Entry<String, String> entry : this.headers.entrySet()) {
request.addHeader(entry.getKey(), entry.getValue());
}
}
HttpClient httpClient = HttpConnectionManager.getInst().getHttpClient();
HttpResponse response = httpClient.execute(request);
int status = response.getStatusLine().getStatusCode();
if (status == HttpStatus.SC_OK) {
HttpEntity entity = response.getEntity();
return entity != null ? EntityUtils.toString(entity, DEFAULT_CHARSET_NAME) : null;
} else {
throw new ClientProtocolException("Unexpected response status: " + status);
}
}


}

从上面4.3+版本的代码中可以看到,既可以在HttpClient实例中配置RequestConfig,也可以在HttpRequest实例中配置RequestConfig,那应该以哪个配置优先呢?结论是:HttpRequest中配置的RequestConfig优先级更高。下面分析一下相关源码就清楚了。

  1. 代码中HttpClient实例是通过HttpClientBuilderbuild()方法创建的,这个方法很长,最终返回的是一个org.apache.http.impl.client.InternalHttpClient对象,它继承了org.apache.http.impl.client.CloseableHttpClient抽象类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public class HttpClientBuilder {


    public CloseableHttpClient build() {
    //省略代码
    return new InternalHttpClient(
    execChain,
    connManagerCopy,
    routePlannerCopy,
    cookieSpecRegistryCopy,
    authSchemeRegistryCopy,
    defaultCookieStore,
    defaultCredentialsProvider,
    defaultRequestConfig != null ? defaultRequestConfig : RequestConfig.DEFAULT,
    closeablesCopy);
    }

    //省略...

    }
  2. 最终发出http请求是在HttpClient类的execute方法中,我们来看CloseableHttpClient中这个方法的调用,最终会调CloseableHttpClient中的doExecute方法,这是一个抽象方法,在org.apache.http.impl.client.InternalHttpClient中实现了这个方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39

    @Override
    protected CloseableHttpResponse doExecute(
    final HttpHost target,
    final HttpRequest request,
    final HttpContext context) throws IOException, ClientProtocolException {
    Args.notNull(request, "HTTP request");
    HttpExecutionAware execAware = null;
    if (request instanceof HttpExecutionAware) {
    execAware = (HttpExecutionAware) request;
    }
    try {
    final HttpRequestWrapper wrapper = HttpRequestWrapper.wrap(request, target);
    final HttpClientContext localcontext = HttpClientContext.adapt(
    context != null ? context : new BasicHttpContext());
    RequestConfig config = null;
    if (request instanceof Configurable) {
    config = ((Configurable) request).getConfig();
    }
    if (config == null) {
    final HttpParams params = request.getParams();
    if (params instanceof HttpParamsNames) {
    if (!((HttpParamsNames) params).getNames().isEmpty()) {
    config = HttpClientParamConfig.getRequestConfig(params, this.defaultConfig);
    }
    } else {
    config = HttpClientParamConfig.getRequestConfig(params, this.defaultConfig);
    }
    }
    if (config != null) {
    localcontext.setRequestConfig(config);
    }
    setupContext(localcontext);
    final HttpRoute route = determineRoute(target, wrapper, localcontext);
    return this.execChain.execute(route, wrapper, localcontext, execAware);
    } catch (final HttpException httpException) {
    throw new ClientProtocolException(httpException);
    }
    }

根据我们前面写的4.3+版本工具类代码,HttpPost、HttpGet等类都继承自HttpEntityEnclosingRequestBase,而HttpEntityEnclosingRequestBase继承自HttpRequestBaseHttpRequestBase实现了Configurable接口,所以request instanceof Configurable为true,最终执行localcontext.setRequestConfig(config);这行代码,由此可知http请求的RequestConfig优先会取HttpRequest中的RequestConfig

  1. HttpRequest中没有配置RequestConfig时,再看setupContext(localcontext);这个方法,这个方法里会设置默认的RequestConfig

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    private void setupContext(final HttpClientContext context) {
    if (context.getAttribute(HttpClientContext.TARGET_AUTH_STATE) == null) {
    context.setAttribute(HttpClientContext.TARGET_AUTH_STATE, new AuthState());
    }
    if (context.getAttribute(HttpClientContext.PROXY_AUTH_STATE) == null) {
    context.setAttribute(HttpClientContext.PROXY_AUTH_STATE, new AuthState());
    }
    if (context.getAttribute(HttpClientContext.AUTHSCHEME_REGISTRY) == null) {
    context.setAttribute(HttpClientContext.AUTHSCHEME_REGISTRY, this.authSchemeRegistry);
    }
    if (context.getAttribute(HttpClientContext.COOKIESPEC_REGISTRY) == null) {
    context.setAttribute(HttpClientContext.COOKIESPEC_REGISTRY, this.cookieSpecRegistry);
    }
    if (context.getAttribute(HttpClientContext.COOKIE_STORE) == null) {
    context.setAttribute(HttpClientContext.COOKIE_STORE, this.cookieStore);
    }
    if (context.getAttribute(HttpClientContext.CREDS_PROVIDER) == null) {
    context.setAttribute(HttpClientContext.CREDS_PROVIDER, this.credentialsProvider);
    }
    //设置默认的RequestConfig对象
    if (context.getAttribute(HttpClientContext.REQUEST_CONFIG) == null) {
    context.setAttribute(HttpClientContext.REQUEST_CONFIG, this.defaultConfig);
    }
    }
  2. 那这个默认的RequestConfig是怎么来的呢,来源就是InternalHttpClient的构造方法,在CloseableHttpClientbuild()方法中回调用InternalHttpClient的构造方法生成实例。传入的RequestConfig就是HttpClientBuilder类中的持有的成员变量。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class HttpClientBuilder {

    private RequestConfig defaultRequestConfig;

    /**
    * Assigns default {@link RequestConfig} instance which will be used
    * for request execution if not explicitly set in the client execution
    * context.
    */
    public final HttpClientBuilder setDefaultRequestConfig(final RequestConfig config) {
    this.defaultRequestConfig = config;
    return this;
    }

    //省略...

    }
  3. 至此就可以得出前面的结论了,框架发起Http请求时会优先使用HttpRequest实例中配置的RequestConfig;如果没有配置,则去拿HttpClient实例中配置的RequestConfig;如果HttpClient中也没有配置,则取RequestConfig.DEFAULT这个默认配置,默认配置如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Builder() {
    super();
    this.staleConnectionCheckEnabled = false;
    this.redirectsEnabled = true;
    this.maxRedirects = 50;
    this.relativeRedirectsAllowed = true;
    this.authenticationEnabled = true;
    this.connectionRequestTimeout = -1;
    this.connectTimeout = -1;
    this.socketTimeout = -1;
    this.contentCompressionEnabled = true;
    }

最后附上几个相关的类的继承关系图。

------ 本文完 ------