为何⼀个@LoadBalanced注解就能让RestTemplate拥有负载均
衡的能⼒?【。。。
每篇⼀句
你应该思考:为什么往往完成⽐完美更重要?
前⾔
在Spring Cloud微服务应⽤体系中,远程调⽤都应负载均衡。我们在使⽤RestTemplate作为远程调⽤客户端的时候,开启负载均衡极其简单:⼀个@LoadBalanced注解就搞定了。
相信⼤家⼤都使⽤过Ribbon做Client端的负载均衡,也许你有和我⼀样的感受:Ribbon虽强⼤但不是特别的好⽤。我研究了⼀番,其实根源还是我们对它内部的原理不够了解,导致对⼀些现象⽆法给出合理解释,同时也影响了我们对它的定制和扩展。本⽂就针对此做出梳理,希望⼤家通过本⽂也能够对Ribbon有⼀个较为清晰的理解(本⽂只解释它@LoadBalanced这⼀⼩块内容)。
开启客户端负载均衡只需要⼀个注解即可,形如这样:
@LoadBalanced // 标注此注解后,RestTemplate就具有了客户端负载均衡能⼒
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
说Spring是Java界最优秀、最杰出的重复发明轮⼦作品⼀点都不为过。本⽂就代领你⼀探究竟,为何开启RestTemplate的负载均衡如此简单。说明:本⽂建⽴在你已经熟练使⽤RestTemplate,并且了解RestTemplate它相关组件的原理的基础上分析。若对这部分还⽐较模糊,强⾏推荐你先参看我前⾯这篇⽂章:
RibbonAutoConfiguration
这是Spring Boot/Cloud启动Ribbon的⼊⼝⾃动配置类,需要先有个⼤概的了解:
@Configuration
// 类路径存在comflix.client.IClient、RestTemplate等时⽣效
@Conditional(RibbonAutoConfiguration.RibbonClassesConditions.class)
// // 允许在单个类中使⽤多个@RibbonClient
@RibbonClients
// 若有Eureka,那就在Eureka配置好后再配置它~~~(如果是别的注册中⼼呢,ribbon还能玩吗?)
@AutoConfigureAfter(name = "org.springframework.cloudflix.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.t.named.NamedContextFactory 此⼯⼚⽤于创建命名的Spring容器
// 这⾥传⼊配置⽂件,每个不同命名空间就会创建⼀个新的容器(和Feign特别像)设置当前容器为⽗容器
@Bean
public SpringClientFactory springClientFactory() {
SpringClientFactory factory = new SpringClientFactory();
factory.figurations);
return factory;
}
// 这个Bean是关键,若你没定义,就⽤系统默认提供的Client了~~~
// 内部使⽤和持有了SpringClientFactory。。。
@Bean
@ConditionalOnMissingBean(LoadBalancerClient.class)
public LoadBalancerClient loadBalancerClient() {
return new RibbonLoadBalancerClient(springClientFactory());
}
...
...
}
这个配置类最重要的是完成了Ribbon相关组件的⾃动配置,有了LoadBalancerClient才能做负载均衡(这⾥使⽤的是它的唯⼀实现类RibbonLoa dBalancerClient)
@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 ⾃动⽣效的配置,我们需要来到这个⾃动配置类:LoadBalancerAutoConfiguration
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 = ptyList();
// LoadBalancerRequestTransformer接⼝:允许使⽤者把request + ServiceInstance --> 改造⼀下
// Spring内部默认是没有提供任何实现类的(匿名的都⽊有)
@Autowired(required = false)
private List<LoadBalancerRequestTransformer> transformers = ptyList();
// 配置⼀个匿名的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 : stTemplates) {
for (RestTemplateCustomizer customizer : customizers) {
customizer.customize(restTemplate);
}
}
});
}
// 这个⼯⼚⽤于createRequest()创建出⼀个LoadBalancerRequest
// 这个请求⾥⾯是包含LoadBalancerClient以及HttpRequest request的
@Bean
@ConditionalOnMissingBean
public LoadBalancerRequestFactory loadBalancerRequestFactory(LoadBalancerClient loadBalancerClient) {
return new LoadBalancerRequestFactory(loadBalancerClient, ansformers);
}
// =========到⽬前为⽌还和负载均衡没啥关系==========
// =========接下来的配置才和负载均衡有关(当然上⾯是基础项)==========
// =========接下来的配置才和负载均衡有关(当然上⾯是基础项)==========
// 若有Retry的包,就是另外⼀份配置,和这差不多~~
@Configuration
@ConditionalOnMissingClass("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<>(Interceptors());
list.add(loadBalancerInterceptor);
restTemplate.setInterceptors(list);
};
}
}
...
}
这段配置代码稍微有点长,我把流程总结为如下⼏步:
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 LoadBalancer(serviceId);
}
}
choose⽅法:传⼊serviceId,然后通过SpringClientFactory获取负载均衡器comflix.loadbalancer.ILoadBalancer,最终委托给它的chooseServer()⽅法选取到⼀个comflix.loadbalancer.Server实例,也就是说真正完成Server选取的是ILoadBalancer。ILoadBalancer以及它相关的类是⼀个较为庞⼤的体系,本⽂不做更多的展开,⽽是只聚焦在我们的流程上springboot是啥
LoadBalancerInterceptor执⾏的时候是直接委托执⾏的ute()这个⽅法:
RibbonLoadBalancerClient:
// hint此处传值为null:⼀视同仁
// 说明:LoadBalancerRequest是通过ateRequest(request, body, execution)创建出来的
// 它实现LoadBalancerRequest接⼝是⽤的⼀个匿名内部类,泛型类型是ClientHttpResponse
// 因为最终执⾏的显然还是执⾏器:ute()
@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 = LoadBalancerContext(serviceId);
...
// 真正的向server发送请求,得到返回值
// 因为有,所以这⾥肯定说执⾏的是ute()⽅法
// so会调⽤URI(),从⽽就会调⽤reconstructURI()⽅法
T returnVal = request.apply(serviceInstance);
return returnVal;
.
.. // 异常处理
}
returnVal是⼀个ClientHttpResponse,最后交给handleResponse()⽅法来处理异常情况(若存在的话),若⽆异常就交给提取器提值:actData(response),这样整个请求就算全部完成了。
使⽤细节
针对@LoadBalanced下的RestTemplate的使⽤,我总结如下细节供以参考:
传⼊的String类型的url必须是绝对路径(...),否则抛出异常:java.lang.IllegalArgumentException: URI is not absolute
serviceId不区分⼤⼩写(user/...效果同USER/...)
serviceId后请不要跟port端⼝号了~~~
最后,需要特别指出的是:标注有@LoadBalanced的RestTemplate只能书写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", 80);
}
};
}
这么⼀来,下⾯这个访问结果就是百度⾸页的html内容喽。
@Test
public void contextLoads() {
String obj = ForObject("my-serviceId", String.class);
System.out.println(obj);
}
此处my-serviceId肯定是不存在的,但得益于我上⾯⾃定义配置的LoadBalancerClient
什么,写死return⼀个Server实例不优雅?确实,总不能每次上线前还把这部分代码给注释掉吧,若有
多个实例呢?还得⾃⼰写负载均衡算法吗?很显然Spring Cloud早早就为我们考虑到了这⼀点:脱离Eureka使⽤配置listOfServers进⾏客户端负载均衡调度(<clientName>.<nameSpa
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论