Spring cloud负载均衡 @LoadBalanced注解原理

news2024/11/29 11:48:16

接上一篇文章,案例代码也在上一篇文章的基础上。

在上一篇文章的案例中,我们创建了作为Eureka server的Eureka注册中心服务、作为Eureka client的userservice、orderservice。

orderservice引入RestTemplate,加入了@LoadBalanced注解,代码如下:

package com;

@SpringBootApplication
@EnableEurekaClient
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class);
    }

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }

}

从而,我们实现了基于Eureka注册中心的微服务治理框架,在orderservice调用userservice的时候通过加了@LoadBalanced注解的RestTemplate实现了负载均衡。

今天的目标是:深入研究@LoadBalanced生效的底层原理。

@LoadBalanced是怎么实现负载均衡的?

我们要回答的第一个问题是,为什么@LoadBalanced能实现负载均衡?

我们从代码的源头一路追查下去…

orderservice通过RestTemplate实现对userservice的调用代码:

@Service
public class OrderService {
    @Autowired
    private RestTemplate restTemplate;
    public String getOrder(){
        //通过userService获取user信息
        String url="http://userservice/user/getUser";
        System.out.println("url"+url);
        User user=restTemplate.getForObject(url,User.class);
        System.out.println(user);
        return user.getName();
    }
}

很容易的,我们需要有一个认知:这里访问的地址http://userservice/user/getUser只可能在Spring cloud服务治理环境下有意义,最终能访问到我们发布到本机上的userservice的如下服务:

1. http://localhost:8080
2. http://localhost:8081
3. http://localhost:8082

必定需要借助Spring Cloud的某一机制将http://userservice转换为上述地址之一。这个转换过程,也就是Spring Cloud的负载均衡机制。

跟踪getForObject:

	@Override
	@Nullable
	public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException {
		RequestCallback requestCallback = acceptHeaderRequestCallback(responseType);
		HttpMessageConverterExtractor<T> responseExtractor =
				new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger);
		return execute(url, HttpMethod.GET, requestCallback, responseExtractor, uriVariables);
	}

调用到execute方法:

	@Override
	@Nullable
	public <T> T execute(String url, HttpMethod method, @Nullable RequestCallback requestCallback,
			@Nullable ResponseExtractor<T> responseExtractor, Object... uriVariables) throws RestClientException {

		URI expanded = getUriTemplateHandler().expand(url, uriVariables);
		return doExecute(expanded, method, requestCallback, responseExtractor);
	}

doExecute方法:

@Nullable
	protected <T> T doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback,
			@Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {

		Assert.notNull(url, "URI is required");
		Assert.notNull(method, "HttpMethod is required");
		ClientHttpResponse response = null;
		try {
			ClientHttpRequest request = createRequest(url, method);
			if (requestCallback != null) {
				requestCallback.doWithRequest(request);
			}
			response = request.execute();
			handleResponse(url, method, response);
			return (responseExtractor != null ? responseExtractor.extractData(response) : null);
		}
		catch (IOException ex) {
			String resource = url.toString();
			String query = url.getRawQuery();
			resource = (query != null ? resource.substring(0, resource.indexOf('?')) : resource);
			throw new ResourceAccessException("I/O error on " + method.name() +
					" request for \"" + resource + "\": " + ex.getMessage(), ex);
		}
		finally {
			if (response != null) {
				response.close();
			}
		}
	}

createRequest方法:

	protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOException {
		ClientHttpRequest request = getRequestFactory().createRequest(url, method);
		initialize(request);
		if (logger.isDebugEnabled()) {
			logger.debug("HTTP " + method.name() + " " + url);
		}
		return request;
	}

最关键的部分来了,就是这个 getRequestFactory()方法,在RestTemplate的父类InterceptingHttpAccessor中定义:

    private final List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();


    public ClientHttpRequestFactory getRequestFactory() {
		List<ClientHttpRequestInterceptor> interceptors = getInterceptors();
		if (!CollectionUtils.isEmpty(interceptors)) {
			ClientHttpRequestFactory factory = this.interceptingRequestFactory;
			if (factory == null) {
				factory = new InterceptingClientHttpRequestFactory(super.getRequestFactory(), interceptors);
				this.interceptingRequestFactory = factory;
			}
			return factory;
		}
		else {
			return super.getRequestFactory();
		}
	}

	public List<ClientHttpRequestInterceptor> getInterceptors() {
		return this.interceptors;
	}

首先通过getInterceptor()方法检查是否有有拦截器,拦截器interceptors是由ClientHttpRequestInterceptor组成的一个list。如果有的话,就会创建InterceptingClientHttpRequestFactory、并且将拦截器interceptors送给InterceptingClientHttpRequestFactory工厂之后,返回工厂InterceptingClientHttpRequestFactory。

然后,方法调用返回到createRequest方法中,调用InterceptingClientHttpRequestFactory的createRequest方法,最终会调用到:

	@Override
	protected ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod, ClientHttpRequestFactory requestFactory) {
		return new InterceptingClientHttpRequest(requestFactory, this.interceptors, uri, httpMethod);
	}

方法最终返回的是InterceptingClientHttpRequest,并且,会将工厂InterceptingClientHttpRequestFactory持有的interceptors传递给InterceptingClientHttpRequest对象:

protected InterceptingClientHttpRequest(ClientHttpRequestFactory requestFactory,
			List<ClientHttpRequestInterceptor> interceptors, URI uri, HttpMethod method) {

		this.requestFactory = requestFactory;
		this.interceptors = interceptors;
		this.method = method;
		this.uri = uri;
	}

之后返回到doExecute方法中,会调用InterceptingClientHttpRequest的父类AbstractClientHttpRequest类的execute方法、之后又转回到InterceptingClientHttpRequest类的executeInternal方法:

	@Override
	protected final ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException {
		InterceptingRequestExecution requestExecution = new InterceptingRequestExecution();
		return requestExecution.execute(this, bufferedOutput);
	}

executeInternal方法创建了InterceptingClientHttpRequest内部类InterceptingRequestExecution对象之后,调用内部对象的execute方法。

查看内部类InterceptingRequestExecution,不难发现,他拿到了宿主类InterceptingClientHttpRequest的拦截器interceptors的迭代器:

private class InterceptingRequestExecution implements ClientHttpRequestExecution {

		private final Iterator<ClientHttpRequestInterceptor> iterator;

		public InterceptingRequestExecution() {
			this.iterator = interceptors.iterator();
		}

		@Override
		public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
			if (this.iterator.hasNext()) {
				ClientHttpRequestInterceptor nextInterceptor = this.iterator.next();
				return nextInterceptor.intercept(request, body, this);
			}
			else {
				HttpMethod method = request.getMethod();
				Assert.state(method != null, "No standard HTTP method");
				ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), method);
				request.getHeaders().forEach((key, value) -> delegate.getHeaders().addAll(key, value));
				if (body.length > 0) {
					if (delegate instanceof StreamingHttpOutputMessage) {
						StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) delegate;
						streamingOutputMessage.setBody(outputStream -> StreamUtils.copy(body, outputStream));
					}
					else {
						StreamUtils.copy(body, delegate.getBody());
					}
				}
				return delegate.execute();
			}
		}
	}

然后在execute方法中首先遍历该迭代器iterator并调用迭代器对象ClientHttpRequestInterceptor的intercept方法。

拦截器ClientHttpRequestInterceptor就是实现负载均衡的关键所在,Spring正是在拦截器ClientHttpRequestInterceptor的intercept方法中,完成了负载均衡的实现:将请求中的服务名称比如本案例中的userservice、替换成了由Eureka注册中心下发下来的具体的userservice服务器(比如http://127.0.0.1:8081)

我们发现最关键的部分就是RestTemplate对象中的拦截器interceptors。

接下来的问题就是:interceptors是什么时候、怎么创建出来的?

拦截器的初始化

从RestTemplate的父类InterceptingHttpAccessor简单追踪一下,不难发现他的interceptors是通过setInterceptors方法赋值的,然后借助开发工具的帮助:
在这里插入图片描述
发现LoadBalanceAutoConfiguration调用了setInterceptors方法。

这个LoadBalanceAutoConfiguration的命名方式是不是很熟悉啊?我们前面分析过SpringBoot的自动配置,就是各种xxxxAutoConfiguration命名的类负责具体的自动配置任务的。

简单了解了下,LoadBalanceAutoConfiguration就是SpringBoot的自动配置类。我们找到LoadBalanceAutoConfiguration类在spring-cloud-commons包下,按图索骥,我们找到了包下的/META-INF/spring.factories文件:
在这里插入图片描述
因此我们知道,SpringBoot的自动配置机制会通过调用LoadBalanceAutoConfiguration类来完成LoadBalance的相关初始化工作。
所以我们接下来的工作就是要研究LoadBalanceAutoConfiguration。

LoadBalanceAutoConfiguration

第一步,从LoadBalanceAutoConfiguration类的源码知道他要通过restTemplateCustomizer方法加载一个RestTemplateCustomizer对象,方法需要一个参数LoadBalancerInterceptor:

		@Bean
		@ConditionalOnMissingBean
		public RestTemplateCustomizer restTemplateCustomizer(final LoadBalancerInterceptor loadBalancerInterceptor) {
			return restTemplate -> {
				List<ClientHttpRequestInterceptor> list = new ArrayList<>(restTemplate.getInterceptors());
				list.add(loadBalancerInterceptor);
				restTemplate.setInterceptors(list);
			};
		}

先看一眼RestTemplateCustomizer类,只有一个customize方法,该方法有一个参数RestTemplate:

public interface RestTemplateCustomizer {

	void customize(RestTemplate restTemplate);

}

这个customize方法已经在restTemplateCustomizer方法中通过lamda表达式定义出来了,代码逻辑很简单:

就是要将LoadBalancerInterceptor拦截器对象通过调用RestTemplate的setInterceptors方法加入到RestTemplate的interceptors中!

似乎快要摸到开关了!!!

那么我们现在又冒出了以下几个问题:

  1. 这个LoadBalancerInterceptor从哪里来?
  2. RestTemplate从哪里来?
  3. 什么时候调用这个已经注入到Spring Ioc容器中的RestTemplateCustomizer的customize方法?

第1、第2两个问题其实很简单。看代码:

    @Configuration(proxyBeanMethods = false)
	@Conditional(RetryMissingOrDisabledCondition.class)
	static class LoadBalancerInterceptorConfig {

		@Bean
		public LoadBalancerInterceptor loadBalancerInterceptor(LoadBalancerClient loadBalancerClient,
				LoadBalancerRequestFactory requestFactory) {
			return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
		}
		...
}

LoadBalancerAutoConfiguration类中有一个静态配置类LoadBalancerInterceptorConfig,通过loadBalancerInterceptor方法注入了LoadBalancerInterceptor 对象,创建对象的时候通过参数注入了loadBalancerClient和LoadBalancerRequestFactory。

第2个问题,RestTemplate的注入,其实是我们从应用层通过@Bean注入的,同时加了@LoadBalanced注解。

现在我们来回答第3个问题:

什么时候调用这个已经注入到Spring Ioc容器中的RestTemplateCustomizer的customize方法?

比前两个问题稍稍复杂了一点:

	@LoadBalanced
	@Autowired(required = false)
	private List<RestTemplate> restTemplates = Collections.emptyList();
	
	@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);
				}
			}
		});
	}

首先看到标注了@Autowire和@LoadBalanced注解的RestTemplates列表的注入,意思是: 如果有加了@LoadBalanced注解的RestTemplates bean的话,就自动装配到restTemplates 变量中。

之后,通过loadBalancedRestTemplateInitializerDeprecated方法注入了一个SmartInitializingSingleton,我们知道SmartInitializingSingleton在装配到Spring Ioc之后会调用他的afterSingletonsInstantiated()方法。这里注入的SmartInitializingSingleton通过lamda实现了afterSingletonsInstantiated()方法,代码逻辑:通过方法参数打包注入到Ioc容器中的RestTemplateCustomizer(前面讲过了,他已经注入到IoC容器中了)到restTemplateCustomizers中,然后遍历restTemplates(这个list在前面说过了,已经把我们通过@Bean和@LoadBalanced注解的RestTemplate对象注入进来了)、针对每一个RestTemplate再遍历restTemplateCustomizers中的RestTemplateCustomizer对象,逐个调用他的customize方法。

OK!对于@LoadBalanced注解从应用、到初始化、生效机制,我们就分析清楚了。

最后还遗留两个小问题,初始化和应用两端各一个:初始化过程中的装配到loadBalancerInterceptor对象中的LoadBalancerClient具体是什么对象、什么时候注入的?应用端最终的负载均衡策略、负载均衡实现逻辑,我们还没具体分析。

下一篇文章解决上述两个问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1166141.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

新时代备考利器 | 国考路上的得力助手,拓世AI为你打造智能学习之路!

随着人工智能&#xff08;AI&#xff09;和大数据时代的到来&#xff0c;其影响已经全面席卷全球&#xff0c;对于人才的需求和培养提出了全新的挑战和要求。这股潮流推动着教育领域逐步迎来深刻的改革和变革&#xff0c;旨在培养适应未来社会发展需求的多元化人才。同时&#…

react+canvas实现横跨整个页面的动态的波浪线(贝塞尔曲线)

本来写这个特效 我打算用css实现的&#xff0c;结果是一波三折&#xff0c;我太难了&#xff0c;最终没能用css实现&#xff0c;转战了canvas来实现。来吧先看效果图 当然这个图的波浪高度、频率、位置、速度都是可调的&#xff0c;请根据自己的需求调整&#xff0c;如果你讲波…

Spring Data Redis + RabbitMQ - 基于 string 实现缓存、计数功能(同步数据)

目录 一、Spring Data Redis 1.1、缓存功能 1.1.1、分析 1.1.2、案例实现 1.1.3、效果演示 1.2、计数功能&#xff08;Redis RabbitMQ&#xff09; 1.2.1、分析 1.2.2、案例实现 一、Spring Data Redis 1.1、缓存功能 1.1.1、分析 使用 redis 作为缓存&#xff0c; M…

EEG公开数据集介绍

EEG公开数据集介绍 0 引言1 项目简单介绍1.1 运动想象1.2 情绪识别1.3 误差相关电位 &#xff08;ErrP&#xff09;1.4 视觉诱发电位 &#xff08;VEP&#xff09;1.5 事件相关电位 [ERP]1.6 慢皮质电位 &#xff08;SCP&#xff09;1.7 静息状态1.8 音乐和脑电图1.9 眨眼/运动…

ruby语言怎么写个通用爬虫程序?

Ruby语言爬虫是指使用Ruby编写的网络爬虫程序&#xff0c;用于自动化地从互联网上获取数据。其中&#xff0c;CRawler是一个基于文本的小型地牢爬虫&#xff0c;它被设计为可扩展&#xff0c;所有游戏数据均通过JSON文件提供&#xff0c;程序仅处理游戏引擎。除此之外&#xff…

企业服务器数据库中了mkp勒索病毒怎么解决,勒索病毒解密数据恢复

在近期&#xff0c;云天数据恢复中心陆续接到很多企业的求助&#xff0c;企业的服务器数据库遭到了勒索病毒攻击&#xff0c;导致企业的所有重要数据被加密。尤其是从10月份以来&#xff0c;勒索病毒的攻击频率越来越高&#xff0c;有很多企业都是二次被攻击&#xff0c;尤其是…

视频特效制作After Effects 2024 for Mac(ae)

After Effects 2024是一款由Adobe公司开发的专业的视频特效和动态图形设计软件&#xff0c;它可以帮助用户创建各种令人惊叹的视觉效果&#xff0c;例如粒子系统、合成特效、绿屏抠像等。该软件支持动画制作&#xff0c;包括关键帧动画、形状动画、运动跟踪等工具&#xff0c;可…

高浓度cod废水怎么处理

高浓度COD废水的处理方法主要有物理法、生物法和化学法。 物理法&#xff1a;一般通过加入絮凝剂&#xff0c;利用絮凝剂的吸附、电中和等作用将水中的颗粒物结团沉降下去&#xff0c;从而达到去除部分来自颗粒物的COD。此方法基本上只对浓度上万、上千的COD起作用&#xff0c…

跑步耳机哪个牌子好?最适合跑步用的耳机排名

在各式各样的耳机当中&#xff0c;运动耳机可以说是使用场景最广的一类了。毕竟运动耳机对于某些方面性能要求会比非运动耳机要高很多&#xff0c;就比如户外运动、健身、骑行等方面。面对这么多运动耳机&#xff0c;哪款更值得入手&#xff1f;今天就来给大家推荐几款很不错的…

力扣 寻找旋转排序数组中的最小值 二分

&#x1f468;‍&#x1f3eb; 题目地址 &#x1f338; AC code class Solution { public int findMin(int[] nums){int res Integer.MAX_VALUE;int l 0;int r nums.length - 1;while (l < r){int mid l r >> 1;if (nums[mid] < res)res nums[mid];//中值 &…

What is 哈希?

哈希 ​ 前言&#xff1a;大一大二就一直听说哈希哈希&#xff0c;但一直都没有真正的概念&#xff1a;What is 哈希&#xff1f;这篇博客就浅浅聊一下作者认知中的哈希。 理解哈希 ​ 哈希&#xff08;Hash&#xff09;也可以称作散列&#xff0c;实质就是一种映射&#xf…

git更改远程仓库地址

1、输入命令【git remote -v】查看当前git远程仓库地址 2、输入命令【git remote set-url origin 新地址】替换成新地址 3、输入命令【git remote -v 】查看是否更新成功

Odoo 网站主题开发指南

Odoo 网站主题开发指南 下载根据本指南开发的主题模块源码 Odoo 网站生成器是一个灵活的工具&#xff0c;可以轻松构建与 Odoo 应用完全集成的网站。使用其提供的主题选项 (options) 和构建块 (blocks) 很容易定制网站。然而&#xff0c;你还可以更进一步深度定制。在本文中&a…

Zinx框架-游戏服务器开发002:按照三层结构模式重构代码功能-待续

文章目录 1 Zinx框架总览2 三层模式的分析3 三层重构原有的功能 - 头文件3.1 通道层Stdin和Stdout类3.1.2 StdInChannel3.1.2 StdOutChannel 3.2 协议层CmdCheck和CmdMsg类3.2.1 CmdCheck单例模式3.2.1.1 单例模式3.2.1.2 * 命令识别类向业务层不同类别做分发 3.2.2 CmdMsg自定…

Python+pandas将Excel文件xlsx批量转换xls(代码全注释)

文章目录 专栏导读背景安装的库代码部分(全注释)视频演示总结&#x1f44d; 该系列文章专栏&#xff1a;[Python办公自动化专栏]PS: xls转xlsx文章在这&#xff1a;【点我直达】 专栏导读 &#x1f338; 欢迎来到Python办公自动化专栏—Python处理办公问题&#xff0c;解放您的…

MySQL缩短查询时间小技巧

背景 今天我要统计数据表的最新更新时间&#xff0c;有些表数据量特别大&#xff0c;所以统计比较费时间&#xff0c;但是如果使用一下小技巧&#xff0c;就会极大加快查询时间&#xff0c;适合小白的调优手段。 查询更新时间 select max(update_time) from test大概表的行数…

外汇天眼:账户惨遭冻结?一招帮你解决问题!

在外汇交易中&#xff0c;难免会出现大大小小的问题&#xff0c;其中账户被冻结也是经常能够见到的。首先摒弃外汇黑平台&#xff0c;在正规的外汇平台&#xff0c;如果出现账户被冻结的问题&#xff0c;可能是以下几种原因&#xff1a; 第一种是使用平台禁止的辅助软件进行薅…

python-opencv写入视频文件无法播放

python-opencv写入视频文件无法播放 在采用Python写OpenCV的视频时&#xff0c;生成的视频总是无法播放&#xff0c;大小只有不到两百k&#xff0c;播放器提示视频已经损坏。网上搜了一些方法&#xff0c;记录下解决办法。 代码如下 fourcc cv2.VideoWriter_fourcc(*MJPG) fp…

CRM客户关系管理系统源码 PHP客户管理系统源码

CRM客户关系管理系统源码 PHP客户管理系统源码 系统采用&#xff1a;PHPMYSQL 开发&#xff0c;功能完善&#xff0c;界面美观。 功能介绍&#xff1a; 1.客户管理&#xff1a;客户列表、今日新增客户、近7天新增客户、本月新增客户、新增客户&#xff08;包括客户名称、所在…

青柚课堂|为3960余名学生解惑青春期

2023年10月30日-11月1日&#xff0c;益阳市海棠学校邀请益阳市蚂蚁社会工作服务中心的儿童性教育公益讲师们开展了以“我们的青春期”为主题的“青柚课堂”性教育公益讲座&#xff0c;授课初中部班级72个&#xff0c;受益青少年3960余人。 在热身游戏的活跃氛围中&#xff0c;讲…