具有负载均衡功能的RestTemplate底层原理(九)

解读

Spring Cloud服务管理框架Eureka简单示例(三)章节中,我们在服务调用端已经使用RestTemplate做了负载均衡,这里就详细解释一下RestTemplate底层原理,为什么一个Spring提供的做为Rest风格客户端的方法,在加了一个Ribbon提供的@LoadBalanced注解后,就能实现负载均衡了呢?

这要得益于Ribbon的@LoadBalanced注解,它提供了一个拦截器,使得Spring在启动的时候,那些被@LoadBalanced注解修饰的RestTemplate类就不再是原来的那个RestTemplate类了,而是一个具有负载均衡功能的RestTemplate类。

自定义“负载均衡器”并实现拦截功能

我们新建一个简单java的maven项目rest-template,使用Spring Boot来搭建这个项目,所以先在pom.xml里面引入Spring Boot的依赖:

<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>1.5.14.BUILD-SNAPSHOT</version>
</parent>
<dependencies>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
	</dependency>
</dependencies>

 

在src/main/java下面创建org.init.springcloud包,之后创建我们的启动类MyApplication:

 

package org.init.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MyApplication {

	public static void main(String[] args) {
		SpringApplication.run(MyApplication.class,args);
	}
	
}

参照使用了Ribbon @LoadBalanced注解的RestTemplate,我们去@LoadBalanced注解去编写一个自定义的负载均衡器注解MyLoadBalanced,去掉一些不相关的其他注解,保留主要的注解:

package org.init.springcloud;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.beans.factory.annotation.Qualifier;

/**
 * 用注解标记一个被配置用在负载均衡客户端上的RestTemplate Bean
 * @author spirit   
 * @date 2018年5月15日 上午11:21:11 
 * @email spirit612@sina.cn
 */
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface MyLoadBalanced {}

接着编写一个Spring的配置类MyConfiguration,实现一个方法,收集我们自定义注解@MyLoadBalanced修饰的RestTemplate类,为了能够看到这种类的具体数目,我们再创建一个类,让它在初始化的时候,就能返回这个数目。

package org.init.springcloud;

import java.util.Collections;
import java.util.List;

import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class MyConfiguration {

	@Autowired(required = false)
	@MyLoadBalanced
	private List<RestTemplate> tpls = Collections.emptyList();
	
	//在初始化之后去创建一个实例bean
	@Bean
	public SmartInitializingSingleton loadBalanceInit(){
		return () -> {
			System.out.println(tpls.size());
		};
	}
	
}

这个时候运行MyApplication类的main方法,启动项目,我们就可以看到控制台打印出了数目:

这个时候当然是不可能有具体数目的,因为我们还没有用自定义注解修饰的RestTemplate。再新建一个控制器MyController,并且用自定义注解去修饰一个RestTemplate,让他在初始化的时候,就拥有我们自定义注解的功能:

package org.init.springcloud;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@Configuration
public class MyController {

	@Bean
	@MyLoadBalanced
	public RestTemplate MyRestTemplate(){
		return new RestTemplate();
	}
	
}

再次启动项目,查看控制台输出:

被@MyLoadBalanced修饰的RestTemplate类还会被加上一些拦截器,这些拦截器是继承了ClientHttpRequestInterceptor接口(这个接口是Spring的内容,有困惑的读者可以自行查阅文档了解)的子类修饰的,Ribbon的底层就是用这些拦截器处理得到的请求,然后用自己特定的一些算法来实现负载均衡,我们这里也定义一个自己的拦截器类MyInterceptor,同样去实现ClientHttpRequestInterceptor接口:

package org.init.springcloud;

import java.io.IOException;

import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;

public class MyInterceptor implements ClientHttpRequestInterceptor {

	@Override
	public ClientHttpResponse intercept(HttpRequest request, byte[] body,
			ClientHttpRequestExecution execute) throws IOException {
		System.out.println("这是我们自定义的拦截器");
		System.out.println("请求的URI地址是:"+request.getURI());
		return execute.execute(request, body);
	}

}

然后是修改配置类MyConfiguration的loadBalanceInit方法,让RestTemplate类添加我们自定义的拦截器:

	//在初始化之后去创建一个实例bean
	@Bean
	public SmartInitializingSingleton loadBalanceInit(){
		return () -> {
			System.out.println("被@MyLoadBalanced修饰的RestTemplate Bean的数目:"+tpls.size());
			for (RestTemplate tpl : tpls) {
				List<ClientHttpRequestInterceptor> interceptors = tpl.getInterceptors();
				MyInterceptor myInterceptor = new MyInterceptor();
				interceptors.add(myInterceptor);
			}
		};
	}

为了不让RestTemplate以前的拦截器丢失,我们先获取了它原来所有的拦截器,之后再添加了一个自定义拦截器,紧接着就可以编写访问方法,测试我们添加的拦截器是否有效了。在控制器MyController里面添加两个方法,一个用于浏览器地址访问,一个用于RestTemplate内部请求:

	@GetMapping(value = "/router")
	@ResponseBody
	public String router(){
		RestTemplate temp = MyRestTemplate();
		return temp.getForObject("http://localhost:8080/invoke", String.class);
	}
	
	@RequestMapping(method = RequestMethod.GET, value = "/invoke", produces = MediaType.APPLICATION_JSON_VALUE)
	public String invoke(){
		return "调用invoke方法";
	}

启动项目,访问http://localhost:8080/router,我们可以看到浏览器返回了字符串,控制台也打印出了拦截器的信息:

可以看出我们的拦截器是生效了,接下来我们就简单模仿一下Ribbon的做法,实现一个低配版本的负载均衡,当然,我们不做那么复杂,仅仅是实现一个请求转发就OK了。新建一个MyHttpRequest类,实现HttpRequest接口,为这个类添加一个构造器,用于传入外部的Http请求,然后改造实现的三个方法,对headers和body部分都不做处理,我们只把这个请求的URI路径给改成一个自定义的地址:http://localhost:8080/forward

package org.init.springcloud;

import java.net.URI;
import java.net.URISyntaxException;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;

public class MyHttpRequest implements HttpRequest {

	private HttpRequest httpRequest;
	
	public MyHttpRequest(HttpRequest httpRequest){
		this.httpRequest = httpRequest;
	}
	
	@Override
	public HttpHeaders getHeaders() {
		return httpRequest.getHeaders();
	}

	@Override
	public HttpMethod getMethod() {
		return httpRequest.getMethod();
	}

	@Override
	public URI getURI() {
		try {
			URI myURI = new URI("http://localhost:8080/forward");
			return myURI;
		} catch (URISyntaxException e) {
			e.printStackTrace();
		}
		return httpRequest.getURI();
	}

}

然后在MyInterceptor拦截器里把请求给替换成我们的请求,也就是改写请求的URI

	@Override
	public ClientHttpResponse intercept(HttpRequest request, byte[] body,
			ClientHttpRequestExecution execute) throws IOException {
		System.out.println("这是我们自定义的拦截器");
		System.out.println("请求的URI地址是:"+request.getURI());
		HttpRequest myHttpRequest = new MyHttpRequest(request);
		return execute.execute(myHttpRequest, body);
	}

最后去MyController控制器里,添加一个方法,也就是我们前面路径里的方法:

@RequestMapping(method = RequestMethod.GET, value = "/forward", produces = MediaType.APPLICATION_JSON_VALUE)
public String forward(){
	return "调用forward方法";
}
	return "调用forward方法";
}

重启项目,访问http://localhost:8080/router,查看浏览器输出字符串和控制台输出:

我们可以看到,请求的确实是invoke方法,但是已经被转发到forward方法上了,Ribbon底层的做法和这个类似,只不过它拥有一套更完整的算法,来帮助我们处理服务请求的分发处理。

最后,大家有什么不懂的或者其他需要交流的内容,也可以进入我的QQ讨论群一起讨论:654331206

Spring Cloud系列:

Spring Cloud介绍与环境搭建(一)

Spring Boot的简单使用(二)

Spring Cloud服务管理框架Eureka简单示例(三)

Spring Cloud服务管理框架Eureka项目集群(四)

Spring Cloud之Eureka客户端健康检测(五)

Netflix之第一个Ribbon程序(六)

Ribbon负载均衡器详细介绍(七)

Spring Cloud中使用Ribbon(八)

具有负载均衡功能的RestTemplate底层原理(九)

OpenFeign之第一个Feign程序(十)

OpenFeign之feign使用简介(十一)

Spring Cloud中使用Feign(十二)

Netflix之第一个Hystrix程序(十三)

Netflix之Hystrix详细分析(十四)

Spring Cloud中使用Hystrix(十五)

Netflix之第一个Zuul程序(十六)

Spring Cloud集群中使用Zuul(十七)

Netflix之Zuul的进阶应用(十八)

消息驱动之背景概述(十九)

消息中间件之RabbitMQ入门讲解(二十)

消息中间件之Kafka入门讲解(二十一)

Spring Cloud整合RabbitMQ或Kafka消息驱动(二十二)

### Ribbon 实现本地负载均衡原理 Ribbon 是 Netflix 开发的一个客户端负载均衡器,能够帮助服务消费者更灵活地路由请求到合适的服务实例上。在微服务架构中,当某个服务有多个实例时,Ribbon 可以自动选择最合适的目标服务器来发起 HTTP 请求。 #### 工作机制 Ribbon 的核心在于它能够在运行期间动态获取目标服务的所有可用实例列表,并基于预设的负载均衡策略决定向哪个实例发送请求。这一过程发生在客户端侧,因此被称为“本地负载均衡”。具体来说: - **服务发现**:借助 Eureka 或 Consul 等注册中心,Ribbon 定期更新所依赖服务的有效地址集合。 - **负载分配决策**:每当应用程序准备发出远程调用之前,Ribbon 就会依据当前配置好的规则(如轮询、随机选取等),挑选出最佳候选者作为实际通信对象[^2]。 #### 默认负载均衡算法 默认情况下,Ribbon 使用 `ZoneAvoidanceRule` 作为其内置的主要调度逻辑,这是一种结合了区域感知特性的改进型轮询法。该方法不仅考虑到了各个节点之间的地理位置关系,还综合评估了它们各自的健康状况等因素,在此基础上做出最优路径规划[^4]。 ### 使用方法 为了使 Spring Cloud 应用程序具备上述功能特性,开发者只需简单几步即可完成集成操作: 1. 添加 Maven/Gradle 依赖项引入必要的库文件; 2. 配置 application.yml 文件指定相关参数选项; 3. 对 RestTemplate 组件标注特定注解以便激活插件支持; 以下是具体的代码示例展示如何快速启用此特性: ```java @Configuration public class AppConfig { @Bean @LoadBalanced // 启动负载均衡能力 public RestTemplate restTemplate(){ return new RestTemplate(); } } ``` 通过以上设置之后,就可以像平常一样利用 RestTemplate 发起对外部资源的访问请求了,而无需关心底层复杂的选路细节[^3]。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值