最近在学习 SpringCloud 的源码,发现 Ribbon 使用了 RestTemplate,因此花了点时间研究了下其源码;顺带着把 HttpClient 的源码也系统地梳理了一下。作为大致的了解。
RestTemplate请求流程
默认情况下,RestTemplate 使用 HttpURLConnection 发起 HTTP 请求。每次请求都需要通过 socket 进行三次握手建立连接,往往效率不高,为了保证更低的延迟我们可以使用 http-client 来发起 HTTP 请求。
RestTemplate使用http-client连接池
// 配置类
@Data
@ConfigurationProperties(prefix = "spring.http.client")
public class HttpClientProperties {
private static final Long DEFAULT_TIME_ALIVE = 60L;
private Integer maxTotal;
private Integer maxPerRoute;
private Long timeAlive = DEFAULT_TIME_ALIVE;
private Boolean evictExpiredConnections;
private Long evictIdleConnections;
private Integer connectionRequestTimeout;
private Integer connectionTimeout;
private Integer readTimeout;
}
// 配置由连接池管理的 HttpClient
@Configuration
@EnableConfigurationProperties(HttpClientProperties.class)
public class HttpClientConfig {
@Autowired
private HttpClientProperties properties;
@Bean
public HttpClient httpClient() {
PoolingHttpClientConnectionManager connectionManager;
if (Objects.nonNull(properties.getTimeAlive())) {
connectionManager =
new PoolingHttpClientConnectionManager(properties.getTimeAlive(), TimeUnit.SECONDS);
} else {
connectionManager = new PoolingHttpClientConnectionManager();
}
Optional.ofNullable(properties.getMaxTotal())
.ifPresent(connectionManager::setMaxTotal);
Optional.ofNullable(properties.getMaxPerRoute())
.ifPresent(connectionManager::setDefaultMaxPerRoute);
HttpClientBuilder httpClientBuilder = HttpClients.custom().setConnectionManager(connectionManager);
if (Objects.equals(properties.getEvictExpiredConnections(), Boolean.TRUE)) {
httpClientBuilder.evictExpiredConnections();
}
if (Objects.nonNull(properties.getEvictIdleConnections())) {
httpClientBuilder.evictIdleConnections(properties.getEvictIdleConnections().intValue(), TimeUnit.SECONDS);
}
return httpClientBuilder.build();
}
}
// 配置 RestTemplate
@Configuration
public class RestTemplateConfig {
@Autowired
private HttpClientProperties httpClientProperties;
@Bean
public RestTemplate httpClientRestTemplate(HttpClient httpClient,
ObjectProvider<List<ClientHttpRequestInterceptor>> interceptors) {
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
Optional.ofNullable(httpClientProperties.getConnectionRequestTimeout())
.ifPresent(requestFactory::setConnectionRequestTimeout);
Optional.ofNullable(httpClientProperties.getConnectionTimeout())
.ifPresent(requestFactory::setConnectTimeout);
Optional.ofNullable(httpClientProperties.getReadTimeout())
.ifPresent(requestFactory::setReadTimeout);
RestTemplate restTemplate = new RestTemplate(requestFactory);
List<ClientHttpRequestInterceptor> ifUnique = interceptors.getIfUnique();
if (!CollectionUtils.isEmpty(ifUnique)) {
restTemplate.setInterceptors(ifUnique);
}
return restTemplate;
}
}
http-client解析
高级用法包含几个主题:
- 连接池化
- 重试策略
- 保持连接状态策略
连接池化
为什么需要实现连接池化呢?
从一台主机到另一台主机建立连接的过程非常复杂,涉及到两个端点之间的多个包交换,这可能非常耗时。连接握手的开销可能很大,特别是对于较小的HTTP消息。如果可以重用打开的连接来执行多个请求,则可以实现更高的数据吞吐量。
HTTP1.1之后允许重用 TCP 连接来发起请求,即对同一个域名或者 IP+PORT 的请求,无需再重新进行三次握手连接。
连接池结构
连接池由 CPool 实现,其内部拥有三个字段分别用于保存:被租借的连接、池中可用连接、获取中连接
private final Set<E> leased;
private final LinkedList<E> available;
private final LinkedList<Future<E>> pending;
并且为每个路由保存了一个 RouteSpecificPool ,表示每个路由的连接池
private final Map<T, RouteSpecificPool<T, C, E>> routeToPool;
RouteSpecificPool 中也包含了该路由本身的:被租借连接、池中可用连接、获取中的连接
private final Set<E> leased;
private final LinkedList<E> available;
private final LinkedList<Future<E>> pending;
可见,CPool 的结构如下
获取连接流程
整个流程,最核心的是从连接池中租借连接的过程。
RestTemplate拦截器使用
通过拦截器,可以为每个请求进行统一操作,比如设置某个请求头。
拦截器由接口 ClientHttpRequestInterceptor 定义,可以实现该接口来自定义拦截器。
PS: 如果请求头设置中文的话,需要使用 URLEncoder 进行编码,在接收端需要使用 URLDecoder 进行解码
public class MyHeaderInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
request.getHeaders().add("my-header", URLEncoder.encode("我是一个同一个头信息", "utf-8"));
return execution.execute(request, body);
}
}
将拦截器设置到 RestTemplate 中
RestTemplate restTemplate = new RestTemplate();
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
interceptors.add(new MyHeaderInterceptor());
restTemplate.setInterceptors(interceptors);
拦截器的原理
protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOException {
// 获取请求工厂时,如果设置了拦截器那么将返回一个包装了原有 ClientHttpRequestFactory 的 InterceptingClientHttpRequestFactory(其本身也实现了接口 ClientHttpRequestFactory)
// 这里使用了装饰器模式,对接口实现类进行了可插拔的增强逻辑
ClientHttpRequest request = getRequestFactory().createRequest(url, method);
initialize(request);
if (logger.isDebugEnabled()) {
logger.debug("HTTP " + method.name() + " " + url);
}
return request;
}
public ClientHttpRequestFactory getRequestFactory() {
List<ClientHttpRequestInterceptor> interceptors = getInterceptors();
if (!CollectionUtils.isEmpty(interceptors)) {
ClientHttpRequestFactory factory = this.interceptingRequestFactory;
if (factory == null) {
factory = new InterceptingClientHttpRequestFactory(super.getRequestFactory(), interceptors);
this.interceptingRequestFactory = factory;
}
return factory;
}
else {
return super.getRequestFactory();
}
}
拦截器执行逻辑
class InterceptingClientHttpRequest extends AbstractBufferingClientHttpRequest {
private final ClientHttpRequestFactory requestFactory;
private final List<ClientHttpRequestInterceptor> interceptors;
private HttpMethod method;
private URI uri;
protected InterceptingClientHttpRequest(ClientHttpRequestFactory requestFactory,
List<ClientHttpRequestInterceptor> interceptors, URI uri, HttpMethod method) {
this.requestFactory = requestFactory;
this.interceptors = interceptors;
this.method = method;
this.uri = uri;
}
@Override
public HttpMethod getMethod() {
return this.method;
}
@Override
public String getMethodValue() {
return this.method.name();
}
@Override
public URI getURI() {
return this.uri;
}
@Override
protected final ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException {
InterceptingRequestExecution requestExecution = new InterceptingRequestExecution();
return requestExecution.execute(this, bufferedOutput);
}
// 内部类的方式,可以获取其外围类的拦截器列表字段,通过迭代器的方式,一个个地进行拦截
private class InterceptingRequestExecution implements ClientHttpRequestExecution {
private final Iterator<ClientHttpRequestInterceptor> iterator;
public InterceptingRequestExecution() {
this.iterator = interceptors.iterator();
}
@Override
public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
if (this.iterator.hasNext()) {
ClientHttpRequestInterceptor nextInterceptor = this.iterator.next();
return nextInterceptor.intercept(request, body, this);
}
else {
HttpMethod method = request.getMethod();
Assert.state(method != null, "No standard HTTP method");
ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), method);
request.getHeaders().forEach((key, value) -> delegate.getHeaders().addAll(key, value));
if (body.length > 0) {
if (delegate instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) delegate;
streamingOutputMessage.setBody(outputStream -> StreamUtils.copy(body, outputStream));
}
else {
StreamUtils.copy(body, delegate.getBody());
}
}
return delegate.execute();
}
}
}
}
RestTemplate重试策略
针对除了四种异常以外的 IOException,默认情况下会进行三次重试(只能是幂等方法,如 GET )
- InterruptedIOException
- UnknownHostException
- ConnectException
- SSLException
默认由 DefaultHttpRequestRetryHandler 实现
public boolean retryRequest(
final IOException exception,
final int executionCount,
final HttpContext context) {
Args.notNull(exception, "Exception parameter");
Args.notNull(context, "HTTP context");
if (executionCount > this.retryCount) {
// Do not retry if over max retry count
return false;
}
if (this.nonRetriableClasses.contains(exception.getClass())) {
return false;
}
for (final Class<? extends IOException> rejectException : this.nonRetriableClasses) {
if (rejectException.isInstance(exception)) {
return false;
}
}
final HttpClientContext clientContext = HttpClientContext.adapt(context);
final HttpRequest request = clientContext.getRequest();
if(requestIsAborted(request)){
return false;
}
if (handleAsIdempotent(request)) {
// Retry if the request is considered idempotent
return true;
}
if (!clientContext.isRequestSent() || this.requestSentRetryEnabled) {
// Retry if the request has not been sent fully or
// if it's OK to retry methods that have been sent
return true;
}
// otherwise do not retry
return false;
}
RestTemplate保持连接状态策略
某些服务器端为了节省资源,会定时关闭一些连接,并且不会通知到客户端。因此通过响应头信息 Keep-Alive 指定连接保持的时间。
当 http-client 获取到该值后,将设置连接保持的时间。默认由 DefaultConnectionKeepAliveStrategy 实现
public long getKeepAliveDuration(final HttpResponse response, final HttpContext context) {
Args.notNull(response, "HTTP response");
final HeaderElementIterator it = new BasicHeaderElementIterator(
response.headerIterator(HTTP.CONN_KEEP_ALIVE));
while (it.hasNext()) {
final HeaderElement he = it.nextElement();
final String param = he.getName();
final String value = he.getValue();
if (value != null && param.equalsIgnoreCase("timeout")) {
try {
return Long.parseLong(value) * 1000;
} catch(final NumberFormatException ignore) {
}
}
}
return -1;
}