前言
上一篇文章中我们通过自己开发了一个负载均衡组件,实现了随机算法的负载均衡功能,如果要实现其他算法,还需要修改代码增加相应的功能。这一篇文章,我们将介绍一个更简单的负载均衡实现,使用**@LoadBalanced**注解实现负载均衡的功能。
项目实战
创建项目
同样的,我们的项目现在依然有一个registry注册中心,一个provider服务提供者,接下来,我们再次修改一下consumer服务消费者的代码:
@EnableEurekaClient @SpringBootApplication @RestController public class ConsumerApplication { public static void main(String[] args) { SpringApplication.run(ConsumerApplication.class, args); } @Bean @LoadBalanced RestTemplate restTemplate() { return new RestTemplate(); } @Autowired DiscoveryClient discoveryClient; @Autowired RestTemplate restTemplate; @GetMapping("/hello2") public String hello2(String name) { String returnInfo = restTemplate.getForObject( "http://provider/hello?name={1}", String.class, name); return returnInfo; } }
在这个版本,同样的,还是创建RestTemplate Bean对象,不同的是上面仅仅增加了@LoadBalanced注解。
启动项目验证
依然正确返回了结果!
太神奇了吧,比我们自己开发的负载均衡组件简单太多了吧,仅仅在restTemplate() 方法上面增加了一个@LoadBalanced注解,怎么就实现的呢?废话不说,为了一探究竟,扒一扒源码吧!
源码分析
首先,点击@LoadBalanced注解进去,没有什么特别之处,那么我们在想想,Spring在创建Bean实例的时候,注解在什么地方起了作用?什么?不知道?翻一下这篇文章吧:
肝了两周,一张图解锁Spring核心源码
通过回顾Spring启动以及Bean的生命周期创建过程,我们就会发现加上@LoadBalancer注解后,项目启动时就会加载LoadBalancerAutoConfiguration这个配置类(通过spring-cloud-commons包下面的的spring.factories)。通过查看该配置类源码,发现其有个静态内部类LoadBalancerInterceptorConfig,其内部又创建了一个负载均衡拦截器:LoadBalancerInterceptor,该拦截器包含有一个loadBalancerClient参数:
@ConditionalOnMissingClass({"org.springframework.retry.support.RetryTemplate"}) static class LoadBalancerInterceptorConfig { LoadBalancerInterceptorConfig() { } @Bean public LoadBalancerInterceptor ribbonInterceptor(LoadBalancerClient loadBalancerClient, LoadBalancerRequestFactory requestFactory) { return new LoadBalancerInterceptor(loadBalancerClient, requestFactory); } @Bean @ConditionalOnMissingBean public RestTemplateCustomizer restTemplateCustomizer(final LoadBalancerInterceptor loadBalancerInterceptor) { return (restTemplate) -> { Listlist = new ArrayList(restTemplate.getInterceptors()); list.add(loadBalancerInterceptor); restTemplate.setInterceptors(list); }; } }
我们继续点击LoadBalancerInterceptor类进入,发现intercept方法,该方法中调用了LoadBalancerClient的execute方法,
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException { URI originalUri = request.getURI(); String serviceName = originalUri.getHost(); Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri); return (ClientHttpResponse)this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution)); }
LoadBalancerClient是一个接口,点击进去我们发现其实现类是RibbonLoadBalancerClient,查看其继承关系:
通过接口中的方法名称,我们可以猜想,choose方法就是选择其中服务列表中其中一个服务,reconstructURI方法就是重新构造请求的URI。
选择服务
choose方法是在RibbonLoadBalancerClient实现类中实现的
public ServiceInstance choose(String serviceId, Object hint) { Server server = this.getServer(this.getLoadBalancer(serviceId), hint); return server == null ? null : new RibbonLoadBalancerClient.RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server)); }
在这个方法中,先调用 getServer 方法获取服务,这个方法最终会调用 ILoadBalancer 接口的 chooseServer 方法,而 ILoadBalancer 接口的实现类默认是ZoneAwareLoadBalancer。
ZoneAwareLoadBalancer 继承自 DynamicServerListLoadBalancer ,而在 DynamicServerListLoadBalancer 的构造方法中,调用了 this.restOfInit(clientConfig);在restOfInit这个方法中,通过 this.updateListOfServers()来获取服务列表;
而在chooseServer ()方法中,就会根据负载均衡算法,选择其中一个服务并返回:
BaseLoadBalancer zoneLoadBalancer = this.getLoadBalancer(zone); server = zoneLoadBalancer.chooseServer(key);
地址替换
选择其中一个服务信息后,怎么将接口从 http://provider/hello 变为 http://localhost:8003/hello 呢?还记得上面我们说的reconstructURI方法吗?通过配置类LoadBalancerAutoConfiguration加载后,会注入LoadBalancerInterceptor拦截器,该拦截器会拦截我们的请求,并对请求地址进行处理,重构方法的具体实现在 LoadBalancerContext 类的 reconstructURIWithServer 方法中
public URI reconstructURIWithServer(Server server, URI original) { String host = server.getHost(); int port = server.getPort(); String scheme = server.getScheme(); if (host.equals(original.getHost()) && port == original.getPort() && scheme == original.getScheme()) { return original; } else { if (scheme == null) { scheme = original.getScheme(); } if (scheme == null) { scheme = (String)this.deriveSchemeAndPortFromPartialUri(original).first(); } try { StringBuilder sb = new StringBuilder(); sb.append(scheme).append("://"); if (!Strings.isNullOrEmpty(original.getRawUserInfo())) { sb.append(original.getRawUserInfo()).append("@"); } sb.append(host); if (port >= 0) { sb.append(":").append(port); } sb.append(original.getRawPath()); if (!Strings.isNullOrEmpty(original.getRawQuery())) { sb.append("?").append(original.getRawQuery()); } if (!Strings.isNullOrEmpty(original.getRawFragment())) { sb.append("#").append(original.getRawFragment()); } URI newURI = new URI(sb.toString()); return newURI; } catch (URISyntaxException var8) { throw new RuntimeException(var8); } } }
可以看到该方法中,将原始的请求地址original,替换成了选取的服务的IP和端口。并最终调用该服务的接口方法。
看到这里,再想想我们上一章的内容,是不是有异曲同工之妙?
总结
通过添加@LoadBalanced注解,就及其简单的实现了负载均衡的功能,与其说是Ribbon的强大,不如说是Spring的强大,Spring在整个上下文创建过程中,在不同的时机开放了一个又一个的接口,这就为各种组件的继承提供了遍历,同时也进一步促进了Spring生态的快速发展。
到此这篇关于SpringCloud超详细讲解负载均衡组件的文章就介绍到这了,更多相关SpringCloud负载均衡组件内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!