RestTemplate入门和实战

1,857 阅读4分钟

最近在学习 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;
}