为什么一个@LoadBalanced注解就能让RestTemplate拥有负载均衡的能力?【享学Spring Cloud】

每篇一句

你应该思考:为何每每完成比完美更重要?html

前言

Spring Cloud微服务应用体系中,远程调用都应负载均衡。咱们在使用RestTemplate做为远程调用客户端的时候,开启负载均衡极其简单:一个@LoadBalanced注解就搞定了
相信你们大都使用过RibbonClient端的负载均衡,也许你有和我同样的感觉:Ribbon虽强大但不是特别的好用。我研究了一番,其实根源仍是咱们对它内部的原理不够了解,致使对一些现象没法给出合理解释,同时也影响了咱们对它的定制和扩展。本文就针对此作出梳理,但愿你们经过本文也可以对Ribbon有一个较为清晰的理解(本文只解释它@LoadBalanced这一小块内容)。java

开启客户端负载均衡只须要一个注解便可,形如这样:算法

@LoadBalanced // 标注此注解后,RestTemplate就具备了客户端负载均衡能力
@Bean
public RestTemplate restTemplate(){
    return new RestTemplate();
}

Spring是Java界最优秀、最杰出的重复发明轮子做品一点都不为过。本文就代领你一探究竟,为什么开启RestTemplate的负载均衡如此简单。spring

说明:本文创建在你已经熟练使用RestTemplate,而且了解RestTemplate它相关组件的原理的基础上分析。若对这部分还比较模糊,强行推荐你参看我前面这篇文章:RestTemplate的使用和原理你都烂熟于胸了吗?【享学Spring MVC】安全

RibbonAutoConfiguration

这是Spring Boot/Cloud启动Ribbon的入口自动配置类,须要先有个大概的了解:服务器

@Configuration
// 类路径存在com.netflix.client.IClient、RestTemplate等时生效
@Conditional(RibbonAutoConfiguration.RibbonClassesConditions.class) 
// // 容许在单个类中使用多个@RibbonClient
@RibbonClients 
// 如有Eureka,那就在Eureka配置好后再配置它~~~(若是是别的注册中心呢,ribbon还能玩吗?)
@AutoConfigureAfter(name = "org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration")
@AutoConfigureBefore({ LoadBalancerAutoConfiguration.class, AsyncLoadBalancerAutoConfiguration.class })
// 加载配置:ribbon.eager-load --> true的话,那么项目启动的时候就会把Client初始化好,避免第一次惩罚
@EnableConfigurationProperties({ RibbonEagerLoadProperties.class, ServerIntrospectorProperties.class })
public class RibbonAutoConfiguration {

	@Autowired
	private RibbonEagerLoadProperties ribbonEagerLoadProperties;
	// Ribbon的配置文件们~~~~~~~(复杂且重要)
	@Autowired(required = false)
	private List<RibbonClientSpecification> configurations = new ArrayList<>();

	// 特征,FeaturesEndpoint这个端点(`/actuator/features`)会使用它org.springframework.cloud.client.actuator.HasFeatures
	@Bean
	public HasFeatures ribbonFeature() {
		return HasFeatures.namedFeature("Ribbon", Ribbon.class);
	}


	// 它是最为重要的,是一个org.springframework.cloud.context.named.NamedContextFactory  此工厂用于建立命名的Spring容器
	// 这里传入配置文件,每一个不一样命名空间就会建立一个新的容器(和Feign特别像) 设置当前容器为父容器
	@Bean
	public SpringClientFactory springClientFactory() {
		SpringClientFactory factory = new SpringClientFactory();
		factory.setConfigurations(this.configurations);
		return factory;
	}

	// 这个Bean是关键,若你没定义,就用系统默认提供的Client了~~~
	// 内部使用和持有了SpringClientFactory。。。
	@Bean
	@ConditionalOnMissingBean(LoadBalancerClient.class)
	public LoadBalancerClient loadBalancerClient() {
		return new RibbonLoadBalancerClient(springClientFactory());
	}
	...
}

这个配置类最重要的是完成了Ribbon相关组件的自动配置,有了LoadBalancerClient才能作负载均衡(这里使用的是它的惟一实现类RibbonLoadBalancerClientapp


@LoadBalanced

注解自己及其简单(一个属性都木有):负载均衡

// 所在包是org.springframework.cloud.client.loadbalancer
// 能标注在字段、方法参数、方法上
// JavaDoc上说得很清楚:它只能标注在RestTemplate上才有效
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {
}

它最大的特色:头上标注有@Qualifier注解,这是它生效的最重要因素之一,本文后半啦我花了大篇幅介绍它的生效时机。
关于@LoadBalanced 自动生效的配置,咱们须要来到这个自动配置类:LoadBalancerAutoConfigurationide

LoadBalancerAutoConfiguration

// Auto-configuration for Ribbon (client-side load balancing).
// 它的负载均衡技术依赖于的是Ribbon组件~
// 它所在的包是:org.springframework.cloud.client.loadbalancer
@Configuration
@ConditionalOnClass(RestTemplate.class) //可见它只对RestTemplate生效
@ConditionalOnBean(LoadBalancerClient.class) // Spring容器内必须存在这个接口的Bean才会生效(参见:RibbonAutoConfiguration)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class) // retry的配置文件
public class LoadBalancerAutoConfiguration {
	
	// 拿到容器内全部的标注有@LoadBalanced注解的Bean们
	// 注意:必须标注有@LoadBalanced注解的才行
	@LoadBalanced
	@Autowired(required = false)
	private List<RestTemplate> restTemplates = Collections.emptyList();	
	// LoadBalancerRequestTransformer接口:容许使用者把request + ServiceInstance --> 改造一下
	// Spring内部默认是没有提供任何实现类的(匿名的都木有)
	@Autowired(required = false)
	private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();

	// 配置一个匿名的SmartInitializingSingleton 此接口咱们应该是熟悉的
	// 它的afterSingletonsInstantiated()方法会在全部的单例Bean初始化完成以后,再调用一个一个的处理BeanName~
	// 本处:使用配置好的全部的RestTemplateCustomizer定制器们,对全部的`RestTemplate`定制处理
	// RestTemplateCustomizer下面有个lambda的实现。若调用者有须要能够书写而后扔进容器里既生效
	// 这种定制器:若你项目中有多个RestTempalte,须要统一处理的话。写一个定制器是个不错的选择
	// (好比统一要放置一个请求拦截器:输出日志之类的)
	@Bean
	public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
		return () -> restTemplateCustomizers.ifAvailable(customizers -> {
			for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
				for (RestTemplateCustomizer customizer : customizers) {
					customizer.customize(restTemplate);
				}
			}
		});
	}
	
	// 这个工厂用于createRequest()建立出一个LoadBalancerRequest
	// 这个请求里面是包含LoadBalancerClient以及HttpRequest request的
	@Bean
	@ConditionalOnMissingBean
	public LoadBalancerRequestFactory loadBalancerRequestFactory(LoadBalancerClient loadBalancerClient) {
		return new LoadBalancerRequestFactory(loadBalancerClient, this.transformers);
	}
	
	// =========到目前为止还和负载均衡没啥关系==========
	// =========接下来的配置才和负载均衡有关(固然上面是基础项)==========

	// 如有Retry的包,就是另一份配置,和这差很少~~
	@Configuration
	@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
	static class LoadBalancerInterceptorConfig {、
	
		// 这个Bean的名称叫`loadBalancerClient`,我我的以为叫`loadBalancerInterceptor`更合适吧(虽然ribbon是惟一实现)
		// 这里直接使用的是requestFactory和Client构建一个拦截器对象
		// LoadBalancerInterceptor但是`ClientHttpRequestInterceptor`,它会介入到http.client里面去
		// LoadBalancerInterceptor也是实现负载均衡的入口,下面详解
		// Tips:这里可没有@ConditionalOnMissingBean哦~~~~
		@Bean
		public LoadBalancerInterceptor ribbonInterceptor(LoadBalancerClient loadBalancerClient, LoadBalancerRequestFactory requestFactory) {
			return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
		}
	
		
		// 向容器内放入一个RestTemplateCustomizer 定制器
		// 这个定制器的做用上面已经说了:在RestTemplate初始化完成后,应用此定制化器在**全部的实例上**
		// 这个匿名实现的逻辑超级简单:向全部的RestTemplate都塞入一个loadBalancerInterceptor 让其具有有负载均衡的能力
		
		// Tips:此处有注解@ConditionalOnMissingBean。也就是说若是调用者本身定义过RestTemplateCustomizer类型的Bean,此处是不会执行的
		// 请务必注意这点:容易让你的负载均衡不生效哦~~~~
		@Bean
		@ConditionalOnMissingBean
		public RestTemplateCustomizer restTemplateCustomizer(final LoadBalancerInterceptor loadBalancerInterceptor) {
			return restTemplate -> {
				List<ClientHttpRequestInterceptor> list = new ArrayList<>(restTemplate.getInterceptors());
				list.add(loadBalancerInterceptor);
				restTemplate.setInterceptors(list);
			};
		}
	}
	...
}

这段配置代码稍微有点长,我把流程总结为以下几步:函数

  1. LoadBalancerAutoConfiguration要想生效类路径必须有RestTemplate,以及Spring容器内必须有LoadBalancerClient的实现Bean
    1. LoadBalancerClient的惟一实现类是:org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient
  2. LoadBalancerInterceptor是个ClientHttpRequestInterceptor客户端请求拦截器。它的做用是在客户端发起请求以前拦截,进而实现客户端的负载均衡
  3. restTemplateCustomizer()返回的匿名定制器RestTemplateCustomizer它用来给全部的RestTemplate加上负载均衡拦截器(须要注意它的@ConditionalOnMissingBean注解~)

不难发现,负载均衡实现的核心就是一个拦截器,就是这个拦截器让一个普通的RestTemplate逆袭成为了一个具备负载均衡功能的请求器

LoadBalancerInterceptor

该类惟一被使用的地方就是LoadBalancerAutoConfiguration里配置上去~

public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {

	// 这个命名都不叫Client了,而叫loadBalancer~~~
	private LoadBalancerClient loadBalancer;
	// 用于构建出一个Request
	private LoadBalancerRequestFactory requestFactory;
	... // 省略构造函数(给这两个属性赋值)

	@Override
	public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException {
		final URI originalUri = request.getURI();
		String serviceName = originalUri.getHost();
		Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
		return this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));
	}
}

此拦截器拦截请求后把它的serviceName委托给了LoadBalancerClient去执行,根据ServiceName可能对应N多个实际的Server,所以就能够从众多的Server中运用均衡算法,挑选出一个最为合适的Server作最终的请求(它持有真正的请求执行器ClientHttpRequestExecution)。


LoadBalancerClient

请求被拦截后,最终都是委托给了LoadBalancerClient处理。

// 由使用负载平衡器选择要向其发送请求的服务器的类实现
public interface ServiceInstanceChooser {

	// 从负载平衡器中为指定的服务选择Service服务实例。
	// 也就是根据调用者传入的serviceId,负载均衡的选择出一个具体的实例出来
	ServiceInstance choose(String serviceId);
}

// 它本身定义了三个方法
public interface LoadBalancerClient extends ServiceInstanceChooser {
	
	// 执行请求
	<T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;
	<T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException;
	
	// 从新构造url:把url中原来写的服务名 换掉 换成实际的
	URI reconstructURI(ServiceInstance instance, URI original);
}

它只有一个实现类RibbonLoadBalancerClient ServiceInstanceChooser是有多个实现类的~)。

RibbonLoadBalancerClient

首先咱们应当关注它的choose()方法:

public class RibbonLoadBalancerClient implements LoadBalancerClient {
	
	@Override
	public ServiceInstance choose(String serviceId) {
		return choose(serviceId, null);
	}
	// hint:你能够理解成分组。若指定了,只会在这个偏好的分组里面去均衡选择
	// 获得一个Server后,使用RibbonServer把server适配起来~~~
	// 这样一个实例就选好了~~~真正请求会落在这个实例上~
	public ServiceInstance choose(String serviceId, Object hint) {
		Server server = getServer(getLoadBalancer(serviceId), hint);
		if (server == null) {
			return null;
		}
		return new RibbonServer(serviceId, server, isSecure(server, serviceId),
				serverIntrospector(serviceId).getMetadata(server));
	}

	// 根据ServiceId去找到一个属于它的负载均衡器
	protected ILoadBalancer getLoadBalancer(String serviceId) {
		return this.clientFactory.getLoadBalancer(serviceId);
	}

}

choose方法:传入serviceId,而后经过SpringClientFactory获取负载均衡器com.netflix.loadbalancer.ILoadBalancer,最终委托给它的chooseServer()方法选取到一个com.netflix.loadbalancer.Server实例,也就是说真正完成Server选取的是ILoadBalancer

ILoadBalancer以及它相关的类是一个较为庞大的体系,本文不作更多的展开,而是只聚焦在咱们的流程上

LoadBalancerInterceptor执行的时候是直接委托执行的loadBalancer.execute()这个方法:

RibbonLoadBalancerClient:

	// hint此处传值为null:一视同仁
	// 说明:LoadBalancerRequest是经过LoadBalancerRequestFactory.createRequest(request, body, execution)建立出来的
	// 它实现LoadBalancerRequest接口是用的一个匿名内部类,泛型类型是ClientHttpResponse
	// 由于最终执行的显然仍是执行器:ClientHttpRequestExecution.execute()
	@Override
	public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
		return execute(serviceId, request, null);
	}
	// public方法(非接口方法)
	public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint) throws IOException {
		// 同上:拿到负载均衡器,而后拿到一个serverInstance实例
		ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
		Server server = getServer(loadBalancer, hint);
		if (server == null) { // 若没找到就直接抛出异常。这里使用的是IllegalStateException这个异常
			throw new IllegalStateException("No instances available for " + serviceId);
		}

		// 把Server适配为RibbonServer  isSecure:客户端是否安全
		// serverIntrospector内省  参考配置文件:ServerIntrospectorProperties
		RibbonServer ribbonServer = new RibbonServer(serviceId, server,
				isSecure(server, serviceId), serverIntrospector(serviceId).getMetadata(server));

		//调用本类的重载接口方法~~~~~
		return execute(serviceId, ribbonServer, request);
	}

	// 接口方法:它的参数是ServiceInstance --> 已经肯定了惟一的Server实例~~~
	@Override
	public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException {
	
		// 拿到Server)(说白了,RibbonServer是execute时的惟一实现)
		Server server = null;
		if (serviceInstance instanceof RibbonServer) {
			server = ((RibbonServer) serviceInstance).getServer();
		}
		if (server == null) {
			throw new IllegalStateException("No instances available for " + serviceId);
		}

		// 说明:执行的上下文是和serviceId绑定的
		RibbonLoadBalancerContext context = this.clientFactory.getLoadBalancerContext(serviceId);
		... 
		// 真正的向server发送请求,获得返回值
		// 由于有拦截器,因此这里确定说执行的是InterceptingRequestExecution.execute()方法
		// so会调用ServiceRequestWrapper.getURI(),从而就会调用reconstructURI()方法
			T returnVal = request.apply(serviceInstance);
			return returnVal;
		... // 异常处理
	}

returnVal是一个ClientHttpResponse,最后交给handleResponse()方法来处理异常状况(若存在的话),若无异常就交给提取器提值:responseExtractor.extractData(response),这样整个请求就算所有完成了。

使用细节

针对@LoadBalanced下的RestTemplate的使用,我总结以下细节供以参考:

  1. 传入的String类型的url必须是绝对路径(http://...),不然抛出异常:java.lang.IllegalArgumentException: URI is not absolute
  2. serviceId不区分大小写(http://user/...效果同http://USER/...
  3. serviceId后请不要跟port端口号了~~~

最后,须要特别指出的是:标注有@LoadBalancedRestTemplate只能书写serviceId而不能再写IP地址/域名去发送请求了。若你的项目中两种case都有须要,请定义多个RestTemplate分别应对不一样的使用场景~

本地测试

了解了它的执行流程后,若须要本地测试(不依赖于注册中心),能够这么来作:

// 由于自动配置头上有@ConditionalOnMissingBean注解,因此自定义一个覆盖它的行为便可
// 此处复写它的getServer()方法,返回一个固定的(访问百度首页)便可,方便测试
@Bean
public LoadBalancerClient loadBalancerClient(SpringClientFactory factory) {
    return new RibbonLoadBalancerClient(factory) {
        @Override
        protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
            return new Server("www.baidu.com", 80);
        }
    };
}

这么一来,下面这个访问结果就是百度首页的html内容喽。

@Test
public void contextLoads() {
	String obj = restTemplate.getForObject("http://my-serviceId", String.class);
	System.out.println(obj);
}

此处my-serviceId确定是不存在的,但得益于我上面自定义配置的LoadBalancerClient

什么,写死return一个Server实例不优雅?确实,总不能每次上线前还把这部分代码给注释掉吧,如有多个实例呢?还得本身写负载均衡算法吗?很显然Spring Cloud早早就为咱们考虑到了这一点:脱离Eureka使用配置listOfServers进行客户端负载均衡调度(<clientName>.<nameSpace>.listOfServers=<comma delimited hostname:port strings>

对于上例我只须要在主配置文件里这么配置一下:

# ribbon.eureka.enabled=false # 若没用euraka,此配置可省略。不然不能够
my-serviceId.ribbon.listOfServers=www.baidu.com # 如有多个实例请用逗号分隔

效果彻底同上。

Tips:这种配置法不须要是完整的绝对路径,http://是能够省略的(new Server()方式亦可)

本身添加一个记录请求日志的拦截器可行吗?

显然是可行的,我给出示例以下:

@LoadBalanced
@Bean
public RestTemplate restTemplate() {
    RestTemplate restTemplate = new RestTemplate();
    List<ClientHttpRequestInterceptor> list = new ArrayList<>();
    list.add((request, body, execution) -> {
        System.out.println("当前请求的URL是:" + request.getURI().toString());
        return execution.execute(request, body);
    });
    restTemplate.setInterceptors(list);
    return restTemplate;
}

这样每次客户端的请求都会打印这句话:当前请求的URI是:http://my-serviceId,通常状况(缺省状况)自定义的拦截器都会在负载均衡拦截器前面执行(由于它要执行最终的请求)。若你有必要定义多个拦截器且要控制顺序,可经过Ordered系列接口来实现~


最后的最后,我抛出一个很是很是重要的问题:

@LoadBalanced
	@Autowired(required = false)
	private List<RestTemplate> restTemplates = Collections.emptyList();

@Autowired + @LoadBalanced能把你配置的RestTemplate自动注入进来拿来定制呢???核心原理是什么?

提示:本原理内容属于Spring Framwork核心技术,建议深刻思考而不囫囵吞枣。有疑问的能够给我留言,我也将会在下篇文章给出详细解答(建议先思考)


推荐阅读

RestTemplate的使用和原理你都烂熟于胸了吗?【享学Spring MVC】
@Qualifier高级应用---按类别批量依赖注入【享学Spring】

总结

本文以你们熟悉的@LoadBalancedRestTemplate为切入点介绍了Ribbon实现负载均衡的执行流程,固然此部分对Ribbon整个的核心负载体系知识来讲知识冰山一角,但它做为敲门砖仍是颇有意义的,但愿本文能勾起你对Ribbon体系的兴趣,深刻了解它~

== 若对Spring、SpringBoot、MyBatis等源码分析感兴趣,可加我wx:fsx641385712,手动邀请你入群一块儿飞 ==
== 若对Spring、SpringBoot、MyBatis等源码分析感兴趣,可加我wx:fsx641385712,手动邀请你入群一块儿飞 ==