@LoadBalanced annotation principle

Integrating Ribbon with RestTemplate

Spring provides a simple and convenient template class for API calls, that is, RestTemplate.

1. Use RestTemplate

When we introduced Eureka earlier, we have already used RestTemplate. This section will explain how to use RestTemplate in more detail.

First, let's take a look at the use of GET requests: create a new project spring rest template and configure the RestTemplate:

@Configuration
public class BeanConfiguration {

    @Bean
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}

Create a new HouseController and add two interfaces. One passes parameters through @ RequestParam and returns an object information; The other passes parameters through @ PathVariable and returns a string. Please try to assemble different forms through two interfaces. The specific code is as follows.

@GetMapping("/house/data")
public HouseInfo getData(@RequestParam("name") String name) {
    return new HouseInfo(1L, "Shanghai" "Hongkou" "Dongti community");
}

@GetMapping("/house/data/{name}")
public String getData2(@PathVariable("name") String name) {
    return name;
}

Create a new HouseClientController for testing. Use RestTemplate to call the two interfaces we just defined. The code is as follows.

@GetMapping("/call/data")
public HouseInfo getData(@RequestParam("name") String name) {
    return restTemplate.getForObject( "http://localhost:8081/house/data?name="+ name, HouseInfo.class);
}

@GetMapping("/call/data/{name}")
public String getData2(@PathVariable("name") String name) {
    return restTemplate.getForObject( "http://localhost:8081/house/data/{name}", String.class, name);
}

The data results can be obtained through the getForObject method of RestTemplate (as shown in the following code). This method has three overloaded implementations:

  • url: the API address of the request. There are two ways, one is in the form of string and the other is in the form of URI.
  • responseType: type of return value.
  • uriVariables: PathVariable parameter. There are two methods, one is variable parameter and the other is Map.

In addition to getforebject, we can also use getForEntity to obtain data. The code is as follows.

@GetMapping("/call/dataEntity")
public HouseInfo getData(@RequestParam("name") String name) {
    ResponseEntity<HouseInfo> responseEntity = restTemplate
            .getForEntity("http://localhost:8081/house/data?name=" + name, HouseInfo.class);
    if (responseEntity.getStatusCodeValue() == 200) {
        return responseEntity.getBody();
    }
    return null;
}

getForEntity can get the returned status code, request header and other information, and get the content of the response through getBody. The rest, like getForObject, have three overloaded implementations.

Next, let's see how to use POST to call the interface. Add a save method in the HouseController to receive HouseInfo data. The code is as follows.

@PostMapping("/house/save")
public Long addData(@RequestBody HouseInfo houseInfo) {
    System.out.println(houseInfo.getName());
    return 1001L;
}

Then write the calling code and call it with postForObject. The code is as follows.

@GetMapping("/call/save")
public Long add() {
    HouseInfo houseInfo = new HouseInfo();
    houseInfo.setCity("Shanghai");
    houseInfo.setRegion("Hongkou");
    houseInfo.setName("×××");
    Long id = restTemplate.postForObject("http://localhost:8081/house/save", houseInfo, Long.class);
    return id;
}

postForObject also has three overloaded implementations. In addition to postforebject, you can also use the postForEntity method. The usage is the same. The code is as follows.

public <T> T postForObject(String url, Object request,
                           Class<T> responseType, Object... uriVariables);

public <T> T postForObject(String url, Object request,
                           Class<T> responseType, Map<String, ?> uriVariables);

public <T> T postForObject(URI url, Object request, Class<T> responseType);

In addition to the methods corresponding to get and post, RestTemplate also provides put, delete and other operation methods. Another practical method is the exchange method. Exchange can execute four request modes: get, post, put and delete. You can learn more ways to use it by yourself.

2. Integrate Ribbon

To integrate Ribbon in Spring Cloud project, you only need to add the following dependencies in pom.xml. In fact, you don't need to configure it, because Ribbon has been referenced in Eureka, and the code is as follows.

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

RestTemplate load balancing example

Previously, we call interfaces through specific interface addresses. RestTemplate can be combined with Eureka to dynamically discover services and call load balancing.

Modify the configuration of RestTemplate and add the annotation @ LoadBalanced that enables RestTemplate to have load balancing capability. The code is shown below.

@Configuration
public class BeanConfiguration {
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}

Modify the code of the interface call and change the IP+PORT to the service name, that is, the name registered in Eureka. The code is as follows.

@GetMapping("/call/data")
public HouseInfo getData(@RequestParam("name") String name) {
    return restTemplate.getForObject("http://ribbon-eureka-demo/house/data?name=" + name, HouseInfo.class);
}

When the interface is called, the service name will be replaced with specific service IP information within the framework, and then called.

@LoadBalanced annotation principle

I believe you must have a question: why can RestTemplate be combined with Eureka after adding @ LoadBalanced to RestTemplate? You can not only use the service name to call the interface, but also load balance?

We owe it to Spring Cloud for doing a lot of underlying work for us, because it encapsulates all these, so it will be so simple for us to use. Framework is to simplify code and improve efficiency.

The main logic here is to add an interceptor to the RestTemplate to replace the requested address before the request, or select the service address according to the specific load policy and then call it. This is the principle of @ LoadBalanced.

Let's implement a simple interceptor to see if it will enter the interceptor before calling the interface. We don't do anything, just output a sentence to prove that we can come in. The specific code is as follows.

public class MyLoadBalancerInterceptor implements ClientHttpRequestInterceptor {
    private LoadBalancerClient loadBalancer;
    private LoadBalancerRequestFactory requestFactory;
    public MyLoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory) {
        this.loadBalancer = loadBalancer;
        this.requestFactory = requestFactory;
    }
    public MyLoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
        this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
    }
    @Override
    public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
            final ClientHttpRequestExecution execution) throws IOException {
        final URI originalUri = request.getURI();
        String serviceName = originalUri.getHost();
        System.out.println("Enter the custom request interceptor" + serviceName);
        Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
        return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
    }
}

After the interceptor is set, we define an annotation, copy the @ LoadBalanced code, and change the name. The code is as follows.

@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface MyLoadBalanced {
}

Then define a configuration class and inject interceptors into RestTemplate. The code is as follows.

@Configuration
public class MyLoadBalancerAutoConfiguration {
    @MyLoadBalanced
    @Autowired(required = false)
    private List<RestTemplate> restTemplates = Collections.emptyList();
    @Bean
    public MyLoadBalancerInterceptor myLoadBalancerInterceptor() {
        return new MyLoadBalancerInterceptor();
    }
    @Bean
    public SmartInitializingSingleton myLoadBalancedRestTemplateInitializer() {
        return new SmartInitializingSingleton() {
          @Override
          public void afterSingletonsInstantiated() {
            for (RestTemplate restTemplate : MyLoadBalancerAutoConfiguration.this.restTemplates){
                List<ClientHttpRequestInterceptor> list = new ArrayList<>(restTemplate.getInterceptors());
                list.add(myLoad BalancerInterceptor());
                restTemplate.setInterceptors(list);
            }
          }
        };
    }
}

Maintain a RestTemplate list of @ MyLoadBalanced, and set the interceptor for the RestTemplate in smartinitializingsingsingleton.

Then modify our previous RestTemplate configuration and change @ LoadBalanced to our custom @ MyLoadBalanced. The code is as follows.

@Bean
//@LoadBalanced
@MyLoadBalanced
public RestTemplate getRestTemplate() {
    return new RestTemplate();
}

Restart the service and access the interface in the service to see the console output, which proves that the interceptor will enter when the interface is called. The output is as follows:

Enter the ribbon Eureka demo in the custom request interceptor

Through this small case, we can clearly understand the working principle of @ LoadBalanced. Next, let's look at the logic in the source code.

First, look at the configuration class and how to set interceptors for RestTemplate. The code is in the org.springframework.cloud.client.loadbalancer.loadbalancenautoconfiguration class in spring-cloud-commons.jar. By viewing the source code of loadbalancenautoconfiguration, you can see that a @ LoadBalanced RestTemplate list is also maintained here. The code is as follows.

@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();
@Bean
public SmartInitializingSingleton loadBalancedRestTemplateInitializer(final List<RestTemplateCustomizer> customizers) {
    return new SmartInitializingSingleton() {
        @Override
        public void afterSingletonsInstantiated() {
            for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
                for (RestTemplateCustomizer customizer : customizers) {
                    customizer.customize(restTemplate);
                }
            }
        }
    };
}

By viewing the configuration of the interceptor, you can know that the interceptor uses the LoadBalancerInterceptor, and the RestTemplate Customizer is used to add the interceptor. The code is as follows.

@Configuration
@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
static class LoadBalancerInterceptorConfig {
    @Bean
    public LoadBalancerInterceptor ribbonInterceptor(LoadBalancerClient loadBalancerClient,
            LoadBalancerRequestFactory requestFactory) {
        return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
    }
    @Bean
    @ConditionalOnMissingBean
    public RestTemplateCustomizer restTemplateCustomizer(
            final LoadBalancerInterceptor loadBalancerInterceptor) {
        return new RestTemplateCustomizer() {
            @Override
            public void customize(RestTemplate restTemplate) {
                List<ClientHttpRequestInterceptor> list = new ArrayList<>(
                  restTemplate.getInterceptors());
                list.add(loadBalancerInterceptor);
                restTemplate.setInterceptors(list);
            }
        };
    }
}

The interceptor code is in org.springframework.cloud.client.loadbalancer.loadbalancerinceptor. The code is as follows.

public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
    private LoadBalancerClient loadBalancer;
    private LoadBalancerRequestFactory requestFactory;
    public LoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory) {
        this.loadBalancer = loadBalancer;
        this.requestFactory = requestFactory;
    }
    public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
        this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
    }
    @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, requestFactory.createRequest(request, body, execution));
    }
}

The main logic is in intercept. The execution is handed over to LoadBalancerClient for processing. A LoadBalancerRequest object is built through LoadBalancer RequestFactory. The code is as follows.

public LoadBalancerRequest<ClientHttpResponse> createRequest(final HttpRequest request, final byte[] body,
        final ClientHttpRequestExecution execution) {
    return new LoadBalancerRequest<ClientHttpResponse>() {
        @Override
        public ClientHttpResponse apply(final ServiceInstance instance) throws Exception {
            HttpRequest serviceRequest = new ServiceRequestWrapper(request, instance, loadBalancer);
            if (transformers != null) {
                for (LoadBalancerRequestTransformer transformer : transformers) {
                    serviceRequest = transformer.transformRequest(serviceRequest,instance);
                }
            }
            return execution.execute(serviceRequest, body);
        }
    };
}

In createRequest, the logic of replacing URI is executed through ServiceRequestWrapper, which gives the URI acquisition to the org.springframework.cloud.client.loadbalancer.LoadBalancer Client#reconstructURI method.

The above is the execution process of the whole RestTemplate combined with @ LoadBalanced. As for the specific implementation, you can study it yourself. Here we only introduce the principle and the whole process.

Ribbon API usage

When you have some special needs and want to obtain the corresponding service information through the Ribbon, you can use the load balancer client. For example, if you want to obtain the service address of a Ribbon Eureka demo service, you can select one through the choose method of LoadBalancerClient:

@Autowired
private LoadBalancerClient loadBalancer;
@GetMapping("/choose")
public Object chooseUrl() {
    ServiceInstance instance = loadBalancer.choose("ribbon-eureka-demo");
    return instance;
}

Access the interface, you can see the returned information as follows:

{
    serviceId: "ribbon-eureka-demo",
    server: {
        host: "localhost",
        port: 8081,
        id: "localhost:8081",
        zone: "UNKNOWN",
        readyToServe: true,
        alive: true,
        hostPort: "localhost:8081",
        metaInfo: {
            serverGroup: null,
            serviceIdForDiscovery: null, instanceId: "localhost:8081",
            appName: null
        }
    },
    secure: false, metadata: { }, host: "localhost", port: 8081,
    uri: "http://localhost:8081"
}

Ribbon hungry loading

The author has seen a situation mentioned in many blogs on the Internet: when calling a service, if the network is bad, the first call will timeout. Many great gods have proposed solutions to this, such as changing the timeout to longer, disabling timeout, etc.

Spring Cloud is currently developing at a high speed, and the version is updated very quickly. The problems we can find are basically fixed when the version is updated, or the best solution is provided.

The problem of timeout is the same. The Ribbon Client is initialized at the time of the first request. If the timeout time is short, the time of initializing the Client plus the time of requesting the interface will lead to the timeout of the first request.

This tutorial is written based on Finchley.SR2. This version has provided a solution to the above problems, that is, eager load. This problem can be solved by configuring eager load to initialize the client in advance.

ribbon.eager-load.enabled=true
ribbon.eager-load.clients=ribbon-eureka-demo

  • ribbon.eager-load.enabled: enable the hungry loading mode of ribbon.
  • ribbon.eager-load.clients: specify the name of the service to be loaded, that is, the service you need to call. If there are multiple, separate them with commas.

How to verify? The network situation is really not easy to simulate, but it can be verified by debugging the source code. Find the corresponding code in org.springframework.cloud.netflix.ribbon.RibbonAutoConfiguration, as shown below.  

@Bean
@ConditionalOnProperty(value = "ribbon.eager-load.enabled")
public RibbonApplicationContextInitializer ribbonApplicationContextInitializer() {
    return new RibbonApplicationContextInitializer(springClientFactory(),ribbonEagerLoadProperties.getClients());
}

Set a breakpoint on the return line and start the service in debug mode. If you can enter the code of this breakpoint, it proves that the configuration has taken effect

Whenever and wherever possible, WeChat official account, Yan Changsheng, reads all tutorials on mobile phone. The official account is C language Chinese network webmaster Operation, daily update, adhere to originality, dare to tell the truth and have an attitude towards everything.

Original text: Spring Cloud Ribbon combined with RestTemplate to achieve load balancing

Tags: Java kafka Distribution Spring Cloud

Posted on Tue, 23 Nov 2021 05:54:00 -0500 by V0oD0o