在Java开发中,访问第三方HTTP协议的网络接口,通常使用的连接工具为JDK自带的HttpURLConnection、HttpClient(现在应该称之为HttpComponents)和OKHttp。
这些Http连接工具,使用起来都比较复杂,如果项目中使用的是Spring框架,可以使用Spring自带的RestTemplate来进行Http连接请求。
RestTemplate底层默认的连接方式是Java中的HttpURLConnection,可以使用ClientHttpRequestFactory来指定底层使用不同的HTTP连接方式。
RestTemplate中默认的连接方式
RestTemplate中默认使用的是SimpleClientHttpRequestFactory,我们这里手动创建SimpleClientHttpRequestFactory可以指定连接的超时时间,读数据的超时时间。
java
package com.morris.user.demo;
import com.morris.user.entity.Order;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
/**
* restTemplate+httpUrlConnection
*/
@Slf4j
public class RestTemplateDemo1 {
public static void main(String[] args) {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(3000);
factory.setReadTimeout(5000);
RestTemplate restTemplate = new RestTemplate(factory);
Order[] orders = restTemplate.getForObject("http://127.0.0.1:8020/order/findOrderByUserId?userId=", Order[].class, 1);
log.info("orders :{}", orders);
}
}
SimpleClientHttpRequestFactory底层在创建请求的时候使用的就是HttpURLConnection。
org.springframework.http.client.SimpleClientHttpRequestFactory#createRequest
java
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
HttpURLConnection connection = openConnection(uri.toURL(), this.proxy);
prepareConnection(connection, httpMethod.name());
if (this.bufferRequestBody) {
return new SimpleBufferingClientHttpRequest(connection, this.outputStreaming);
}
else {
return new SimpleStreamingClientHttpRequest(connection, this.chunkSize, this.outputStreaming);
}
}
RestTemplate与HttpClient的结合
只需要在构造RestTemplate实例时传入HttpComponentsClientHttpRequestFactory对象即可。
java
package com.morris.user.demo;
import com.morris.user.entity.Order;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
/**
* RestTemplate+HttpClient
*/
@Slf4j
public class RestTemplateDemo2 {
public static void main(String[] args) {
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
RestTemplate restTemplate = new RestTemplate(factory);
Order[] orders = restTemplate.getForObject("http://127.0.0.1:8020/order/findOrderByUserId?userId=", Order[].class, 1);
log.info("orders :{}", orders);
}
}
HttpComponentsClientHttpRequestFactory底层在创建请求时使用了HttpClient。
org.springframework.http.client.HttpComponentsClientHttpRequestFactory#createRequest
java
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
HttpClient client = getHttpClient();
HttpUriRequest httpRequest = createHttpUriRequest(httpMethod, uri);
postProcessHttpRequest(httpRequest);
HttpContext context = createHttpContext(httpMethod, uri);
if (context == null) {
context = HttpClientContext.create();
}
// Request configuration not set in the context
if (context.getAttribute(HttpClientContext.REQUEST_CONFIG) == null) {
// Use request configuration given by the user, when available
RequestConfig config = null;
if (httpRequest instanceof Configurable) {
config = ((Configurable) httpRequest).getConfig();
}
if (config == null) {
config = createRequestConfig(client);
}
if (config != null) {
context.setAttribute(HttpClientContext.REQUEST_CONFIG, config);
}
}
if (this.bufferRequestBody) {
return new HttpComponentsClientHttpRequest(client, httpRequest, context);
}
else {
return new HttpComponentsStreamingClientHttpRequest(client, httpRequest, context);
}
}
RestTemplate与HttpClient的在生产环境使用的最佳实践
在构建HttpClient时,经常需要配置很多信息,例如RequestTimeout、ConnectTimeout、SocketTimeout、代理、是否允许重定向、连接池等信息。
在HttpClient,对这些参数进行配置需要使用到RequestConfig类的一个内部类Builder。
这里将这些常用的配置抽取出来放到配置文件中:
java
package com.morris.user.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HeaderElement;
import org.apache.http.HeaderElementIterator;
import org.apache.http.HttpHost;
import org.apache.http.client.HttpClient;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicHeaderElementIterator;
import org.apache.http.protocol.HTTP;
import org.apache.http.ssl.SSLContextBuilder;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@EnableConfigurationProperties(HttpClientConfig.class)
@ConditionalOnClass(RestTemplate.class)
@Configuration
@Slf4j
public class RestTemplateAutoConfiguration {
@Resource
private HttpClientConfig httpClientConfig;
@Bean
@ConditionalOnClass(CloseableHttpClient.class)
public RestTemplate httpClientRestTemplate(ClientHttpRequestFactory clientHttpRequestFactory){
return new RestTemplate(clientHttpRequestFactory);
}
@Bean
@ConditionalOnClass(CloseableHttpClient.class)
public ClientHttpRequestFactory clientHttpRequestFactory(HttpClient httpClient) {
HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory();
clientHttpRequestFactory.setHttpClient(httpClient);
clientHttpRequestFactory.setConnectTimeout(httpClientConfig.getRequest().getConnectTimeout());
clientHttpRequestFactory.setReadTimeout(httpClientConfig.getRequest().getReadTimeout());
clientHttpRequestFactory.setConnectionRequestTimeout(httpClientConfig.getRequest().getConnectionRequestTimeout());
clientHttpRequestFactory.setBufferRequestBody(httpClientConfig.getRequest().isBufferRequestBody());
return clientHttpRequestFactory;
}
@Bean
@Primary
@ConditionalOnClass(CloseableHttpClient.class)
public HttpClient httpClient() throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
try {
// 设置信任SSL访问
SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, (arg0, arg1) -> true).build();
httpClientBuilder.setSSLContext(sslContext);
// 任何主机都不会抛出SSLException异常
HostnameVerifier hostnameVerifier = NoopHostnameVerifier.INSTANCE;
SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, hostnameVerifier);
Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
// 注册HTTP和HTTPS请求
.register("http", PlainConnectionSocketFactory.getSocketFactory())
.register("https", sslConnectionSocketFactory).build();
// 使用Httpclient连接池的方式配置
PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
poolingHttpClientConnectionManager.setMaxTotal(httpClientConfig.getPool().getMaxTotalConnect());
poolingHttpClientConnectionManager.setDefaultMaxPerRoute(httpClientConfig.getPool().getMaxConnectPerRoute());
poolingHttpClientConnectionManager.setValidateAfterInactivity(httpClientConfig.getPool().getValidateAfterInactivity());
httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager);
httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(httpClientConfig.getPool().getRetryTimes(), true));
httpClientBuilder.setKeepAliveStrategy(connectionKeepAliveStrategy());
return httpClientBuilder.build();
} catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
log.error("初始化HTTP连接池出错", e);
throw e;
}
}
/**
* 配置长连接保持策略
* @return ConnectionKeepAliveStrategy
*/
public ConnectionKeepAliveStrategy connectionKeepAliveStrategy(){
return (response, context) -> {
// Honor 'keep-alive' header
HeaderElementIterator it = new BasicHeaderElementIterator(
response.headerIterator(HTTP.CONN_KEEP_ALIVE));
while (it.hasNext()) {
HeaderElement he = it.nextElement();
String param = he.getName();
String value = he.getValue();
if (value != null && "timeout".equalsIgnoreCase(param)) {
try {
return Long.parseLong(value) * 1000;
} catch(NumberFormatException error) {
log.error("解析长连接过期时间异常", error);
}
}
}
HttpHost target = (HttpHost) context.getAttribute(HttpClientContext.HTTP_TARGET_HOST);
//如果请求目标地址,单独配置了长连接保持时间,使用该配置
Optional<Map.Entry<String, Integer>> any = Optional.ofNullable(httpClientConfig.getPool().getKeepAliveTargetHost()).orElseGet(HashMap::new)
.entrySet().stream().filter(
e -> e.getKey().equalsIgnoreCase(target.getHostName())).findAny();
//否则使用默认长连接保持时间
return any.map(en -> en.getValue() * 1000L).orElse(httpClientConfig.getPool().getKeepAliveTime() * 1000L);
};
}
}