RestTemplate集成Ribbon超时重试机制

简介

之前遇到配置ribbon的超时参数,发现无法让restTemplate生效,因此想要了解RestTemplate相关机制,以及与Ribbon集成时的相关逻辑;

RestTemplate类图

RestTemplate类图RestTemplate继承祖父类HttpAccessor的createRequest方法和父类InterceptingHttpAccessor的getRequestFactory方法;这两个方法决定了每一个Request的创建机制;

createRequest方法如下:

	protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOException {
		ClientHttpRequest request = getRequestFactory().createRequest(url, method);//由子类定义的RequestFactory来进行创建Request
		if (logger.isDebugEnabled()) {
			logger.debug("Created " + method.name() + " request for \"" + url + "\"");
		}
		return request;
	}

getRequestFactory方法如下:

	public ClientHttpRequestFactory getRequestFactory() {
		List<ClientHttpRequestInterceptor> interceptors = getInterceptors();
		if (!CollectionUtils.isEmpty(interceptors)) {//在有拦截器的情况下,使用InterceptingClientHttpRequestFactory
			ClientHttpRequestFactory factory = this.interceptingRequestFactory;
			if (factory == null) {//设置RequestFactory为InterceptingRequestFactory
				factory = new InterceptingClientHttpRequestFactory(super.getRequestFactory(), interceptors);
				this.interceptingRequestFactory = factory;
			}
			return factory;
		}
		else {
			return super.getRequestFactory();
		}
	}

RestTemplate调用时序图

大致时序

具体流程就是:

  • 1.RestTemplate根据传入的参数创建一个基础的HttpRequest;
  • 2.InterceptingClientHttpRequest利用内部的Interceptors对HttpRequest进行拦截处理,每个Interceptors都会传入InterceptingRequestExecution参数,来保证HttpRequest的最终处理;
  • 3.Interceptors处理完成后LoadBalancerClient的execute方法,在该方法中会回调InterceptingRequestExecution的execute方法来完成最后的HttpRequest的处理,源码如下:
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();
			}
		}
	}

Ribbon集成

根据RestTemplate的流程时序,Ribbon提供了两个对象来侵入RestTemplate:

  • 1.RibbonLoadBalancerClient,继承LoadBalancerClient,其execute()实现方法如下:
@Override
	public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException {
		Server server = null;
		if(serviceInstance instanceof RibbonServer) {
			server = ((RibbonServer)serviceInstance).getServer();
		}
		if (server == null) {
			throw new IllegalStateException("No instances available for " + serviceId);
		}

		RibbonLoadBalancerContext context = this.clientFactory
				.getLoadBalancerContext(serviceId);
		RibbonStatsRecorder statsRecorder = new RibbonStatsRecorder(context, server);

		try {
			T returnVal = request.apply(serviceInstance);//回调,来完成最后的HttpRequest请求构建
			statsRecorder.recordStats(returnVal);
			return returnVal;
		}
		// catch IOException and rethrow so RestTemplate behaves correctly
		catch (IOException ex) {
			statsRecorder.recordStats(ex);
			throw ex;
		}
		catch (Exception ex) {
			statsRecorder.recordStats(ex);
			ReflectionUtils.rethrowRuntimeException(ex);
		}
		return null;
	}

  • 2.RibbonLoadBalancedRetryPolicy:该对象继承LoadBalancedRetryPolicy,用来实现重试策略,通过配置servicename.ribbon.MaxAutoRetries等参数可以实现重试配置,该对象会
    配合RetryLoadBalancerInterceptor使用来是的RestTemplate请求能够实现重试,RetryLoadBalancerInterceptor的interceptor方法源码如下:
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
										final ClientHttpRequestExecution execution) throws IOException {
		final URI originalUri = request.getURI();
		final String serviceName = originalUri.getHost();
		Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
		final LoadBalancedRetryPolicy retryPolicy = lbRetryFactory.createRetryPolicy(serviceName,
				loadBalancer);
		RetryTemplate template = createRetryTemplate(serviceName, request, retryPolicy);//创建RetryTemplate来实现重试
		return template.execute(context -> {//进入重试调用链
			ServiceInstance serviceInstance = null;
			if (context instanceof LoadBalancedRetryContext) {
				LoadBalancedRetryContext lbContext = (LoadBalancedRetryContext) context;
				serviceInstance = lbContext.getServiceInstance();
			}
			if (serviceInstance == null) {
				serviceInstance = loadBalancer.choose(serviceName);
			}
			//在重试调用上下文中进行request的初始执行,由此处返回到InterceptingRequestExecution中执行request执行
			ClientHttpResponse response = RetryLoadBalancerInterceptor.this.loadBalancer.execute(
					serviceName, serviceInstance,
					requestFactory.createRequest(request, body, execution));
			int statusCode = response.getRawStatusCode();
			if (retryPolicy != null && retryPolicy.retryableStatusCode(statusCode)) {
				byte[] bodyCopy = StreamUtils.copyToByteArray(response.getBody());
				response.close();
				throw new ClientHttpResponseStatusCodeException(serviceName, response, bodyCopy);
			}
			return response;
		}, new LoadBalancedRecoveryCallback<ClientHttpResponse, ClientHttpResponse>() {
			//This is a special case, where both parameters to LoadBalancedRecoveryCallback are
			//the same.  In most cases they would be different.
			@Override
			protected ClientHttpResponse createResponse(ClientHttpResponse response, URI uri) {
				return response;
			}
		});
	}

Ribbon配置加载

Ribbon加载相关配置类有两个:

  • RibbonClientConfiguration:主要用来初始化Ribbon的DefaultClientConfigImpl,IRule,ILoadBalancer,RibbonLoadBalancerContext,RetryHandler等等;
  • LoadBalancerAutoConfiguration:该配置主要用来初始化
    LoadBalancer相关配置,如LoadBalancerInterceptor以及RestTemplateCustomizer来实现RestTemplate相关ClientHttpRequestInterceptor注入,另外还会加载
    RetryLoadBalancerInterceptor
    RetryLoadBalancerInterceptor必须在spring.cloud.loadbalancer.retry=true的情况下才会生效;

Ribbon配置的超时时间对RestTemplate不生效原因

这个可以看下RibbonClientConfiguration初始化IClientConfig源码,如下:

	@Bean
	@ConditionalOnMissingBean
	public IClientConfig ribbonClientConfig() {
		DefaultClientConfigImpl config = new DefaultClientConfigImpl();
		config.loadProperties(this.name);//加载配置
		config.set(CommonClientConfigKey.ConnectTimeout, DEFAULT_CONNECT_TIMEOUT);//设置默认连接时间
		config.set(CommonClientConfigKey.ReadTimeout, DEFAULT_READ_TIMEOUT);//设置默认请求时间;
		return config;
	}

这就导致了无论Ribbon的配置的超时参数是多少,这都会导致超时参数被覆盖;
但是这里并不是真正导致超时时间不生效的原因,而是SimpleClientHttpRequestFactory,因为SimpleClientHttpRequestFactory会在request数据完全准备好了之后进行Connection的初始化,可以查看SimpleClientHttpRequestFactory方法prepareConnection,源码如下:

protected void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException {
		if (this.connectTimeout >= 0) {
			connection.setConnectTimeout(this.connectTimeout);
		}
		if (this.readTimeout >= 0) {
			connection.setReadTimeout(this.readTimeout);
		}

		connection.setDoInput(true);

		if ("GET".equals(httpMethod)) {
			connection.setInstanceFollowRedirects(true);
		}
		else {
			connection.setInstanceFollowRedirects(false);
		}

		if ("POST".equals(httpMethod) || "PUT".equals(httpMethod) ||
				"PATCH".equals(httpMethod) || "DELETE".equals(httpMethod)) {
			connection.setDoOutput(true);
		}
		else {
			connection.setDoOutput(false);
		}

		connection.setRequestMethod(httpMethod);
	}

}

SimpleClientHttpRequestFactory中的connectionTimeout和ReadTimeout并不会被Ribbon中的参数覆盖,因此要解决超时配置问题就只能在代码层面进行配置,如下:

 S	@Bean
	@LoadBalanced//用于负载均衡的调用微服务实例,会将http://${spring.application.name}/path解析成ip+port的形式
	public RestTemplate getRibbonTemplate() {
		 SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();  
		 requestFactory.setConnectTimeout(10000);  
		 requestFactory.setReadTimeout(10000);  
		 
		 RestTemplate restTemplate = new RestTemplate(requestFactory);  
		 return restTemplate;
	}

版权声明:本文为heyiwuwu原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。