Why does a @LoadBalanced annotation enable RestTemplate to have load balancing capabilities? [Enjoy Spring Cloud]

Each sentence

You should think: Why is completing more important than perfection?

Preface

In Spring Cloud microservice application system, remote calls should be load balanced. When we use RestTemplate as a remote call client, it's extremely simple to turn on load balancing: a @LoadBalanced annotation is done.
I believe that most of you have used Ribbon to do load balancing on the Client side. Perhaps you have the same feeling as me: Ribbon is powerful but not particularly useful. I have studied it for a while. In fact, the reason is that we don't know enough about its internal principles, which leads to the inability to give a reasonable explanation for some phenomena. At the same time, it also affects our customization and expansion of it. This article combs this, hoping that you can also have a clearer understanding of Ribbon through this article (this article only explains its @LoadBalanced this small piece of content).

Opening client load balancing only requires one annotation, as follows:

@LoadBalanced // After annotating this annotation, RestTemplate has client load balancing capabilities
@Bean
public RestTemplate restTemplate(){
    return new RestTemplate();
}

It's no exaggeration to say that Spring is the best and most outstanding reinvention wheel in Java. This article will show you why it's so easy to turn on RestTemplate's load balancing.

Description: This article is based on your knowledge of RestTemplate and the principles of its related components. If this part is still vague, I strongly recommend you to refer to my previous article: Are you familiar with the use and principles of RestTemplate? [Enjoy Spring MVC]

RibbonAutoConfiguration

This is Spring Boot/Cloud's entry auto-configuration class for launching Ribbon. It needs a general understanding first.

@Configuration
// The classpath takes effect when there are com.netflix.client.IClient, RestTemplate, etc.
@Conditional(RibbonAutoConfiguration.RibbonClassesConditions.class) 
// // Allow multiple @RibbonClient in a single class
@RibbonClients 
// If you have Eureka, configure it after Eureka is configured. ~ (If it's another registry, can ribbon still play?)
@AutoConfigureAfter(name = "org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration")
@AutoConfigureBefore({ LoadBalancerAutoConfiguration.class, AsyncLoadBalancerAutoConfiguration.class })
// Load configuration: ribbon. eager - load - > true, then the Client will be initialized when the project starts, avoiding the first penalty
@EnableConfigurationProperties({ RibbonEagerLoadProperties.class, ServerIntrospectorProperties.class })
public class RibbonAutoConfiguration {

    @Autowired
    private RibbonEagerLoadProperties ribbonEagerLoadProperties;
    // Ribbon configuration files
    @Autowired(required = false)
    private List<RibbonClientSpecification> configurations = new ArrayList<>();

    // Features, the Endpoint of Features (`actuator/features') will use its org. spring framework. cloud. client. actuator. HasFeatures
    @Bean
    public HasFeatures ribbonFeature() {
        return HasFeatures.namedFeature("Ribbon", Ribbon.class);
    }


    // It's the most important, an org. spring framework. cloud. context. named. NamedContextFactory, which is used to create named Spring containers
    // Here, the configuration file is passed in, and each different namespace creates a new container (like Feign in particular) that sets the current container as the parent container.
    @Bean
    public SpringClientFactory springClientFactory() {
        SpringClientFactory factory = new SpringClientFactory();
        factory.setConfigurations(this.configurations);
        return factory;
    }

    // This Bean is the key. If you don't define it, use the Client provided by default.~~~
    // Internal use and ownership of Spring ClientFactory...
    @Bean
    @ConditionalOnMissingBean(LoadBalancerClient.class)
    public LoadBalancerClient loadBalancerClient() {
        return new RibbonLoadBalancerClient(springClientFactory());
    }
    ...
}

The most important thing about this configuration class is that it completes the automatic configuration of Ribbon-related components. Load Balancer Client is the only implementation class that can be used for load balancing (in this case, its only implementation class, Ribbon Load Balancer Client)

@LoadBalanced

Annotations themselves and their simplicity (one attribute is mundane):

// The package is org. spring framework. cloud. client. loadbalancer
// Can be labeled on fields, method parameters, methods
// JavaDoc makes it clear that it is only valid if it is tagged on RestTemplate
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {
}

Its biggest feature is that it has @Qualifier annotation on its head, which is one of the most important factors for its entry into force. In the second half of this article, I spent a lot of time introducing its entry into force.
For the @LoadBalanced automatic configuration, we need to come to this automatic configuration class: LoadBalancer AutoConfiguration

LoadBalancerAutoConfiguration

// Auto-configuration for Ribbon (client-side load balancing).
// Its load balancing technology relies on Ribbon components.~
// Its package is: org. spring framework. cloud. client. loadbalancer
@Configuration
@ConditionalOnClass(RestTemplate.class) //So it only works for RestTemplate.
@ConditionalOnBean(LoadBalancerClient.class) // Bean s with this interface must exist in the Spring container to take effect (see: Ribbon AutoConfiguration)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class) // retry configuration file
public class LoadBalancerAutoConfiguration {
    
    // Get all the beans in the container with the @LoadBalanced annotation
    // Note: You must annotate @LoadBalanced
    @LoadBalanced
    @Autowired(required = false)
    private List<RestTemplate> restTemplates = Collections.emptyList();    
    // LoadBalancer Request Transformer Interface: Allows users to revamp request + Service Instance - >.
    // Spring does not provide any implementation classes by default (all anonymous)
    @Autowired(required = false)
    private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();

    // Configure an anonymous Smart Initializing Singleton interface that we should be familiar with
    // Its afterSingletons Instantiated () method calls one processing Bean Name after all singleton beans are initialized.~
    // Here: Use all the RestTemplate Customizer customizers configured to customize all `RestTemplate'.
    // There is an implementation of lambda under RestTemplate Customizer. It takes effect if the caller needs to write and then throw it into the container.
    // This customizer: If you have multiple RestTempalte s in your project, they need to be handled uniformly. Writing a customizer is a good choice
    // (For example, uniformly place a request interceptor: output log, etc.)
    @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);
                }
            }
        });
    }
    
    // This factory is used for createRequest() to create a LoadBalancerRequest
    // This request contains LoadBalancerClient and HttpRequest request
    @Bean
    @ConditionalOnMissingBean
    public LoadBalancerRequestFactory loadBalancerRequestFactory(LoadBalancerClient loadBalancerClient) {
        return new LoadBalancerRequestFactory(loadBalancerClient, this.transformers);
    }
    
    // ========= So far it has nothing to do with load balancing.==========
    // ========= The next configuration is related to load balancing (above is the foundation, of course).==========

    // If you have Retry's package, it's another configuration, which is about the same as that.~~
    @Configuration
    @ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
    static class LoadBalancerInterceptorConfig {,
    
        // The name of this Bean is `loadBalancerClient', and I personally think it would be more appropriate to call it `loadBalancerInterceptor'(although ribbon is the only implementation)
        // The direct use here is requestFactory and Client to build an interceptor object
        // Load Balancer Interceptor but `Client HttpRequest Interceptor', it will intervene in http.client
        // Load Balancer Interceptor is also an entry point for load balancing, as described below.
        // Tips: There's no @Conditional On Missing Bean here.~~~~
        @Bean
        public LoadBalancerInterceptor ribbonInterceptor(LoadBalancerClient loadBalancerClient, LoadBalancerRequestFactory requestFactory) {
            return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
        }
    
        
        // Insert a RestTemplate Customizer customizer into the container
        // The role of this customizer has been said above: After RestTemplate initialization is complete, apply this customizer on all instances of **
        // The logic of this anonymous implementation is super simple: cram a loadBalancer Interceptor into all RestTemplate to give it load balancing capabilities
        
        // Tips: Here is the annotation @ConditionalOnMissingBean. That is, if the caller has defined a Bean of RestTemplateCustomizer type, it will not be executed here.
        // Be aware of this: it's easy to make your load balancing ineffective.~~~~
        @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 is a little bit long. I summarize the process as follows:

  1. The LoadBalancer AutoConfiguration must have RestTemplate for the class path to take effect, and LoadBalancer Client implementation Bean in the Spring container

        1. The only implementation class of `LoadBalancerClient'is: `org. spring framework. cloud. netflix. ribbon. RibbonLoadBalancerClient'.`
  2. LoadBalancer Interceptor is a Client HttpRequest Interceptor client request interceptor. Its function is to intercept the request before the client initiates the request, and then realize the load balance of the client.
  3. RestTemplate Customizer () returns the anonymous customizer RestTemplate Customizer, which is used to add load balancing interceptors to all RestTemplate (note its @ConditionalOnMissingBean annotation ~)

It is not difficult to find that the core of load balancing implementation is an interceptor, which makes a common EstTemplate reverse attack become a requester with load balancing function.

LoadBalancerInterceptor

The only use of this class is to configure it in LoadBalancer AutoConfiguration~

public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {

    // This name is not Client, but loadBalancer.~~~
    private LoadBalancerClient loadBalancer;
    // Used to build a Request
    private LoadBalancerRequestFactory requestFactory;
    ... // Omit the constructor (assign values to these two attributes)

    @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 intercepting the request, the interceptor delegates its serviceName to Load Balancer Client to execute it. According to ServiceName, it may correspond to more than N actual servers, so it can use the balancing algorithm from many servers to select the most suitable Server to make the final request (it holds the real request executor ClientHtt). PRequest Execution).

LoadBalancerClient

After the request is intercepted, it is ultimately delegated to LoadBalancerClient for processing.

// Class implementation of selecting the server to send the request to using the load balancer
public interface ServiceInstanceChooser {

    // Select a Service service instance from the load balancer for the specified service.
    // That is, according to the service Id passed in by the caller, a specific instance of load balancing is selected.
    ServiceInstance choose(String serviceId);
}

// It itself defines three methods
public interface LoadBalancerClient extends ServiceInstanceChooser {
    
    // Execution request
    <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;
    <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException;
    
    // Reconstruct url: Replace the service name originally written in the URL with the actual one
    URI reconstructURI(ServiceInstance instance, URI original);
}

It has only one implementation class, RibbonLoadBalancerClient (Service Instance Chooser has multiple implementation classes ~).

RibbonLoadBalancerClient

First of all, we should pay attention to its selection () method:

public class RibbonLoadBalancerClient implements LoadBalancerClient {
    
    @Override
    public ServiceInstance choose(String serviceId) {
        return choose(serviceId, null);
    }
    // hint: You can understand constituent groups. If specified, the selection will only be balanced within the preference group.
    // Once you get a server, use Ribbon Server to adapt the server~~~
    // Such an instance will select the ~~ real request and it will fall on this instance.~
    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));
    }

    // Find a load balancer that belongs to ServiceId
    protected ILoadBalancer getLoadBalancer(String serviceId) {
        return this.clientFactory.getLoadBalancer(serviceId);
    }

}

choose method: Pass in serviceId, then get the load balancer com.netflix.loadbalancer.ILoadBalancer through Spring ClientFactory, and finally select an instance of com.netflix.loadbalancer.Server by the chooseServer() method entrusted to it. That is to say, the ILoadBalancer that really completes the selection of Server is ILoadBalancer.

ILoadBalancer and its related classes are a relatively large system. This article does not expand more, but focuses only on our process.

LoadBalancer Interceptor is executed by directly delegating the execution of loadBalancer.execute(). This method:

RibbonLoadBalancerClient: 

    // hint is passed here as null: non-discriminatory
    // Note: LoadBalancerRequest was created by LoadBalancerRequestFactory.createRequest(request, body, execution)
    // It implements the LoadBalancerRequest interface using an anonymous inner class with the generic type ClientHttpResponse
    // Because the final execution is obviously still the executor: ClientHttpRequestExecution.execute()
    @Override
    public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
        return execute(serviceId, request, null);
    }
    // public method (non-interface method)
    public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint) throws IOException {
        // Identical: Get the load balancer, and then get a server Instance instance
        ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
        Server server = getServer(loadBalancer, hint);
        if (server == null) { // If not, throw the exception directly. The exception IllegalStateException is used here.
            throw new IllegalStateException("No instances available for " + serviceId);
        }

        // Adapt Server to Ribbon Server isSecure: Is the client secure
        // Server Introspector Introspector Introductory Reference Profile: Server Introspector Properties
        RibbonServer ribbonServer = new RibbonServer(serviceId, server,
                isSecure(server, serviceId), serverIntrospector(serviceId).getMetadata(server));

        //Calling overloaded interface methods of this class~~~~~
        return execute(serviceId, ribbonServer, request);
    }

    // Interface method: Its parameter is Service Instance - > The only Server instance has been determined~~~
    @Override
    public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException {
    
        // Get Server (Ribbon Server is the only implementation of execute, to put it bluntly)
        Server server = null;
        if (serviceInstance instanceof RibbonServer) {
            server = ((RibbonServer) serviceInstance).getServer();
        }
        if (server == null) {
            throw new IllegalStateException("No instances available for " + serviceId);
        }

        // Note: The execution context is bound to serviceId
        RibbonLoadBalancerContext context = this.clientFactory.getLoadBalancerContext(serviceId);
        ... 
        // Really send a request to the server and get the return value
        // Because of the interceptor, I'm sure the InterceptingRequestExecution.execute() method is executed here.
        // so calls ServiceRequestWrapper.getURI() and thus reconstructURI() method.
            T returnVal = request.apply(serviceInstance);
            return returnVal;
        ... // exception handling
    }

ReVal is a ClientHttpResponse, which is finally handleResponse() to handle exceptions (if any) and, if no exceptions exist, to the extractor to raise the value: responseExtractor.extractData(response), so that the entire request is completed.

Use details

For the use of RestTemplate under @LoadBalanced, I summarize the following details for your reference:

  1. The url of the incoming String type must be an absolute path (http://...), otherwise an exception is thrown: java.lang.IllegalArgumentException: URI is not absolute
  2. Service Id is case-insensitive (http://user/... works as well as http://USER/...)
  3. Please don't follow the port number after serviceId~~~

Finally, it should be pointed out that EstTemplate marked @LoadBalanced can only write serviceId and can no longer write IP address/domain name to send requests. If both case s are needed in your project, define multiple RestTemplate scenarios for different usage scenarios~

Local testing

Once you understand its execution process, if you need a local test (independent of the registry), you can do this:

// Because the @ConditionalOnMissingBean annotation is on the automatic configuration header, you can customize a behavior that overrides it.
// Here, copy its getServer() method and return to a fixed (visit Baidu homepage) to facilitate testing.
@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);
        }
    };
}

In this way, the following result of the visit is the content of html on Baidu's home page.

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

my-serviceId certainly does not exist here, but thanks to Load Balancer Client, which I have customized above.

What, writing to return a Server instance is not elegant? Indeed, you can't annotate this part of the code every time you go online. What if there are multiple instances? Do you have to write your own load balancing algorithm? Spring Cloud apparently took this into account early on: using configuration listOfServers for client load balancing scheduling without Eureka (<clientName>. <nameSpace>.listOfServers=<comma delimited hostname: port strings>)

For the example above, I just need to configure it in the main configuration file as follows:

# ribbon.eureka.enabled=false # If euraka is not used, this configuration can be omitted. Otherwise, No.
my-serviceId.ribbon.listOfServers=www.baidu.com # If there are multiple instances, separate them by commas

The effect is exactly the same.

Tips: This configuration does not need to be a complete absolute path, http://can be omitted (new Server() mode can also be)

Is it feasible to add an interceptor to record request logs?

Obviously, it is feasible. I give the following examples:

@LoadBalanced
@Bean
public RestTemplate restTemplate() {
    RestTemplate restTemplate = new RestTemplate();
    List<ClientHttpRequestInterceptor> list = new ArrayList<>();
    list.add((request, body, execution) -> {
        System.out.println("Currently requested URL Yes," + request.getURI().toString());
        return execution.execute(request, body);
    });
    restTemplate.setInterceptors(list);
    return restTemplate;
}

The URI of the current request is http://my-serviceId, and in general (default) the custom interceptor is executed in front of the load balancing interceptor (because it will execute the final request). If you need to define multiple interceptors and control the sequence, you can do it through the Ordered series interface.~

Finally, I would like to raise a very important question:

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

@ Can Autowired + @LoadBalanced automatically inject the estTemplate you configure and customize it??? What are the core principles?

Tip: This principle belongs to the core technology of Spring Framwork. It is suggested to think deeply and not swallow dates. If you have any questions, please leave me a message, and I will give you a detailed answer in the next article.

Recommended reading

Are you familiar with the use and principles of RestTemplate? [Enjoy Spring MVC]
@ Qualifier Advanced Applications - Bulk Dependency Injection by Category

summary

This paper introduces Ribbon's implementation process of load balancing from the familiar @LoadBalanced and RestTemplate. Of course, this part is the tip of Ribbon's knowledge of the whole core load system, but it is still meaningful as a knock-on brick. I hope this article can arouse your interest in Ribbon system. Get to know it~

== If you are interested in Spring, Spring Boot, MyBatis and other source code analysis, you can add me wx: fsx641385712, invite you to join the group and fly together manually.==
== If you are interested in Spring, Spring Boot, MyBatis and other source code analysis, you can add me wx: fsx641385712, invite you to join the group and fly together manually.==

Keywords: Java Spring Mybatis Attribute

Added by Sarok on Tue, 17 Sep 2019 07:26:08 +0300