In the last tutorial, we simply used the Ribbon to make a call to the load, which means that the Ribbon can be used alone.
It will be easier to use Ribbon in Spring Cloud, because Spring Cloud has a layer of encapsulation based on Ribbon and integrates many configurations. This section will use the Ribbon in the Spring Cloud project.
Integrating Ribbon with RestTemplate
Spring provides a simple and convenient template class to call API, that is RestTemplate.
1. Use RestTemplate
When we introduced Eureka earlier, we have already used RestTemplate. This section will explain the specific usage of RestTemplate in more detail.
First, let's take a look at the usage of GET request: 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.
public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables); public <T> T getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables); public <T> T getForObject(URI url, Class<T> responseType);
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 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 operation methods such as put and delete. 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
Integrating Ribbon in Spring Cloud project only needs to be in POM The following dependencies can be added to the XML. In fact, there is no need to configure it, because the 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. Recommended distributed architecture source code
@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 low-level work for us, because it encapsulates all these, and it will be so simple for us to use. Framework is to simplify the 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.
Now let's implement a simple interceptor to see if it will enter the interceptor before calling the interface. We don't do any operation, 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 can 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 of RestTemplate in smartinitializingsingsingleton.
Then change 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 output of the console, which proves that the interceptor will enter when the interface is called. The output is as follows:
Enter the custom request interceptor ribbon-eureka-demo
Through this small case, we can clearly know the working principle of @ LoadBalanced. Next, let's take a 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 spring cloud Commons Org. Jar springframework. cloud. client. loadbalancer. In the loadbalancenautoconfiguration class, by viewing the source code of loadbalancenautoconfiguration, you can see that here also maintains a RestTemplate list of @ LoadBalanced. 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); } } } }; }
You can see from the configuration of the interceptor that the interceptor uses LoadBalancerInterceptor and 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 at org springframework. cloud. client. loadbalancer. In loadbalancerinterceptor, 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 acquisition of URI to 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 to obtain it. 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 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 condition is bad, the first call will timeout. Many great gods have proposed solutions to this, such as changing the timeout to a longer time, 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 relatively 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 based on Finchley Written by 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 clients, separate them with commas.
How to verify? The way of debugging the source code is not very good, but it can be verified through the network. Org springframework. cloud. netflix. ribbon. Find the corresponding code in 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 then start the service in debugging mode. If you can enter the code of this breakpoint, it proves that the configuration has taken effect!