zoukankan      html  css  js  c++  java
  • 为何一个@LoadBalanced注解就能让RestTemplate拥有负载均衡的能力?

    每篇一句

    你应该思考:为什么往往完成比完美更重要?

    前言

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

    开启客户端负载均衡只需要一个注解即可,形如这样:

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

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

    说明:本文建立在你已经熟练使用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才能做负载均衡(这里使用的是它的唯一实现类RibbonLoadBalancerClient


    @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 = 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);
    }

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

    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】

    ------------------------------------------------------

    Ribbon是如何通过一个@LoadBalanced注解就实现负载均衡的

    一.介绍下测试用到的服务
    在这里插入图片描述
    从Eureka注册中心中可以可以看出有EUREKA-CLIENT和RIBBON-CLIENT的服务,其中EUREKA-CLIENT有两个节点作为服务提供者,而RIBBON-CLIENT则是服务消费者,通过RestTemplate来消费EUREKA-CLIENT的服务。

    下面代码就是简单实现Ribbon负载均衡的配置类:

    @Configuration
    public class RibbonConfig {
    
        @Bean
        @LoadBalanced
        RestTemplate getRestTemlate() {
            return new RestTemplate();
        }
    }
    

    这样简单的通过一个@LoadBalanced注解在RestTemplate上 ,在RestTemplate 远程调用的时候,就会出现负载均衡的效果。

    二.一步一步理清Ribbon负载均衡的逻辑

    1. 首先全局搜索@LoadBalanced这个注解,发现在LoadBalancerAutoConfiguration类有用到该注解:
    @Configuration
    @ConditionalOnClass(RestTemplate.class)
    @ConditionalOnBean(LoadBalancerClient.class)
    @EnableConfigurationProperties(LoadBalancerRetryProperties.class)
    public class LoadBalancerAutoConfiguration {
        
        /**
        *  这段代码的作用是将有用@LoadBalanced注解的RestTemplate注入
        */
    	@LoadBalanced
    	@Autowired(required = false)
    	private List<RestTemplate> restTemplates = Collections.emptyList();
    }
    

    分析以上代码:

    • 通过@Configuration表明这是一个配置类
    • 通过@ConditionalOnClass(RestTemplate.class)可以知道RestTemplate类要在类路径上存在才会实例化LoadBalancerAutoConfiguration
    • 通过@ConditionalOnBean(LoadBalancerClient.class)可以知道LoadBalancerClient类要存在才会实例化LoadBalancerAutoConfiguration
    • @EnableConfigurationProperties(LoadBalancerRetryProperties.class)是用来使用@ConfigurationProperties注解的类LoadBalancerRetryProperties生效,贴上部分LoadBalancerRetryProperties类的代码,会更清晰:
    @ConfigurationProperties("spring.cloud.loadbalancer.retry")
    public class LoadBalancerRetryProperties {
    
    	private boolean enabled = true;
    
    	/**
    	 * Returns true if the load balancer should retry failed requests.
    	 * @return True if the load balancer should retry failed requests; false otherwise.
    	 */
    	public boolean isEnabled() {
    		return this.enabled;
    	}
    
    1. 所以重启下RIBBON-CLIENT服务,Debug继续看LoadBalancerAutoConfiguration 类的代码,发现在启动时会先进入LoadBalancerAutoConfiguration 的loadBalancerRequestFactory方法,实例化出LoadBalancerRequestFactory
        @Bean
    	@ConditionalOnMissingBean
    	public LoadBalancerRequestFactory loadBalancerRequestFactory(
    			LoadBalancerClient loadBalancerClient) {
    		return new LoadBalancerRequestFactory(loadBalancerClient, this.transformers);
    	}
    

    接下去断点进入LoadBalancerAutoConfiguration 类中的静态内部类LoadBalancerInterceptorConfig的ribbonInterceptor方法,可以看出这是为了实例化出LoadBalancerInterceptor 拦截器

        @Configuration
    	@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
    	static class LoadBalancerInterceptorConfig {
    
    		@Bean
    		public LoadBalancerInterceptor ribbonInterceptor(
    				LoadBalancerClient loadBalancerClient,
    				LoadBalancerRequestFactory requestFactory) {
    			return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
    		}
    

    继续跟断点,进入了loadBalancedRestTemplateInitializerDeprecated方法,可以看出这个方法里主要的逻辑代码是customizer.customize(restTemplate)

        @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);
    				}
    			}
    		});
    	}
    

    继续Debug,断点进入LoadBalancerAutoConfiguration类中的静态内部类LoadBalancerInterceptorConfig:

    @Configuration
    @ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
    	static class LoadBalancerInterceptorConfig {
    		@Bean
    		@ConditionalOnMissingBean
    		public RestTemplateCustomizer restTemplateCustomizer(
    				final LoadBalancerInterceptor loadBalancerInterceptor) {
    			return restTemplate -> {
    				List<ClientHttpRequestInterceptor> list = new ArrayList<>(
    						restTemplate.getInterceptors());
    				list.add(loadBalancerInterceptor);
    				restTemplate.setInterceptors(list);
    			};
    		}
    	}
    

    通过 list.add(loadBalancerInterceptor)和restTemplate.setInterceptors(list)两段代码可以看出,这是要给restTemplate加上loadBalancerInterceptor拦截器。

    那么接下来看看loadBalancerInterceptor拦截器里做了什么,通过页面发起一个http请求,断点进入到LoadBalancerInterceptor类的intercept方法,

    @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));
    	}
    

    截图看下信息:
    在这里插入图片描述
    可以看到该方法取得了request里的url和servicName,然后将这些参数交给loadBalancer.execute去执行方法。而loadBalancer是LoadBalancerClient类的实例。
    看下LoadBalancerClient的类图,可以看到LoadBalancerClient继承了ServiceInstanceChooser,LoadBalancerClient的实现类是RibbonLoadBalancerClient
    在这里插入图片描述
    逻辑继续,断点进入了RibbonLoadBalancerClient的execute方法

    public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint)
    			throws IOException {
    		ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
    		Server server = getServer(loadBalancer, hint);
    		if (server == null) {
    			throw new IllegalStateException("No instances available for " + serviceId);
    		}
    		RibbonServer ribbonServer = new RibbonServer(serviceId, server,
    				isSecure(server, serviceId),
    				serverIntrospector(serviceId).getMetadata(server));
    
    		return execute(serviceId, ribbonServer, request);
    	}
    

    跟着断点一步一步看方法:

    • ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
      在这里插入图片描述
      经过这个方法,得到loadBalancer,从截图里可以看到,loadBalancer里有个allServerList集合,里面有两个对象,端口号分别是8763和8762,这就是我们提供的服务节点。

    • Server server = getServer(loadBalancer, hint)
      在这里插入图片描述
      从图里可以看出,通过这个getServer方法,会返回给我们一个当前可调用的服务节点,而至于怎么返回服务节点,会再写一篇分析,写完后会更新链接到该篇。

    • 生成RibbonServer 作为参数传入execute方法

    • 运行execute方法

    接着跟进execute方法
    在这里插入图片描述
    可以看该方法里的关键执行方法是:
    T returnVal = request.apply(serviceInstance);
    接着看apply方法,发现它是LoadBalancerRequest接口的方法,该接口却没有具体的实现类:

    public interface LoadBalancerRequest<T> {
    
    	T apply(ServiceInstance instance) throws Exception;
    
    }
    

    思路回溯,是request对象调用的apply方法,而request其实是execute方法传进来的参数,追溯到源头,发现是LoadBalancerInterceptor类的intercept方法里this.requestFactory.createRequest(request, body, execution)生成了LoadBalancerRequest,然后作为参数传入,之后再调用了apply方法

    @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));
    	}
    

    跟进createRequest方法里:

    在这里插入图片描述
    可以从图中看到,经过一些操作后,生成的serviceRequest对象里的serviceId是eureka-client,也就是我们的服务节点名,而server是localhost:8763,这是具体的服务节点ip,之后作为参数调用org.springframework.http.client包下的InterceptingClientHttpRequest类中的execute方法

    断点进入该方法:
    在这里插入图片描述
    可以看出通过requestFactory.createRequest(request.getURI(), method)方法生成了ClientHttpRequest类的实例delegate,它的url就是我们最后真正要请求的,最后正常调用delegate.execute()方法取得返回ClientHttpResponse就好了。

    而这里产生了一个疑问,url是怎么产生的?重新发起请求断点试下
    发现关键在LoadBalancerRequestFactory类中的createRequest方法中的这句:

    HttpRequest serviceRequest = new ServiceRequestWrapper(request, instance,his.loadBalancer);
    

    跟进ServiceRequestWrapper类中,发现它继承了HttpRequestWrapper 类,同时重写了getURI方法

    public class ServiceRequestWrapper extends HttpRequestWrapper {
    
    	private final ServiceInstance instance;
    
    	private final LoadBalancerClient loadBalancer;
    
    	public ServiceRequestWrapper(HttpRequest request, ServiceInstance instance,
    			LoadBalancerClient loadBalancer) {
    		super(request);
    		this.instance = instance;
    		this.loadBalancer = loadBalancer;
    	}
    
    	@Override
    	public URI getURI() {
    		URI uri = this.loadBalancer.reconstructURI(this.instance, getRequest().getURI());
    		return uri;
    	}
    
    }
    

    断点打在getURI方法里:
    在这里插入图片描述
    可以看到该方法返回了我们最后需要的url。

    最后,关于Ribbon是如何通过一个@LoadBalanced注解就实现负载均衡的分析就到这了,还是有很多疏漏的地方,但是大致的逻辑就是这样的了,还有一些更深层的比如如何根据策略选出当前提供服务的节点等,留待后续补充,来日方长~

  • 相关阅读:
    Java并发编程实战 第11章 性能与可伸缩性
    Lock的await/singal 和 Object的wait/notify 的区别(转载)
    线程阻塞和挂起(网络收集)
    Java并发编程实战 第10章 避免活跃性危险
    Java并发编程实战 第8章 线程池的使用
    Java并发编程实战 第6章 任务并行 第7章 取消与关闭
    Java并发编程实战 第5章 构建基础模块
    Java编程思想 第21章 并发
    Java并发编程实战 第4章 对象的组合
    Java并发编程实战 第3章 对象的共享
  • 原文地址:https://www.cnblogs.com/zhoading/p/12192896.html
Copyright © 2011-2022 走看看