Series of articles:
SpringCloud source series (1) – registry initialization for Eureka
SpringCloud source code series (2) – Registry Eureka service registration, renewal
SpringCloud source code series (3) – Registry Eureka crawl registry
SpringCloud source code series (4) – Registry Eureka service offline, failure, self-protection mechanism
SpringCloud source series (5) – Registry EurekaServer cluster for Eureka
SpringCloud source Code Series (6) – Summary of the Registry Eureka
SpringCloud source series (7) – load balancing Ribbon RestTemplate
SpringCloud source series (8) – load balancing Ribbon core principles
SpringCloud source series (9) – load balancing Ribbon core components and configuration
Java HTTP component library
HTTP component library
First, take a look at the common Java HTTP component library. In the Ribbon, you can enable an HTTP component to communicate between services through different configurations.
HTTP component libraries in Java can be broadly divided into three categories:
- JDK standard library HttpURLConnection
- Apache HttpComponents HttpClient
- OkHttp
The biggest advantage of HttpURLConnection is that it does not need to introduce additional dependencies. However, HttpURLConnection has a low encapsulation level and is cumbersome to use. Too few supported features, lack of connection pool management, mechanical domain name control, HTTP/2 support, etc.
Both Apache HttpComponents HttpClient and OkHttp support connection pool management, timeout, and idle connection count control. OkHttp has a more friendly interface design and supports HTTP/2, which Android developers use more often.
2. Configure timeout retry
Add the following default Settings to demo-Consumer: read and connect timeout is set to 1 second, which is also the default. Then retry a Server with 1 retries. The Ribbon HTTP client timeouts and retries are later validated based on these configurations.
ribbon:
Client read timeout
ReadTimeout: 1000
Client connection timeout
ConnectTimeout: 1000
If set to true, retry all types, such as POST, PUT, and DELETE
OkToRetryOnAllOperations: false
# retries
MaxAutoRetries: 1
Retry a maximum of several instances
MaxAutoRetriesNextServer: 1
Copy the code
The demo-producer interface then sleeps for three seconds, causing network latency, and the Demo-producer starts two instances.
@GetMapping("/v1/uuid")
public ResponseEntity<String> getUUID(a) throws InterruptedException {
String uuid = UUID.randomUUID().toString();
logger.info("generate uuid: {}", uuid);
Thread.sleep(3000);
return ResponseEntity.ok(uuid);
}
Copy the code
The Ribbon uses HttpURLConnection by default
The Ribbon default HTTP component
Without adding additional configuration, let’s take a look at what HTTP components the Ribbon uses by default.
First through the analysis can know before, by default, LoadBalancerAutoConfiguration configuration classes will be added to the RestTemplate LoadBalancerInterceptor interceptors. Then when you call the RestTemplate, in the doExecute method, when you create the ClientHttpRequest, because the interceptor is configured, So it is ClientHttpRequestFactory InterceptingClientHttpRequestFactory, And create InterceptingClientHttpRequestFactory incoming ClientHttpRequestFactory SimpleClientHttpRequestFactory default is the parent class.
protected <T> T doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback,
@Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {
/ /...
ClientHttpResponse response = null;
try {
/ / create ClientHttpRequest
ClientHttpRequest request = createRequest(url, method);
if(requestCallback ! =null) {
requestCallback.doWithRequest(request);
}
// ClientHttpRequest initiates a request
response = request.execute();
handleResponse(url, method, response);
return(responseExtractor ! =null ? responseExtractor.extractData(response) : null); }}protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOException {
// getRequestFactory gets ClientHttpRequestFactory
ClientHttpRequest request = getRequestFactory().createRequest(url, method);
initialize(request);
return request;
}
public ClientHttpRequestFactory getRequestFactory(a) {
List<ClientHttpRequestInterceptor> interceptors = getInterceptors();
if(! CollectionUtils.isEmpty(interceptors)) { ClientHttpRequestFactory factory =this.interceptingRequestFactory;
if (factory == null) {
/ / have the interceptor: super getRequestFactory () returns the SimpleClientHttpRequestFactory by default
factory = new InterceptingClientHttpRequestFactory(super.getRequestFactory(), interceptors);
this.interceptingRequestFactory = factory;
}
return factory;
}
else {
// There is no interceptor
return super.getRequestFactory(); }}Copy the code
InterceptingClientHttpRequestFactory founded the factory class is InterceptingClientHttpRequest ClientHttpRequest type. When the execute method of ClientHttpRequest is called in the doExecute method of the RestTemplate, Call the InterceptingRequestExecution InterceptingClientHttpRequest of inner class.
In InterceptingRequestExecution the execute method, the first is to iterate through all the interceptor for RestTemplate customization, Finally, a ClientHttpRequest is created through the requestFactory to initiate the final HTTP call. From here, you can see that with or without an interceptor, the ClientHttpRequest will eventually be created using the requestFactory.
private class InterceptingRequestExecution implements ClientHttpRequestExecution {
private final Iterator<ClientHttpRequestInterceptor> iterator;
@Override
public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
if (this.iterator.hasNext()) {
// Interceptor customizes the 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);
/ /...
returndelegate.execute(); }}}Copy the code
RequestFactory is in front of the incoming SimpleClientHttpRequestFactory, here can be seen from its createRequest method, by default, It uses the JDK standard HTTP library component HttpURLConnection to communicate requests between services.
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
// JDK standard HTTP library 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); }}Copy the code
Conclusion:
-
As you can see from the source code analysis above, the Ribbon default HTTP client is HttpURLConnection.
-
In the previous default timeout Settings, you can verify that the timeout Settings do not take effect. The Ribbon blocks for 3 seconds and then returns the result. This indicates that the Ribbon does not support timeout retries by default.
-
HttpURLConnection is newly created each time, and the connection is closed after the request is returned. There is no connection pool management mechanism, and the establishment and closure of network connections degrades performance. Therefore, it is best not to use the default configuration in a formal environment.
HttpClient configuration class
From RibbonClientConfiguration configuration class definition as you can see, Import the HttpClientConfiguration, OkHttpRibbonConfiguration, RestClientRibbonConfiguration, HttpClientRibbonConfiguration four The last of the HttpClient configuration classes is the default configuration class, and the first three only take effect if some configuration is enabled.
@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 {}Copy the code
Into HttpClientRibbonConfiguration, the configuration class in ribbon. Httpclient. Enabled = true to come into force, and the default is true. This configuration class initializes HttpClient when ILoadBalancer is fetched from SpringClientFactory, According to the order will be initialized HttpClientConnectionManager, CloseableHttpClient, RibbonLoadBalancingHttpClient. CloseableHttpClient is a component in Apache HttpComponents HttpClient, which means Apache HttpComponents should be used as the HTTP component library by default.
But after the previous source code analysis, as well as the test found that the final actually go HttpURLConnection, does not use CloseableHttpClient. The ribbon. Httpclient. Enabled set to false, also does not have what effect, or the default HttpURLConnection. We’ll talk about that later.
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(name = "org.apache.http.client.HttpClient")
/ / ribbon. Httpclient. Enabled by default is 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;
}
/ / in the spring - retry is introduced, which can be retried RetryTemplate, creates 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 connection pool manager
@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();
returnhttpClient; }}}Copy the code
A diagram summarizes the RestTemplate default call process
From the previous analysis, you can conclude that the call process of the RestTemplate in the default configuration can be roughly represented in the following figure.
To enable the RestClient
Enable RestClient
You can add the following configuration to enable RestClient:
Ribbon: # Close httpClient httpClient: enabled:falseRestClient: enabled:trueRestClient HTTP: client: enabled:true
Copy the code
Enter RestClientRibbonConfiguration as you can see, as long as the ribbon. HTTP. Client. Enabled, ribbon. The restclient. Enabled one configuration is enabled, You can enable RestClient.
@SuppressWarnings("deprecation")
@Configuration(proxyBeanMethods = false)
/ / ConditionalOnRibbonRestClient enabling conditions
@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);
returnclient; }}@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 {}}Copy the code
RestClient inherited from AbstractLoadBalancerAwareClient. Note that RestClient is out of date, so we do not want to enable RestClient in a production environment.
@Deprecated
public class RestClient extends AbstractLoadBalancerAwareClient<HttpRequest.HttpResponse> {}Copy the code
RestTemplate configuration RestClient
LoadBalancerContext class architecture
The class structure of the LoadBalancerContext architecture is as follows. You can see that the Ribbon supports Feign, OkHttp, HttpClient, and RestClient. The default configuration using the implementation class is RibbonLoadBalancerContext.
2. RestTemplate ClientHttpRequest factory class configuration
Then see RibbonAutoConfiguration has the following configuration, with the front RestClientRibbonConfiguration, too, meet @ ConditionalOnRibbonRestClient conditions.
As you can see, it creates RibbonClientHttpRequestFactory and set into the RestTemplate, that is to say, At this time of the RestTemplate requestFactory, it is not the default SimpleClientHttpRequestFactory, but 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 set for RibbonClientHttpRequestFactory
return restTemplate -> restTemplate
.setRequestFactory(ribbonClientHttpRequestFactory);
}
/ / ClientHttpRequest factory class = > RibbonClientHttpRequestFactory
@Bean
public RibbonClientHttpRequestFactory ribbonClientHttpRequestFactory(a) {
return new RibbonClientHttpRequestFactory(this.springClientFactory); }}Copy the code
Also, because here is configured with RestTemplateCustomizer, originally the default configuration, created in the LoadBalancerAutoConfiguration RestTemplateCustomizer method is not effective.
The RestTemplateCustomizer LoadBalancerAutoConfiguration is to add LoadBalancerInterceptor RestTemplate interceptors, Therefore, the original LoadBalancerInterceptor does not take effect when RestClient is enabled.
@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 = newArrayList<>(restTemplate.getInterceptors()); list.add(loadBalancerInterceptor); restTemplate.setInterceptors(list); }; }}Copy the code
So RestTemplate the doExecute method, when call createRequest method to create ClientHttpRequest RibbonClientHttpRequestFactory will use to create, If you go in, the actual type of ClientHttpRequest is 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);
}
Copy the code
The Execute method of the RibbonHttpRequest is called, the executeInternal method of the actual group is called, and finally the RestClient is used to initiate the load balancing call.
protected ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException {
try {
// ...
HttpRequest request = builder.build();
// client => RestClient
HttpResponse response = client.executeWithLoadBalancer(request, config);
return newRibbonHttpResponse(response); }}Copy the code
RestClient HTTP invocation
RestClient “is actually into the parent class executeWithLoadBalancer AbstractLoadBalancerAwareClient executeWithLoadBalancer method.
LoadBalancerCommand (ILoadBalancer, ILoadBalancer, ILoadBalancer, ILoadBalancer, ILoadBalancer) Then, the Original URI is reconstructed through the ServerOperation of Submit. After the reconstruction, the EXECUTE of RestClient is called to initiate the HTTP request.
public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
// Load balancing command
LoadBalancerCommand<T> command = buildLoadBalancerCommand(request, requestConfig);
try {
// Initiate a load balancing request
return command.submit(
new ServerOperation<T>() {
@Override
public Observable<T> call(Server server) {
// Refactor the URI to replace the service name with the Server IP address and port
URI finalUri = reconstructURIWithServer(server, request.getUri());
S requestForServer = (S) request.replaceUri(finalUri);
try {
// execute Invokes the execute of RestClient
return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
}
catch (Exception e) {
returnObservable.error(e); } } }) .toBlocking() .single(); }}Copy the code
If you look at the Execute method of RestClient, it turns out that RestClient actually uses jersey-based WebResource to initiate HTTP requests.
private HttpResponse execute(HttpRequest.Verb verb, URI uri, Map
> headers, Map
> params, IClientConfig overriddenClientConfig, Object requestEntity)
,>
,> throws Exception {
// ...
WebResource is a Jersey-based HTTP client component
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;
}
Copy the code
A diagram summarizes the RestClient call flow
Finally, the RestTemplate request flow based on RestClient can be summarized in the following figure.
HttpClient or OkHttp does not work on RestTemplate
Enabling HttpClient does not take effect
By default, the ribbon. Httpclient. Enabled = true, will be initialized in the HttpClientRibbonConfiguration apache httpcomponents related components, analysis has been made before, However, the related components are not used in the RestTemplate.
That is, Apache HttpComponents is enabled by default, but the RestTemplate ends up using HttpURLConnection to initiate HTTP requests instead of CloseableHttpClient as configured.
OkHttp does not take effect
First we need to add the OkHttp dependency:
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
</dependency>
Copy the code
Then add the following configuration to enable OkHttp:
ribbon:
httpclient:
enabled: false
# enable okhttp
okhttp:
enabled: true
Copy the code
After configuration ribbon. Okhttp. Enabled = true, will be initialized in the OkHttpRibbonConfiguration okhttp related components.
However, after debugging, you will find that it still follows the default flow, which is to end up with an HTTP request using HttpURLConnection, the same effect as 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; }}}Copy the code
Cause The HttpClient or OkHttp function does not take effect
As you can see from the previous analysis, neither Apache HttpComponents nor OkHttp has any effect on the RestTemplate, and HttpURLConnection is still used to initiate HTTP requests. So why is this happening? We can look at the setRequestFactory method of the RestTemplate.
By using the approach of RestTemplate setRequestFactory comments can also be learned that the default requestFactory SimpleClientHttpRequestFactory, It is an HttpURLConnection based on the JDK standard HTTP library.
The default HttpURLConnection does not support PATCH. If you want to support PATCH, set it to Apache HttpComponents or OkHttp 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;
}
Copy the code
The ClientHttpRequestFactory architecture class structure is as follows:
So how does RestClient work? As can be seen from the analysis in the previous section, RibbonAutoConfiguration has the following configuration: The RibbonClientHttpRequestFactoryConfiguration RestTemplate via the custom RestTemplateCustomizer requestFactory for setting 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(a) {
return new RibbonClientHttpRequestFactory(this.springClientFactory); }}Copy the code
RibbonClientHttpRequestFactory is corresponding to the RestClient, that is to say, to enable the OkHttp or HttpClient, still need to create your own corresponding ClientHttpRequestFactory, And set it to the RestTemplate. Can be seen from the upper class structure, is to provide the HttpComponentsClientHttpRequestFactory and OkHttp3ClientHttpRequestFactory factory class.
If apache HttpClient or OkHttp is enabled, the ClientHttpRequestFactory implementation object is not created for RestTemplate. It feels like a BUG in spring-Cloud-Netflix-Ribbon.
Sure enough, this was confirmed after issues#3948 was posted on github, but the author says it’s not a bug, it should count as an enhancement, and spring-cloud-netflix is already in maintenance mode, and the spring cloud team has no plans to add this new feature.
Customize the RestTemplate using Apache HttpComponents
Since it is not officially supported, we will implement it ourselves. If you want RestTemplate to use httpComponents’ components, you need to create a ClientHttpRequestFactory yourself and set it to the RestTemplate. Let’s take a step-by-step look at how to fix this problem.
Set the HttpComponentsClientHttpRequestFactory
Httpcomponents ClientHttpRequestFactory provided during the implementation class is HttpComponentsClientHttpRequestFactory, but does not use this factory directly, Because it creates HttpComponentsClientHttpRequest don’t have the ability to retry, it directly USES CloseableHttpClient execution request, although have the function of a timeout, but can’t try again. Also, it has no load balancing capability by nature and requires the LoadBalancerInterceptor interceptor to reconstruct the 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 newHttpComponentsClientHttpResponse(httpResponse); }}Copy the code
So, if you don’t need to retry function, can create a HttpComponentsClientHttpRequest directly, and set to the RestTemplate. This will use LoadBalancerInterceptor to do load balancing, refactoring URI, and then use HttpComponentsClientHttpRequest to execute the request.
@Bean
@LoadBalanced
public RestTemplate restTemplate(a) {
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(requestFactory);
return restTemplate;
}
Copy the code
Custom Apache ClientHttpRequestFactory
If you want the RestTemplate to have load-balancing capability, use apache HttpComponents, and retry capability, you need to customize the ClientHttpRequestFactory. I’ll talk about retries separately later.
Contrast the RestClient can be found that RibbonClientHttpRequestFactory create RibbonHttpRequest actually is to use RestClient execute the request, RestClient internally uses LoadBalancerCommand to retry.
Similar, at least we will use the configured RibbonLoadBalancingHttpClient to execute the request, so you need to customize a similar RibbonHttpRequest.
Custom apache ClientHttpRequest
Create ApacheClientHttpRequest inherited from RibbonHttpRequest, core point is to inject RibbonLoadBalancingHttpClient, if you want to retry support, Need to inject RetryableRibbonLoadBalancingHttpClient. RetryableRibbonLoadBalancingHttpClient will create after introducing spring – retry, retry to see behind the analysis.
Then in executeInternal according to retryable judgment, if you want to try again, just call the execute method, see RetryableRibbonLoadBalancingHttpClient source can be found, it is itself a support load balancing, The Server is automatically selected.
If you don’t need to retry, you need to call executeWithLoadBalancer, which uses LoadBalancerCommand to submit the request, just like RestClient. But not the same place is RibbonLoadBalancingHttpClient executeWithLoadBalancer will not retry, this also in the back analysis.
/**
* 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;
/** * Whether to retry */
private final boolean retryable;
public ApacheClientHttpRequest(URI uri, HttpMethod httpMethod, String serviceId, RibbonLoadBalancingHttpClient client, Apache Ribbon load balancing client IClientConfig config,booleanRetryable // Retry) {
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 && ! (clientinstanceof 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 use RetryTemplate to do load balancing and try again
response = client.execute(request, config);
} else {
/ / executeWithLoadBalancer RibbonLoadBalancingHttpClient need to call to have the ability to load balance
response = client.executeWithLoadBalancer(request, config);
}
return new RibbonHttpResponse(response);
} catch (Exception e) {
throw newIOException(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, newArrayList<>()); }}Copy the code
Custom apache ClientHttpRequestFactory
Create ApacheClientHttpRequestFactory inherited from HttpComponentsClientHttpRequestFactory, Basically, you create a custom ApacheClientHttpRequest in the createRequest method. RibbonLoadBalancingHttpClient can be obtained from the 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);
// Obtain the Apache Ribbon load balancing client
RibbonLoadBalancingHttpClient httpClient = this.clientFactory.getClient(serviceId, RibbonLoadBalancingHttpClient.class);
// Create a custom ApacheClientHttpRequest
return newApacheClientHttpRequest(originalUri, httpMethod, serviceId, httpClient, clientConfig, retryable); }}Copy the code
Customize the Apache ClientHttpRequestFactory configuration class
With the configuration of the RestClient class, custom ApacheClientHttpRequestFactory configuration class, likewise, enabled by default httpclient. In the presence of RetryTemplate, set ApacheClientHttpRequestFactory retryable parameter is true, false otherwise.
Then the custom RestTemplateCustomizer, set the ApacheClientHttpRequestFactory to the RestTemplate, Note that the LoadBalancerInterceptor is not added to the 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 -> {
/ / set RequestFactory
restTemplate.setRequestFactory(apacheClientHttpRequestFactory);
// Add an interceptor to remove Content-Length, otherwise an error will be reported
ClientHttpRequestInterceptor removeHeaderLenInterceptor = (request, bytes, execution) -> {
request.getHeaders().remove(HTTP.CONTENT_LEN);
return execution.execute(request, bytes);
};
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>(restTemplate.getInterceptors());
// Add the Interceptor that removes the content-length request header
interceptors.add(removeHeaderLenInterceptor);
restTemplate.setInterceptors(interceptors);
};
}
@Bean
@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
public ApacheClientHttpRequestFactory apacheClientHttpRequestFactory(a) {
return new ApacheClientHttpRequestFactory(springClientFactory, false);
}
// Retry is supported
@Bean
@ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate")
public ApacheClientHttpRequestFactory retryableApacheClientHttpRequestFactory(a) {
return new ApacheClientHttpRequestFactory(springClientFactory, true); }}}Copy the code
Simple debugging
Once configured, start the Demo-Consumer service for a quick test.
1, first of all requests will be entered the RestTemplate the doExecute, then through the createRequest, calls to create ApacheClientHttpRequest ApacheClientHttpRequestFactory.
2. Execute (ApacheClientHttpRequest) Is called RibbonLoadBalancingHttpClient executeWithLoadBalancer method.
3, finally, into RibbonLoadBalancingHttpClient the execute method, it will ask to the proxy object delegate to perform, The delegate is configured in HttpClientRibbonConfiguration CloseableHttpClient object, is the actual type InternalHttpClient.
After verification and custom configuration, the RestTemplate is finally enabled to perform HTTP requests using the Apache HttpComponents component. Try that one again and we’ll look at it later.
A diagram summarizes the apache HttpClient call flow
Here is a diagram to summarize the RestTemplate execution process based on Apache HttpClient
Customize the RestTemplate using OkHttp
Set the OkHttp3ClientHttpRequestFactory
Similar, can give the RestTemplate set OkHttp3ClientHttpRequestFactory directly, but it also does not have the ability to retry.
@Bean
@LoadBalanced
public RestTemplate restTemplate(a) {
OkHttp3ClientHttpRequestFactory requestFactory = new OkHttp3ClientHttpRequestFactory();
RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(requestFactory);
return restTemplate;
}
Copy the code
Custom OkHttp ClientHttpRequestFactory
Similar to customizing Apache HttpComponents, I’ve just put the code for the three classes out there. Main difference is that using AbstractLoadBalancingClient, apache is 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;
/** * Whether to retry */
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 && ! (clientinstanceof 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 itself have the ability to load balance
response = client.execute(request, config);
} else {
/ / executeWithLoadBalancer RibbonLoadBalancingHttpClient need to call to have the ability to load balance
response = client.executeWithLoadBalancer(request, config);
}
return new RibbonHttpResponse(response);
} catch (Exception e) {
throw newIOException(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, newArrayList<>()); }}Copy the code
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 newOkHttpClientHttpRequest(originalUri, httpMethod, serviceId, httpClient, clientConfig, retryable); }}Copy the code
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(a) {
return new OkHttpClientHttpRequestFactory(springClientFactory, false);
}
@Bean
@ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate")
public OkHttpClientHttpRequestFactory retryableOkHttpClientHttpRequestFactory(a) {
return new OkHttpClientHttpRequestFactory(springClientFactory, true); }}}Copy the code