SpringCloud 源码系列(10)— 负载均衡Ribbon 之 HTTP客户端组件

2,178 阅读14分钟

系列文章:

SpringCloud 源码系列(1)— 注册中心Eureka 之 启动初始化

SpringCloud 源码系列(2)— 注册中心Eureka 之 服务注册、续约

SpringCloud 源码系列(3)— 注册中心Eureka 之 抓取注册表

SpringCloud 源码系列(4)— 注册中心Eureka 之 服务下线、故障、自我保护机制

SpringCloud 源码系列(5)— 注册中心Eureka 之 EurekaServer集群

SpringCloud 源码系列(6)— 注册中心Eureka 之 总结篇

SpringCloud 源码系列(7)— 负载均衡Ribbon 之 RestTemplate

SpringCloud 源码系列(8)— 负载均衡Ribbon 之 核心原理

SpringCloud 源码系列(9)— 负载均衡Ribbon 之 核心组件与配置

Java HTTP 组件库

1、HTTP 组件库

首先简单了解下常用的 Java HTTP 组件库,Ribbon 中通过不同的配置便可以启用某个 HTTP 组件来进行服务间的通信。

Java 中的 HTTP 组件库,大体可以分为三类:

  • JDK 自带的标准库 HttpURLConnection
  • Apache HttpComponents HttpClient
  • OkHttp

HttpURLConnection 发起 HTTP 请求最大的优点是不需要引入额外的依赖,但是 HttpURLConnection 封装层次太低,使用起来非常繁琐。支持的特性太少,缺乏连接池管理、域名机械控制,无法支持 HTTP/2 等。

Apache HttpComponents HttpClient 和 OkHttp 都支持连接池管理、超时、空闲连接数控制等特性。OkHttp 接口设计更友好,且支持 HTTP/2,Android 开发中用得更多。

2、超时重试配置

先给 demo-consumer 中添加如下默认配置,即读取、连接超时时间设置为 1 秒,这也是默认值。然后重试次数为1,重试一个Server。后面基于这些配置来验证Ribbon HTTP客户端的超时和重试。

ribbon:
  # 客户端读取超时时间
  ReadTimeout: 1000
  # 客户端连接超时时间
  ConnectTimeout: 1000
  # 默认只重试 GET,设置为 true 时将重试所有类型,如 POST、PUT、DELETE
  OkToRetryOnAllOperations: false
  # 重试次数
  MaxAutoRetries: 1
  # 最多重试几个实例
  MaxAutoRetriesNextServer: 1

然后 demo-producer 的接口休眠3秒,造成网络延迟的现象,并且 demo-producer 启两个实例。

@GetMapping("/v1/uuid")
public ResponseEntity<String> getUUID() throws InterruptedException {
    String uuid = UUID.randomUUID().toString();
    logger.info("generate uuid: {}", uuid);
    Thread.sleep(3000);
    return ResponseEntity.ok(uuid);
}

Ribbon 默认使用 HttpURLConnection

Ribbon 默认的 HTTP 组件

在不添加其它配置的情况下,我们来看下 Ribbon 默认使用的 HTTP 组件是什么。

首先通过之前的分析可以知道,默认情况下,LoadBalancerAutoConfiguration 配置类会向 RestTemplate 添加 LoadBalancerInterceptor 拦截器。然后在 RestTemplate 调用时,即在 doExecute 方法中,创建 ClientHttpRequest 时,因为配置了拦截器,所以 ClientHttpRequestFactory 就是 InterceptingClientHttpRequestFactory,而且创建 InterceptingClientHttpRequestFactory 传入的 ClientHttpRequestFactory 默认是父类的 SimpleClientHttpRequestFactory

protected <T> T doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback,
        @Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {
    //...
    ClientHttpResponse response = null;
    try {
        // 创建 ClientHttpRequest
        ClientHttpRequest request = createRequest(url, method);
        if (requestCallback != null) {
            requestCallback.doWithRequest(request);
        }
        // ClientHttpRequest 发起请求
        response = request.execute();
        handleResponse(url, method, response);
        return (responseExtractor != null ? responseExtractor.extractData(response) : null);
    }
}

protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOException {
    // getRequestFactory 获取 ClientHttpRequestFactory
    ClientHttpRequest request = getRequestFactory().createRequest(url, method);
    initialize(request);
    return request;
}

public ClientHttpRequestFactory getRequestFactory() {
    List<ClientHttpRequestInterceptor> interceptors = getInterceptors();
    if (!CollectionUtils.isEmpty(interceptors)) {
        ClientHttpRequestFactory factory = this.interceptingRequestFactory;
        if (factory == null) {
            // 有拦截器的情况: super.getRequestFactory() 默认返回的是 SimpleClientHttpRequestFactory
            factory = new InterceptingClientHttpRequestFactory(super.getRequestFactory(), interceptors);
            this.interceptingRequestFactory = factory;
        }
        return factory;
    }
    else {
        // 无拦截器的情况
        return super.getRequestFactory();
    }
}

InterceptingClientHttpRequestFactory 这个工厂类创建的 ClientHttpRequest 类型是 InterceptingClientHttpRequest。最终 RestTemplate 的 doExecute 方法中调用 ClientHttpRequest 的 execute 方法时,就调用到了 InterceptingClientHttpRequest 中的内部类 InterceptingRequestExecution 中。

InterceptingRequestExecution 的 execute 方法中,首先是遍历所有拦截器对 RestTemplate 定制化,最后则通过 requestFactory 创建 ClientHttpRequest 来发起最终的 HTTP 调用。从这里可以看出,无论有没有拦截器,其实最终都会使用 requestFactory 来创建 ClientHttpRequest。

private class InterceptingRequestExecution implements ClientHttpRequestExecution {
    private final Iterator<ClientHttpRequestInterceptor> iterator;

    @Override
    public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
        if (this.iterator.hasNext()) {
            // 拦截器定制化 RestTemplate
            ClientHttpRequestInterceptor nextInterceptor = this.iterator.next();
            return nextInterceptor.intercept(request, body, this);
        }
        else {
            HttpMethod method = request.getMethod();
            // delegate => SimpleBufferingClientHttpRequest
            ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), method);
            //...
            return delegate.execute();
        }
    }
}

这里的 requestFactory 就是前面传进来的 SimpleClientHttpRequestFactory,从它的 createRequest 方法可以看出,默认情况下,就是用的 JDK 标准 HTTP 库组件 HttpURLConnection 来进行服务间的请求通信。

public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
    // JDK 标准HTTP库 HttpURLConnection
    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);
    }
}

总结

  • 从前面的源码分析可以看出,Ribbon 默认的 HTTP 客户端是 HttpURLConnection。

  • 在前面默认的超时配置下,可以验证出超时配置并未生效,一直阻塞3秒后才返回了结果,说明 Ribbon 默认情况下就不支持超时重试。

  • HttpURLConnection 每次都是新创建的,请求返回来之后就关闭连接,没有连接池管理机制,网络连接的建立和关闭本身就会损耗一定的性能,所以正式环境下,最好不要使用默认的配置。

HttpClient 配置类

RibbonClientConfiguration 配置类的定义可以看到,其导入了 HttpClientConfiguration、OkHttpRibbonConfiguration、RestClientRibbonConfiguration、HttpClientRibbonConfiguration 四个 HttpClient 的配置类,通过注释也可以了解到,最后一个是默认配置类,前面三个在某些配置启用的情况下才会生效。

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties
// Order is important here, last should be the default, first should be optional
@Import({ HttpClientConfiguration.class, OkHttpRibbonConfiguration.class,
        RestClientRibbonConfiguration.class, HttpClientRibbonConfiguration.class })
public class RibbonClientConfiguration {

}

进入 HttpClientRibbonConfiguration,这个配置类在 ribbon.httpclient.enabled=true 时才生效,而且默认为 true。在从 SpringClientFactory 中获取 ILoadBalancer 时,会通过这个配置类初始化 HttpClient,按先后顺序会初始化 HttpClientConnectionManager、CloseableHttpClient、RibbonLoadBalancingHttpClient。CloseableHttpClient 是 Apache HttpComponents HttpClient 中的组件,也就是说默认情况下应该是使用 apache HttpComponents 作为 HTTP 组件库

但经过前面源码的分析,以及测试发现,最终其实走的的 HttpURLConnection,并没有用到 CloseableHttpClient。把 ribbon.httpclient.enabled 设置为 false,也没有什么影响,还是默认走 HttpURLConnection。我们后面再来分析这个问题。

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(name = "org.apache.http.client.HttpClient")
// ribbon.httpclient.enabled 默认为 true
@ConditionalOnProperty(name = "ribbon.httpclient.enabled", matchIfMissing = true)
public class HttpClientRibbonConfiguration {
    @RibbonClientName
    private String name = "client";

    // RibbonLoadBalancingHttpClient
    @Bean
    @ConditionalOnMissingBean(AbstractLoadBalancerAwareClient.class)
    @ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
    public RibbonLoadBalancingHttpClient ribbonLoadBalancingHttpClient(.....) {
        RibbonLoadBalancingHttpClient client = new RibbonLoadBalancingHttpClient(httpClient, config, serverIntrospector);
        return client;
    }

    // 在引入了 spring-retry 时,即可以重试的 RetryTemplate 时,就创建 RetryableRibbonLoadBalancingHttpClient
    @Bean
    @ConditionalOnMissingBean(AbstractLoadBalancerAwareClient.class)
    @ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate")
    public RetryableRibbonLoadBalancingHttpClient retryableRibbonLoadBalancingHttpClient(.....) {
        RetryableRibbonLoadBalancingHttpClient client = new RetryableRibbonLoadBalancingHttpClient(httpClient, config, serverIntrospector, loadBalancedRetryFactory);
        return client;
    }

    @Configuration(proxyBeanMethods = false)
    protected static class ApacheHttpClientConfiguration {
        private CloseableHttpClient httpClient;

        // HttpClient 连接池管理器
        @Bean
        @ConditionalOnMissingBean(HttpClientConnectionManager.class)
        public HttpClientConnectionManager httpClientConnectionManager(....) {
            //...
            final HttpClientConnectionManager connectionManager = connectionManagerFactory
                    .newConnectionManager(false, maxTotalConnections, maxConnectionsPerHost, timeToLive, ttlUnit, registryBuilder);
            return connectionManager;
        }

        // HttpClient => CloseableHttpClient
        @Bean
        @ConditionalOnMissingBean(CloseableHttpClient.class)
        public CloseableHttpClient httpClient(....) {
            //...
            this.httpClient = httpClientFactory.createBuilder()
                    .setDefaultRequestConfig(defaultRequestConfig)
                    .setConnectionManager(connectionManager).build();
            return httpClient;
        }
    }
}

一张图总结 RestTemplate 默认调用过程

从前面的分析,可以总结出默认配置下的 RestTemplate 的调用过程大致可以用下图来表示。

启用 RestClient

配置启用 RestClient

可以添加如下配置启用 RestClient:

ribbon:
  # 关闭 httpclient
  httpclient:
    enabled: false
  # 启用 RestClient
  restclient:
    enabled: true
  # 启用 RestClient
  http:
    client:
      enabled: true

进入 RestClientRibbonConfiguration 可以看到,只要 ribbon.http.client.enabledribbon.restclient.enabled 其中一个配置了启用,就可以启用 RestClient。

@SuppressWarnings("deprecation")
@Configuration(proxyBeanMethods = false)
// 启用条件 ConditionalOnRibbonRestClient
@RibbonAutoConfiguration.ConditionalOnRibbonRestClient
class RestClientRibbonConfiguration {
    @RibbonClientName
    private String name = "client";

    @Bean
    @Lazy
    @ConditionalOnMissingBean(AbstractLoadBalancerAwareClient.class)
    public RestClient ribbonRestClient(IClientConfig config, ILoadBalancer loadBalancer,
            ServerIntrospector serverIntrospector, RetryHandler retryHandler) {
        RestClient client = new RibbonClientConfiguration.OverrideRestClient(config, serverIntrospector);
        return client;
    }
}

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnRibbonRestClientCondition.class)
@interface ConditionalOnRibbonRestClient {
}

private static class OnRibbonRestClientCondition extends AnyNestedCondition {
    @Deprecated // remove in Edgware"
    @ConditionalOnProperty("ribbon.http.client.enabled")
    static class ZuulProperty {
    }

    @ConditionalOnProperty("ribbon.restclient.enabled")
    static class RibbonProperty {
    }
}

RestClient 继承自 AbstractLoadBalancerAwareClient。需要注意的是,RestClient 已经过期,所以生产环境中我们就不要启用 RestClient 了。

@Deprecated
public class RestClient extends AbstractLoadBalancerAwareClient<HttpRequest, HttpResponse> {

}

RestTemplate 配置 RestClient

1、LoadBalancerContext 类体系结构

负载均衡上下文 LoadBalancerContext 体系的类结构如下。可以看出,Ribbon 是支持 Feign、OkHttp、HttpClient、RestClient 的。默认配置下使用的实现类是 RibbonLoadBalancerContext

2、RestTemplate 的 ClientHttpRequest 工厂类配置

接着看 RibbonAutoConfiguration 中有如下的配置,跟前面 RestClientRibbonConfiguration 也是一样,满足 @ConditionalOnRibbonRestClient 的条件。

可以看到,它会创建 RibbonClientHttpRequestFactory 并设置到 RestTemplate 中,也就是说,这时 RestTemplate 中的 requestFactory 就不是默认的 SimpleClientHttpRequestFactory 了,而是 RibbonClientHttpRequestFactory

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(HttpRequest.class)
@ConditionalOnRibbonRestClient
protected static class RibbonClientHttpRequestFactoryConfiguration {
    @Autowired
    private SpringClientFactory springClientFactory;

    @Bean
    public RestTemplateCustomizer restTemplateCustomizer(
            final RibbonClientHttpRequestFactory ribbonClientHttpRequestFactory) {
        // RestTemplate 设置 requestFactory 为 RibbonClientHttpRequestFactory
        return restTemplate -> restTemplate
                .setRequestFactory(ribbonClientHttpRequestFactory);
    }

    // ClientHttpRequest 工厂类 => RibbonClientHttpRequestFactory
    @Bean
    public RibbonClientHttpRequestFactory ribbonClientHttpRequestFactory() {
        return new RibbonClientHttpRequestFactory(this.springClientFactory);
    }
}

而且,由于这里配置了 RestTemplateCustomizer,原本默认配置下,在 LoadBalancerAutoConfiguration 中创建 RestTemplateCustomizer 的方法就不会生效了。

LoadBalancerAutoConfiguration 中的 RestTemplateCustomizer 是向 RestTemplate 中添加 LoadBalancerInterceptor 拦截器,所以在启用了 RestClient 的情况下,原本的 LoadBalancerInterceptor 就不会生效了。

@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
static class LoadBalancerInterceptorConfig {

    @Bean
    public LoadBalancerInterceptor ribbonInterceptor(LoadBalancerClient loadBalancerClient, LoadBalancerRequestFactory requestFactory) {
        return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
    }

    @Bean
    @ConditionalOnMissingBean
    public RestTemplateCustomizer restTemplateCustomizer(final LoadBalancerInterceptor loadBalancerInterceptor) {
        return restTemplate -> {
            List<ClientHttpRequestInterceptor> list = new ArrayList<>(restTemplate.getInterceptors());
            list.add(loadBalancerInterceptor);
            restTemplate.setInterceptors(list);
        };
    }
}

那么 RestTemplate 的 doExecute 方法中,在调用 createRequest 方法创建 ClientHttpRequest 时,就会用 RibbonClientHttpRequestFactory 来创建,进去可以看到 ClientHttpRequest 的实际类型就是 RibbonHttpRequest

public ClientHttpRequest createRequest(URI originalUri, HttpMethod httpMethod) throws IOException {
    String serviceId = originalUri.getHost();
    
    IClientConfig clientConfig = this.clientFactory.getClientConfig(serviceId);
    RestClient client = this.clientFactory.getClient(serviceId, RestClient.class);
    HttpRequest.Verb verb = HttpRequest.Verb.valueOf(httpMethod.name());

    return new RibbonHttpRequest(originalUri, verb, client, clientConfig);
}

调用 RibbonHttpRequest 的 execute 方法,实际组中是调用了它的 executeInternal 方法,然后最后是使用 RestClient 来发起负载均衡的调用。

protected ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException {
    try {
        // ...
        HttpRequest request = builder.build();
        // client => RestClient
        HttpResponse response = client.executeWithLoadBalancer(request, config);
        return new RibbonHttpResponse(response);
    }
}

RestClient HTTP调用

RestClient 的 executeWithLoadBalancer 实际是进入到父类 AbstractLoadBalancerAwareClientexecuteWithLoadBalancer 方法中。

从这个方法可以知道,主要的负载均衡请求是在 LoadBalancerCommand 中的,LoadBalancerCommand 必定会通过负载均衡器 ILoadBalancer 得到一个 Server,然后通过 submit 的这个 ServerOperation 对原始URI进行重构,重构之后调用 RestClient 的 execute 发起HTTP请求。

public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
    // 负载均衡命令
    LoadBalancerCommand<T> command = buildLoadBalancerCommand(request, requestConfig);

    try {
        // 发起负载均衡请求
        return command.submit(
            new ServerOperation<T>() {
                @Override
                public Observable<T> call(Server server) {
                    // 重构 URI,将服务名用 Server 的 IP 和端口替换
                    URI finalUri = reconstructURIWithServer(server, request.getUri());
                    S requestForServer = (S) request.replaceUri(finalUri);
                    try {
                        // execute 发起调用,实际调用的是 RestClient 中的 execute
                        return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
                    }
                    catch (Exception e) {
                        return Observable.error(e);
                    }
                }
            })
            .toBlocking()
            .single();
    }
}

再看 RestClient 的 execute 方法,最终可以发现,RestClient 其实是使用基于 jerseyWebResource 来发起 HTTP 请求的。

private HttpResponse execute(HttpRequest.Verb verb, URI uri,
        Map<String, Collection<String>> headers, Map<String, Collection<String>> params,
        IClientConfig overriddenClientConfig, Object requestEntity) throws Exception {
    // ...
    // WebResource 是基于 jersey 封装的 HTTP 客户端组件
    WebResource xResource = restClient.resource(uri.toString());
    ClientResponse jerseyResponse;
    Builder b = xResource.getRequestBuilder();
    Object entity = requestEntity;

    switch (verb) {
    case GET:
        jerseyResponse = b.get(ClientResponse.class);
        break;
    case POST:
        jerseyResponse = b.post(ClientResponse.class, entity);
        break;
    case PUT:
        jerseyResponse = b.put(ClientResponse.class, entity);
        break;
    case DELETE:
        jerseyResponse = b.delete(ClientResponse.class);
        break;
    case HEAD:
        jerseyResponse = b.head();
        break;
    case OPTIONS:
        jerseyResponse = b.options(ClientResponse.class);
        break;
    default:
        throw new ClientException(ClientException.ErrorType.GENERAL, "You have to one of the REST verbs such as GET, POST etc.");
    }

    thisResponse = new HttpClientResponse(jerseyResponse, uri, overriddenClientConfig);
    if (thisResponse.getStatus() == 503){
        thisResponse.close();
        throw new ClientException(ClientException.ErrorType.SERVER_THROTTLED);
    }
    return thisResponse;
}

一张图总结 RestClient 调用流程

最后,RestTemplate 基于 RestClient 的请求流程可以用下图来做个总结。

HttpClient 或 OkHttp 对RestTemplate不生效 (官方BUG)

启用 HttpClient 不生效

默认情况下,ribbon.httpclient.enabled=true,在 HttpClientRibbonConfiguration 中会初始化 apache httpcomponents 相关的组件,前已经分析过了,但是在 RestTemplate 中并未使用相关的组件。

也就是说,默认情况下启用了 apache httpcomponents,但是 RestTemplate 最后是使用 HttpURLConnection 来发起 HTTP 请求的,而不是配置的 CloseableHttpClient

启用 OkHttp 不生效

首先需要加入 OkHttp 的依赖:

<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
</dependency>

然后添加如下配置就可以启用 OkHttp:

ribbon:
  httpclient:
    enabled: false
  # 启用 okhttp
  okhttp:
    enabled: true

配置 ribbon.okhttp.enabled=true 后,在 OkHttpRibbonConfiguration 中会初始化 OkHttp 相关的组件。

但是调试之后会发现,其实它还是走的默认的流程,就是最终用 HttpURLConnection 发起 HTTP 请求,跟 httpcomponents 是一样的效果。

@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty("ribbon.okhttp.enabled")
@ConditionalOnClass(name = "okhttp3.OkHttpClient")
public class OkHttpRibbonConfiguration {
	@RibbonClientName
	private String name = "client";

	@Bean
	@ConditionalOnMissingBean(AbstractLoadBalancerAwareClient.class)
	@ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate")
	public RetryableOkHttpLoadBalancingClient retryableOkHttpLoadBalancingClient(...) {
		RetryableOkHttpLoadBalancingClient client = new RetryableOkHttpLoadBalancingClient(
				delegate, config, serverIntrospector, loadBalancedRetryFactory);
		return client;
	}

	@Bean
	@ConditionalOnMissingBean(AbstractLoadBalancerAwareClient.class)
	@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
	public OkHttpLoadBalancingClient okHttpLoadBalancingClient(....) {
		OkHttpLoadBalancingClient client = new OkHttpLoadBalancingClient(delegate, config, serverIntrospector);
		return client;
	}

	@Configuration(proxyBeanMethods = false)
	protected static class OkHttpClientConfiguration {
		private OkHttpClient httpClient;

		@Bean
		@ConditionalOnMissingBean(ConnectionPool.class)
		public ConnectionPool httpClientConnectionPool(IClientConfig config, OkHttpClientConnectionPoolFactory connectionPoolFactory) {
			//...
			return connectionPoolFactory.create(maxTotalConnections, timeToLive, ttlUnit);
		}

		@Bean
		@ConditionalOnMissingBean(OkHttpClient.class)
		public OkHttpClient client(OkHttpClientFactory httpClientFactory, ConnectionPool connectionPool, IClientConfig config) {
			this.httpClient = httpClientFactory.createBuilder(false)
					.connectTimeout(ribbon.connectTimeout(), TimeUnit.MILLISECONDS)
					.readTimeout(ribbon.readTimeout(), TimeUnit.MILLISECONDS)
					.followRedirects(ribbon.isFollowRedirects())
					.connectionPool(connectionPool).build();
			return this.httpClient;
		}
	}
}

启用 HttpClient 或 OkHttp 不生效的原因分析

经过前面的分析,可以知道启用 apache httpcomponents 或者 OkHttp,对 RestTemplate 都没有起作用,最终还是用 HttpURLConnection 发起 HTTP 请求。那为什么为出现这种情况呢?我们可以看下 RestTemplate 的 setRequestFactory 方法。

通过 RestTemplate 的 setRequestFactory 方法的注释也可以了解到,默认的 requestFactory 是 SimpleClientHttpRequestFactory,它是基于 JDK 标准 HTTP 库的 HttpURLConnection

默认的 HttpURLConnection 不支持 PATCH,如果想支持,需设置为 Apache HttpComponentsOkHttp 的 requestFactory。

/**
 * Set the request factory that this accessor uses for obtaining client request handles.
 * <p>The default is a {@link SimpleClientHttpRequestFactory} based on the JDK's own
 * HTTP libraries ({@link java.net.HttpURLConnection}).
 * <p><b>Note that the standard JDK HTTP library does not support the HTTP PATCH method.
 * Configure the Apache HttpComponents or OkHttp request factory to enable PATCH.</b>
 * @see #createRequest(URI, HttpMethod)
 * @see SimpleClientHttpRequestFactory
 * @see org.springframework.http.client.HttpComponentsAsyncClientHttpRequestFactory
 * @see org.springframework.http.client.OkHttp3ClientHttpRequestFactory
 */
public void setRequestFactory(ClientHttpRequestFactory requestFactory) {
    Assert.notNull(requestFactory, "ClientHttpRequestFactory must not be null");
    this.requestFactory = requestFactory;
}

ClientHttpRequestFactory 体系类结构如下:

那 RestClient 又是如何生效的呢?通过上一节的分析可以知道,在 RibbonAutoConfiguration 中有如下的配置,这个 RibbonClientHttpRequestFactoryConfiguration 通过自定义 RestTemplateCustomizer 向 RestTemplate 设置了 requestFactory 为 RibbonClientHttpRequestFactory

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(HttpRequest.class)
@ConditionalOnRibbonRestClient
protected static class RibbonClientHttpRequestFactoryConfiguration {
    @Autowired
    private SpringClientFactory springClientFactory;

    @Bean
    public RestTemplateCustomizer restTemplateCustomizer(final RibbonClientHttpRequestFactory ribbonClientHttpRequestFactory) {
        return restTemplate -> restTemplate.setRequestFactory(ribbonClientHttpRequestFactory);
    }

    @Bean
    public RibbonClientHttpRequestFactory ribbonClientHttpRequestFactory() {
        return new RibbonClientHttpRequestFactory(this.springClientFactory);
    }
}

RibbonClientHttpRequestFactory 是对应 RestClient 的,也就是说要启用 OkHttp 或 HttpClient,还需自己创建对应的 ClientHttpRequestFactory,并设置给 RestTemplate。从上面的类结构可以看出,是提供了 HttpComponentsClientHttpRequestFactoryOkHttp3ClientHttpRequestFactory 工厂类了的。

这里其实就较奇怪,既然启用了 apache HttpClient 或者 OkHttp,却没有创建对应的 ClientHttpRequestFactory 实现类对象设置给 RestTemplate,感觉这是 spring-cloud-netflix-ribbon 的一个 BUG。

果然,在 github 上提 issues#3948 后得到证实,但作者说这不算一个bug,应该算作一个增强,而且 spring-cloud-netflix 已经进入维护模式了,Spring Cloud 团队也并不打算增加这个新特性了。

定制 RestTemplate 使用 Apache httpcomponents

既然官方不支持,那我们就自己实现来支持。如果想让 RestTemplate 使用 httpcomponents 的组件,就需要自己创建一个 ClientHttpRequestFactory,并设置给 RestTemplate。下面我们一步步来看看如何修复这个问题。

设置 HttpComponentsClientHttpRequestFactory

httpcomponents 中提供的 ClientHttpRequestFactory 实现类是 HttpComponentsClientHttpRequestFactory,但是并不能直接使用这个工厂类,因为它创建的 HttpComponentsClientHttpRequest 不具备重试的能力,它直接使用 CloseableHttpClient 执行请求,虽然有超时的功能,但不能重试。而且,它本质上也没有负载均衡的能力,需要借助 LoadBalancerInterceptor 拦截器来重构 URI。

final class HttpComponentsClientHttpRequest extends AbstractBufferingClientHttpRequest {
    private final HttpClient httpClient;
    private final HttpUriRequest httpRequest;
    private final HttpContext httpContext;

    @Override
    protected ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException {
        // ...
        // httpClient => CloseableHttpClient
        HttpResponse httpResponse = this.httpClient.execute(this.httpRequest, this.httpContext);
        return new HttpComponentsClientHttpResponse(httpResponse);
    }
}

所以,如果不需要重试的功能,可以直接创建一个 HttpComponentsClientHttpRequest,并设置给 RestTemplate 即可。这样就会使用 LoadBalancerInterceptor 来做负载均衡,重构 URI,然后用 HttpComponentsClientHttpRequest 来执行请求。

@Bean
@LoadBalanced
public RestTemplate restTemplate() {
    HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
    RestTemplate restTemplate = new RestTemplate();
    restTemplate.setRequestFactory(requestFactory);
    return restTemplate;
}

定制 Apache ClientHttpRequestFactory

如果想让 RestTemplate 即有负载均衡的能力,又能使用 apache HttpComponents 组件,且具备重试的功能,我们就需要自己定制 ClientHttpRequestFactory 了。关于重试后面再单独来讲。

对比 RestClient 可以发现,RibbonClientHttpRequestFactory 创建的 RibbonHttpRequest 其实是使用 RestClient 执行请求,而 RestClient 内部使用 LoadBalancerCommand 来进行重试。

类似的,我们至少要用上已经配置好的 RibbonLoadBalancingHttpClient 来执行请求,所以需要自定义一个类似的 RibbonHttpRequest 。

定制 apache ClientHttpRequest

创建 ApacheClientHttpRequest 继承自 RibbonHttpRequest,核心的点在于要注入 RibbonLoadBalancingHttpClient,如果要支持重试,需注入 RetryableRibbonLoadBalancingHttpClient。RetryableRibbonLoadBalancingHttpClient 在引入 spring-retry 后才会创建,这个后面分析重试时再看。

然后在 executeInternal 根据 retryable 判断,如果要重试,就调用 execute 方法,看 RetryableRibbonLoadBalancingHttpClient 的源码可以发现,它本身是支持负载均衡的,会自动选择 Server。

如果不需要重试,就需要调用 executeWithLoadBalancer,它是利用 LoadBalancerCommand 来提交请求,就跟 RestClient 是一样的了。但是不一样的地方是 RibbonLoadBalancingHttpClient 的 executeWithLoadBalancer 是不会进行重试的,这个也放到后面分析。

/**
 * Apache ClientHttpRequest
 *
 * @author bojiangzhou
 */
public class ApacheClientHttpRequest extends RibbonHttpRequest {

    private final URI uri;
    private final HttpMethod httpMethod;
    private final String serviceId;
    private final RibbonLoadBalancingHttpClient client;
    private final IClientConfig config;
    /**
     * 是否重试
     */
    private final boolean retryable;

    public ApacheClientHttpRequest(URI uri, HttpMethod httpMethod, String serviceId,
                                   RibbonLoadBalancingHttpClient client, // apache Ribbon负载均衡客户端
                                   IClientConfig config,
                                   boolean retryable // 是否重试
                                   ) {
        super(uri, null, null, config);
        this.uri = uri;
        this.httpMethod = httpMethod;
        this.serviceId = serviceId;
        this.client = client;
        this.config = config;
        this.retryable = retryable;
        if (retryable && !(client instanceof RetryableRibbonLoadBalancingHttpClient)) {
            throw new IllegalArgumentException("Retryable client must be RetryableRibbonLoadBalancingHttpClient");
        }
    }

    @Override
    protected ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException {
        try {
            RibbonApacheHttpRequest request = new RibbonApacheHttpRequest(buildCommandContext(headers));

            HttpResponse response;
            if (retryable) {
                // RetryableRibbonLoadBalancingHttpClient 使用 RetryTemplate 做负载均衡和重试
                response = client.execute(request, config);
            } else {
                // RibbonLoadBalancingHttpClient 需调用 executeWithLoadBalancer 才具备负载均衡的能力
                response = client.executeWithLoadBalancer(request, config);
            }

            return new RibbonHttpResponse(response);
        } catch (Exception e) {
            throw new IOException(e);
        }
    }

    protected RibbonCommandContext buildCommandContext(HttpHeaders headers) throws IOException {
        ByteArrayInputStream requestEntity = null;
        ByteArrayOutputStream bufferedOutput = (ByteArrayOutputStream) this.getBodyInternal(headers);
        if (bufferedOutput != null) {
            requestEntity = new ByteArrayInputStream(bufferedOutput.toByteArray());
            bufferedOutput.close();
        }

        return new RibbonCommandContext(serviceId, httpMethod.name(), uri.toString(), retryable,
                headers, new LinkedMultiValueMap<>(), requestEntity, new ArrayList<>());
    }
}

定制 apache ClientHttpRequestFactory

创建 ApacheClientHttpRequestFactory 继承自 HttpComponentsClientHttpRequestFactory,主要是在 createRequest 方法中创建自定义的 ApacheClientHttpRequest。RibbonLoadBalancingHttpClient 可以从 SpringClientFactory 中获取。

/**
 * Apache HttpComponents ClientHttpRequest factory
 *
 * @author bojiangzhou
 */
public class ApacheClientHttpRequestFactory extends HttpComponentsClientHttpRequestFactory {
    private final SpringClientFactory clientFactory;
    private final boolean retryable;

    public ApacheClientHttpRequestFactory(SpringClientFactory clientFactory, boolean retryable) {
        this.clientFactory = clientFactory;
        this.retryable = retryable;
    }

    @Override
    @NonNull
    public ClientHttpRequest createRequest(URI originalUri, HttpMethod httpMethod) throws IOException {
        String serviceId = originalUri.getHost();
        IClientConfig clientConfig = this.clientFactory.getClientConfig(serviceId);
        // 获取 apache Ribbon负载均衡客户端
        RibbonLoadBalancingHttpClient httpClient = this.clientFactory.getClient(serviceId, RibbonLoadBalancingHttpClient.class);
		// 创建自定义的 ApacheClientHttpRequest
        return new ApacheClientHttpRequest(originalUri, httpMethod, serviceId, httpClient, clientConfig, retryable);
    }
}

定制 apache ClientHttpRequestFactory 配置类

跟 RestClient 的配置类类似,定制 ApacheClientHttpRequestFactory 的配置类,同样的,默认启用 httpclient。在存在 RetryTemplate 时,就设置 ApacheClientHttpRequestFactory 的 retryable 参数为 true,否则为 false。

然后自定义 RestTemplateCustomizer,将 ApacheClientHttpRequestFactory 设置到 RestTemplate 中,注意这时 LoadBalancerInterceptor 就不会添加到 RestTemplate 中了。

/**
 *
 * @author bojiangzhou
 */
@Configuration
@ConditionalOnClass(RestTemplate.class)
@AutoConfigureAfter(name = "org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration")
@AutoConfigureBefore({LoadBalancerAutoConfiguration.class, AsyncLoadBalancerAutoConfiguration.class})
@ConditionalOnProperty(name = "ribbon.httpclient.restTemplate.enabled", matchIfMissing = true)
public class ApacheClientHttpRequestFactoryConfiguration {

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(HttpClient.class)
    @ConditionalOnProperty(name = "ribbon.httpclient.enabled", matchIfMissing = true)
    static class ClientHttpRequestFactoryConfiguration {
        @Autowired
        private SpringClientFactory springClientFactory;

        @Bean
        @ConditionalOnMissingBean
        public RestTemplateCustomizer restTemplateCustomizer(final ApacheClientHttpRequestFactory apacheClientHttpRequestFactory) {
            return restTemplate -> {
                // 设置 RequestFactory
                restTemplate.setRequestFactory(apacheClientHttpRequestFactory);

                // 添加移除 Content-Length 的拦截器,否则会报错
                ClientHttpRequestInterceptor removeHeaderLenInterceptor = (request, bytes, execution) -> {
                    request.getHeaders().remove(HTTP.CONTENT_LEN);
                    return execution.execute(request, bytes);
                };

                List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>(restTemplate.getInterceptors());
                // 添加移除Content-Length请求头的Interceptor
                interceptors.add(removeHeaderLenInterceptor);
                restTemplate.setInterceptors(interceptors);
            };
        }

        @Bean
        @ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
        public ApacheClientHttpRequestFactory apacheClientHttpRequestFactory() {
            return new ApacheClientHttpRequestFactory(springClientFactory, false);
        }

        // 支持重试
        @Bean
        @ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate")
        public ApacheClientHttpRequestFactory retryableApacheClientHttpRequestFactory() {
            return new ApacheClientHttpRequestFactory(springClientFactory, true);
        }
    }
}

简单调试下

配置好之后,把 demo-consumer 服务启动起来,简单测试下。

1、首先请求会进入到 RestTemplate 的 doExecute 中,然后通过 createRequest,调用 ApacheClientHttpRequestFactory 创建 ApacheClientHttpRequest。

2、接着调用 ApacheClientHttpRequest 的 execute 方法,在 ApacheClientHttpRequest 的 executeInternal 中,就会调用 RibbonLoadBalancingHttpClient 的 executeWithLoadBalancer 方法。

3、最后,进入 RibbonLoadBalancingHttpClient 的 execute 方法中,它又将请求转给了代理对象 delegate 来执行,delegate 就是在 HttpClientRibbonConfiguration 中配置的 CloseableHttpClient 对象,实际类型是 InternalHttpClient

经过验证,通过自定义的配置,最终使得 RestTemplate 可以使用 apache httpcomponents 组件来执行 HTTP 请求。重试那块后面再来研究。

一张图总结apache HttpClient 的调用流程

还是用一张图来总结下 RestTemplate 基于 apache HttpClient 后的执行流程

定制 RestTemplate 使用 OkHttp

设置 OkHttp3ClientHttpRequestFactory

类似的,可以给 RestTemplate 直接设置 OkHttp3ClientHttpRequestFactory,但它同样也不具备重试的能力。

@Bean
@LoadBalanced
public RestTemplate restTemplate() {
    OkHttp3ClientHttpRequestFactory requestFactory = new OkHttp3ClientHttpRequestFactory();
    RestTemplate restTemplate = new RestTemplate();
    restTemplate.setRequestFactory(requestFactory);
    return restTemplate;
}

定制 OkHttp ClientHttpRequestFactory

与定制 apache httpcomponents 类似,我这里就直接把三个类的代码放出来了。主要的差异就在于使用的 AbstractLoadBalancingClient 不同,apache 是 RibbonLoadBalancingHttpClient,okhttp 是 OkHttpLoadBalancingClient

1、OkHttpClientHttpRequest:

/**
 * OkHttp ClientHttpRequest
 *
 * @author bojiangzhou
 */
public class OkHttpClientHttpRequest extends RibbonHttpRequest {

    private final URI uri;
    private final HttpMethod httpMethod;
    private final String serviceId;
    private final OkHttpLoadBalancingClient client;
    private final IClientConfig config;
    /**
     * 是否重试
     */
    private final boolean retryable;

    public OkHttpClientHttpRequest(URI uri,
                                   HttpMethod httpMethod,
                                   String serviceId,
                                   OkHttpLoadBalancingClient client,
                                   IClientConfig config,
                                   boolean retryable) {
        super(uri, null, null, config);
        this.uri = uri;
        this.httpMethod = httpMethod;
        this.serviceId = serviceId;
        this.client = client;
        this.config = config;
        this.retryable = retryable;
        if (retryable && !(client instanceof RetryableOkHttpLoadBalancingClient)) {
            throw new IllegalArgumentException("Retryable client must be RetryableOkHttpLoadBalancingClient");
        }
    }

    @Override
    protected ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException {
        try {
            OkHttpRibbonRequest request = new OkHttpRibbonRequest(buildCommandContext(headers));

            HttpResponse response;
            if (retryable) {
                // RetryableRibbonLoadBalancingHttpClient 本身具备负载均衡的能力
                response = client.execute(request, config);
            } else {
                // RibbonLoadBalancingHttpClient 需调用 executeWithLoadBalancer 才具备负载均衡的能力
                response = client.executeWithLoadBalancer(request, config);
            }

            return new RibbonHttpResponse(response);
        } catch (Exception e) {
            throw new IOException(e);
        }
    }

    protected RibbonCommandContext buildCommandContext(HttpHeaders headers) throws IOException {
        ByteArrayInputStream requestEntity = null;
        ByteArrayOutputStream bufferedOutput = (ByteArrayOutputStream) this.getBodyInternal(headers);
        if (bufferedOutput != null) {
            requestEntity = new ByteArrayInputStream(bufferedOutput.toByteArray());
            bufferedOutput.close();
        }

        return new RibbonCommandContext(serviceId, httpMethod.name(), uri.toString(), retryable,
                headers, new LinkedMultiValueMap<>(), requestEntity, new ArrayList<>());
    }
}

2、OkHttpClientHttpRequestFactory:

/**
 * OkHttp ClientHttpRequest factory
 *
 * @author bojiangzhou
 */
public class OkHttpClientHttpRequestFactory extends OkHttp3ClientHttpRequestFactory {

    private final SpringClientFactory clientFactory;
    private final boolean retryable;

    public OkHttpClientHttpRequestFactory(SpringClientFactory clientFactory, boolean retryable) {
        this.clientFactory = clientFactory;
        this.retryable = retryable;
    }

    @Override
    @NonNull
    public ClientHttpRequest createRequest(URI originalUri, HttpMethod httpMethod) {
        String serviceId = originalUri.getHost();
        if (serviceId == null) {
            throw new IllegalStateException(
                    "Invalid hostname in the URI [" + originalUri.toASCIIString() + "]");
        }
        IClientConfig clientConfig = this.clientFactory.getClientConfig(serviceId);
        OkHttpLoadBalancingClient httpClient = this.clientFactory.getClient(serviceId, OkHttpLoadBalancingClient.class);

        return new OkHttpClientHttpRequest(originalUri, httpMethod, serviceId, httpClient, clientConfig, retryable);
    }
}

3、OkHttpClientHttpRequestFactoryConfiguration

/**
 *
 * @author bojiangzhou
 */
@Configuration
@ConditionalOnClass(RestTemplate.class)
@AutoConfigureAfter(name = "org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration")
@AutoConfigureBefore({LoadBalancerAutoConfiguration.class, AsyncLoadBalancerAutoConfiguration.class})
@ConditionalOnProperty(name = "ribbon.okhttp.restTemplate.enabled", matchIfMissing = true)
public class OkHttpClientHttpRequestFactoryConfiguration {

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnProperty("ribbon.okhttp.enabled")
    @ConditionalOnClass(name = "okhttp3.OkHttpClient")
    static class ClientHttpRequestFactoryConfiguration {

        @Autowired
        private SpringClientFactory springClientFactory;

        @Bean
        @ConditionalOnMissingBean
        public RestTemplateCustomizer restTemplateCustomizer(
                final OkHttpClientHttpRequestFactory okHttpClientHttpRequestFactory) {
            return restTemplate -> restTemplate
                    .setRequestFactory(okHttpClientHttpRequestFactory);
        }

        @Bean
        @ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
        public OkHttpClientHttpRequestFactory okHttpClientHttpRequestFactory() {
            return new OkHttpClientHttpRequestFactory(springClientFactory, false);
        }

        @Bean
        @ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate")
        public OkHttpClientHttpRequestFactory retryableOkHttpClientHttpRequestFactory() {
            return new OkHttpClientHttpRequestFactory(springClientFactory, true);
        }
    }
}