Why a @LoadBalanced annotation can make RestTemplate have load balancing capabilities? [Learn to enjoy Spring Cloud]

Each one

You should think about: Why do tend to complete more important than perfect?

Foreword

In the Spring Cloudmicro-service application system, remote call should load balancing. We use RestTemplateas a remote call to a client when open load balancing is extremely simple: a @LoadBalancedcomment to get .
I believe we mostly used Ribbonto do Client-side load balancing, maybe you and I have the same feeling: Ribbon, although not particularly powerful but easy to use . I studied a lot, in fact, the root of its principles or do we not know enough about the internal, leading to some phenomena can not give a reasonable explanation, but also its impact on our customized and extended . This paper comb made for this, I hope you can pass this article on Ribbona clearer understanding (we only have to explain it a @LoadBalancedthis small piece of content).

Open client load balancing can be only one comment, shaped like this:

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

He said Springthe Java community the best and most outstanding works of reinventing the wheel invention can not be overemphasized. This article behalf of the collar you find out, why open RestTemplateload balancing so simple.

Description: In this paper, you already familiar with RestTemplateand understand the RestTemplatebasic principles of its related components of the analysis. If this part is still relatively vague, forced to recommend that you see in front of my article: RestTemplate use and principles you are overripe in the chest yet? [Enjoy learning Spring MVC]

RibbonAutoConfiguration

This is the Spring Boot/Cloudstart Ribbonentry automatically configures the class, you need to have a general understanding:

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

This configuration is most important is completed Ribbonautomatically configure the relevant components, have LoadBalancerClientto do load balancing (used here is its unique implementation class RibbonLoadBalancerClient)


@LoadBalanced

Notes itself and its simplicity (a property of all wood):

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

Its biggest feature: the head marked with @Qualifiernotes, this is one of the most important factors of its entry into force, after the article, I spent a lot of friends half length on the timing of its entry into force.
About @LoadBalancedconfigured to automatically take effect, we need to come to the auto-configuration categories: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);
            };
        }
    }
    ...
}

This configuration code a little bit long, I summarized in the following process steps:

  1. LoadBalancerAutoConfigurationTo take effect classpath must have RestTemplate, as well as within the Spring container must have LoadBalancerClientrealized Bean
    1. LoadBalancerClientThe only implementation class is:org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient
  2. LoadBalancerInterceptorA ClientHttpRequestInterceptorclient request interceptors. Its role is to intercept before the client sends a request, thus achieving load balancing client
  3. restTemplateCustomizer()Return anonymous customizer RestTemplateCustomizerIt is used to all RestTemplateplus load balancing interceptor (it should be noted that the @ConditionalOnMissingBeancomment ~)

Not difficult to find, load balancing implementation of the core is an interceptor, the interceptor is to make a general RestTemplatecounter-attack has become a requester with load-balancing capabilities

LoadBalancerInterceptor

This class is the only place to be used LoadBalancerAutoConfigurationin the configuration up ~

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

After this interceptor intercepts the request it is serviceNameentrusted to LoadBalancerClientto perform, according to ServiceNamemay correspond to a plurality of actual N Server, so you can use equalization algorithms from numerous Server, pick out one of the most suitable Serverfor final request (it holds true request actuators ClientHttpRequestExecution).


LoadBalancerClient

After the request was blocked, ultimately entrusted to the LoadBalancerClientprocess.

// 由使用负载平衡器选择要向其发送请求的服务器的类实现
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);
}

It has only one implementation class RibbonLoadBalancerClient( ServiceInstanceChoosera plurality of classes implemented ~).

RibbonLoadBalancerClient

First of all we should be concerned about its choose()methods:

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方法: Incoming serviceId, then SpringClientFactoryget a load balancer com.netflix.loadbalancer.ILoadBalancer, eventually entrusted to it by chooseServer()the method to select a com.netflix.loadbalancer.Serverinstance, that really completed Serverchosen is ILoadBalancer.

ILoadBalancerAnd its related classes is a relatively large system, we do not do more to expand, but only focus on our processes

LoadBalancerInterceptorWhen the execution is carried out directly trust loadBalancer.execute()this method:

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;
        ... // 异常处理
    }

returnValIs a ClientHttpResponse, and finally to handleResponse()a method to handle exceptional conditions (if present), without exception on to mention extractor Found: responseExtractor.extractData(response)so even completed the entire request.

Details

For @LoadBalancedunder RestTemplateuse, I summarized below for details refer to:

  1. Incoming Stringtype url must be absolute ( http://...), otherwise throw an exception:java.lang.IllegalArgumentException: URI is not absolute
  2. serviceIdCase-insensitive http://user/...效果同http://USER/...( )
  3. serviceIdPlease do not tell port port number the ~ ~ ~

Finally, it should be pointed out that: marked with @LoadBalancedthe RestTemplateonly writing serviceIdbut can not write IP地址/域名to send the request. If your project needs to have two kinds of case, please define multiple RestTemplateaddress different usage scenarios were ~

Local test

Understand its execution process after local tests if necessary (do not rely on registry), you can do so:

// 因为自动配置头上有@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);
        }
    };
}

This way, following the results of this visit is the Baidu home page of html content myself.

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

Here my-serviceIdcertainly does not exist, but thanks to my custom configuration of the aboveLoadBalancerClient

What, dead write returna Serverinstance is not elegant? Indeed, you can not always on the front line also this part of the code to comment out of it, if multiple instances of it? We had to write their own load balancing algorithm do? It is clear Spring Cloudearly on as we consider this point: from the Eureka using the configuration listOfServers for client load balancing scheduling ( <clientName>.<nameSpace>.listOfServers=<comma delimited hostname:port strings>)

For the example I just need to master configuration file to configure it so:

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

The effect is entirely above.

Tips: This method does not require a complete configuration absolute path, http://may be omitted ( new Server()the way also)

A log requests to add your own interceptor feasible?

Obviously it is feasible, I give the following example:

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

So each client's request will be printed these words: 当前请求的URI是:http://my-serviceIdthe general situation (default) Custom interceptors will be executed in front of the load balancing interceptor (because it needs to perform final request). If you need to define a plurality of interceptors and control sequence can Orderedbe implemented serial interface -


Finally, the last, I throw a very, very important question:

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

@Autowired+ @LoadBalancedCan you configure RestTemplateautomatic injection used to come in to customize it? ? ? What is the core principle?

> Tip: The principle contents belong to Spring Framworkthe core technology, it is recommended to think deeply without gulping. Doubt can give me a message, I will also give a detailed answer in the next article (it is recommended to think)

Recommended Reading

RestTemplate use and principles you are overripe in the chest yet? [Enjoy learning Spring MVC]
@Qualifier advanced applications --- batch dependency injection by category enjoy learning [Spring]

to sum up

In this paper, the familiar @LoadBalancedand RestTemplateas an entry point describes the Ribbonload balancing implementation process, of course, this part of the Ribbonknowledge of the entire tip of the iceberg of the load system is the core of knowledge, but it makes sense as a stepping stone, I hope this can arouse your of Ribboninterest in the system, understand it ~

== If for Spring, SpringBoot, MyBatis source code analysis and other interested can add me wx: fsx641385712, manually invite you to take off into a group ==
== If for Spring, SpringBoot, MyBatis source code analysis and other interested can add me wx : fsx641385712, manually invite you into the group took off ==

Guess you like

Origin www.cnblogs.com/fangshixiang/p/11532729.html