User-Profile-Image
hankin
  • 5
  • Java
  • Kotlin
  • Spring
  • Web
  • SQL
  • MegaData
  • More
  • Experience
  • Enamiĝu al vi
  • 分类
    • Zuul
    • Zookeeper
    • XML
    • WebSocket
    • Web Notes
    • Web
    • Vue
    • Thymeleaf
    • SQL Server
    • SQL Notes
    • SQL
    • SpringSecurity
    • SpringMVC
    • SpringJPA
    • SpringCloud
    • SpringBoot
    • Spring Notes
    • Spring
    • Servlet
    • Ribbon
    • Redis
    • RabbitMQ
    • Python
    • PostgreSQL
    • OAuth2
    • NOSQL
    • Netty
    • MySQL
    • MyBatis
    • More
    • MinIO
    • MegaData
    • Maven
    • LoadBalancer
    • Kotlin Notes
    • Kotlin
    • Kafka
    • jQuery
    • JavaScript
    • Java Notes
    • Java
    • Hystrix
    • Git
    • Gateway
    • Freemarker
    • Feign
    • Eureka
    • ElasticSearch
    • Docker
    • Consul
    • Ajax
    • ActiveMQ
  • 页面
    • 归档
    • 摘要
    • 杂图
    • 问题随笔
  • 友链
    • Spring Cloud Alibaba
    • Spring Cloud Alibaba - 指南
    • Spring Cloud
    • Nacos
    • Docker
    • ElasticSearch
    • Kotlin中文版
    • Kotlin易百
    • KotlinWeb3
    • KotlinNhooo
    • 前端开源搜索
    • Ktorm ORM
    • Ktorm-KSP
    • Ebean ORM
    • Maven
    • 江南一点雨
    • 江南国际站
    • 设计模式
    • 熊猫大佬
    • java学习
    • kotlin函数查询
    • Istio 服务网格
    • istio
    • Ktor 异步 Web 框架
    • PostGis
    • kuangstudy
    • 源码地图
    • it教程吧
    • Arthas-JVM调优
    • Electron
    • bugstack虫洞栈
    • github大佬宝典
    • Sa-Token
    • 前端技术胖
    • bennyhuo-Kt大佬
    • Rickiyang博客
    • 李大辉大佬博客
    • KOIN
    • SQLDelight
    • Exposed-Kt-ORM
    • Javalin—Web 框架
    • http4k—HTTP包
    • 爱威尔大佬
    • 小土豆
    • 小胖哥安全框架
    • 负雪明烛刷题
    • Kotlin-FP-Arrow
    • Lua参考手册
    • 美团文章
    • Java 全栈知识体系
    • 尼恩架构师学习
    • 现代 JavaScript 教程
    • GO相关文档
    • Go学习导航
    • GoCN社区
    • GO极客兔兔-案例
    • 讯飞星火GPT
    • Hollis博客
    • PostgreSQL德哥
    • 优质博客推荐
    • 半兽人大佬
    • 系列教程
    • PostgreSQL文章
    • 云原生资料库
    • 并发博客大佬
Help?

Please contact us on our email for need any support

Support
    首页   ›   Spring   ›   SpringCloud   ›   Ribbon   ›   正文
Ribbon

@LoadBalanced注解为何就让RestTemplate拥有负载均衡的能力?(Ribbon)

2020-08-21 01:51:19
1960  0 0
参考目录 隐藏
1) RibbonAutoConfiguration
2) @LoadBalanced
3) LoadBalancerAutoConfiguration
4) LoadBalancerInterceptor
5) LoadBalancerClient
6) RibbonLoadBalancerClient
7) 拓展:ILoadBalancer 接口:
8) 详细:
9) 使用细节
10) 本地测试
11) 自己添加一个记录请求日志的拦截器可行吗?

阅读完需:约 23 分钟

在Spring Cloud微服务应用体系中,远程调用都应负载均衡。我们在使用RestTemplate作为远程调用客户端的时候,开启负载均衡极其简单:一个@LoadBalanced注解就搞定了。
相信使用过Ribbon做Client端的负载均衡,也许你有和我一样的感受:Ribbon虽强大但不是特别的好用。我研究了一番,其实根源还是我们对它内部的原理不够了解,导致对一些现象无法给出合理解释,同时也影响了我们对它的定制和扩展。本文就针对此做出梳理,希望大家通过本文也能够对Ribbon有一个较为清晰的理解(本文只解释它@LoadBalanced这一小块内容)。

开启客户端负载均衡只需要一个注解即可,形如这样:

@LoadBalanced // 标注此注解后,RestTemplate就具有了客户端负载均衡能力
@Bean
public RestTemplate restTemplate(){
    return new RestTemplate();
}

说Spring是Java界最优秀、最杰出的重复发明轮子作品一点都不为过。本文就代领你一探究竟,为何开启RestTemplate的负载均衡如此简单。

可以回顾前面关于 RestTemplate 的内容:

Spring—RestTemplate的使用和原理

RibbonAutoConfiguration

这是Spring Boot/Cloud启动Ribbon的入口自动配置类,需要先有个大概的了解:

@Configuration
// 类路径存在com.netflix.client.IClient、RestTemplate等时生效
@Conditional(RibbonAutoConfiguration.RibbonClassesConditions.class) 
// // 允许在单个类中使用多个@RibbonClient
@RibbonClients 
// 若有Eureka,那就在Eureka配置好后再配置它~~~(如果是别的注册中心呢,ribbon还能玩吗?)
@AutoConfigureAfter(name = "org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration")
@AutoConfigureBefore({ LoadBalancerAutoConfiguration.class, AsyncLoadBalancerAutoConfiguration.class })
// 加载配置:ribbon.eager-load --> true的话,那么项目启动的时候就会把Client初始化好,避免第一次惩罚
@EnableConfigurationProperties({ RibbonEagerLoadProperties.class, ServerIntrospectorProperties.class })
public class RibbonAutoConfiguration {

	@Autowired
	private RibbonEagerLoadProperties ribbonEagerLoadProperties;
	// Ribbon的配置文件们~~~~~~~(复杂且重要)
	@Autowired(required = false)
	private List<RibbonClientSpecification> configurations = new ArrayList<>();

	// 特征,FeaturesEndpoint这个端点(`/actuator/features`)会使用它org.springframework.cloud.client.actuator.HasFeatures
	@Bean
	public HasFeatures ribbonFeature() {
		return HasFeatures.namedFeature("Ribbon", Ribbon.class);
	}


	// 它是最为重要的,是一个org.springframework.cloud.context.named.NamedContextFactory  此工厂用于创建命名的Spring容器
	// 这里传入配置文件,每个不同命名空间就会创建一个新的容器(和Feign特别像) 设置当前容器为父容器
	@Bean
	public SpringClientFactory springClientFactory() {
		SpringClientFactory factory = new SpringClientFactory();
		factory.setConfigurations(this.configurations);
		return factory;
	}

	// 这个Bean是关键,若你没定义,就用系统默认提供的Client了~~~
	// 内部使用和持有了SpringClientFactory。。。
	@Bean
	@ConditionalOnMissingBean(LoadBalancerClient.class)
	public LoadBalancerClient loadBalancerClient() {
		return new RibbonLoadBalancerClient(springClientFactory());
	}
	...
}

这个配置类最重要的是完成了Ribbon相关组件的自动配置,有了LoadBalancerClient才能做负载均衡(这里使用的是它的唯一实现类RibbonLoadBalancerClient)


@LoadBalanced

注解本身及其简单(一个属性都木有):

// 所在包是org.springframework.cloud.client.loadbalancer
// 能标注在字段、方法参数、方法上
// JavaDoc上说得很清楚:它只能标注在RestTemplate上才有效
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {
}

它最大的特点:头上标注有@Qualifier注解,这是它生效的最重要因素之一,本文后半花了大篇幅介绍它的生效时机。
关于@LoadBalanced自动生效的配置,我们需要来到这个自动配置类:LoadBalancerAutoConfiguration

LoadBalancerAutoConfiguration

// Auto-configuration for Ribbon (client-side load balancing).
// 它的负载均衡技术依赖于的是Ribbon组件~
// 它所在的包是:org.springframework.cloud.client.loadbalancer
@Configuration
@ConditionalOnClass(RestTemplate.class) //可见它只对RestTemplate生效
@ConditionalOnBean(LoadBalancerClient.class) // Spring容器内必须存在这个接口的Bean才会生效(参见:RibbonAutoConfiguration)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class) // retry的配置文件
public class LoadBalancerAutoConfiguration {
	
	// 拿到容器内所有的标注有@LoadBalanced注解的Bean们
	// 注意:必须标注有@LoadBalanced注解的才行
	@LoadBalanced
	@Autowired(required = false)
	private List<RestTemplate> restTemplates = Collections.emptyList();	
	// LoadBalancerRequestTransformer接口:允许使用者把request + ServiceInstance --> 改造一下
	// Spring内部默认是没有提供任何实现类的(匿名的都木有)
	@Autowired(required = false)
	private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();

	// 配置一个匿名的SmartInitializingSingleton 此接口我们应该是熟悉的
	// 它的afterSingletonsInstantiated()方法会在所有的单例Bean初始化完成之后,再调用一个一个的处理BeanName~
	// 本处:使用配置好的所有的RestTemplateCustomizer定制器们,对所有的`RestTemplate`定制处理
	// RestTemplateCustomizer下面有个lambda的实现。若调用者有需要可以书写然后扔进容器里既生效
	// 这种定制器:若你项目中有多个RestTempalte,需要统一处理的话。写一个定制器是个不错的选择
	// (比如统一要放置一个请求拦截器:输出日志之类的)
	@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);
				}
			}
		});
	}
	
	// 这个工厂用于createRequest()创建出一个LoadBalancerRequest
	// 这个请求里面是包含LoadBalancerClient以及HttpRequest request的
	@Bean
	@ConditionalOnMissingBean
	public LoadBalancerRequestFactory loadBalancerRequestFactory(LoadBalancerClient loadBalancerClient) {
		return new LoadBalancerRequestFactory(loadBalancerClient, this.transformers);
	}
	
	// =========到目前为止还和负载均衡没啥关系==========
	// =========接下来的配置才和负载均衡有关(当然上面是基础项)==========

	// 若有Retry的包,就是另外一份配置,和这差不多~~
	@Configuration
	@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
	static class LoadBalancerInterceptorConfig {、
	
		// 这个Bean的名称叫`loadBalancerClient`,我个人觉得叫`loadBalancerInterceptor`更合适吧(虽然ribbon是唯一实现)
		// 这里直接使用的是requestFactory和Client构建一个拦截器对象
		// LoadBalancerInterceptor可是`ClientHttpRequestInterceptor`,它会介入到http.client里面去
		// LoadBalancerInterceptor也是实现负载均衡的入口,下面详解
		// Tips:这里可没有@ConditionalOnMissingBean哦~~~~
		@Bean
		public LoadBalancerInterceptor ribbonInterceptor(LoadBalancerClient loadBalancerClient, LoadBalancerRequestFactory requestFactory) {
			return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
		}
	
		
		// 向容器内放入一个RestTemplateCustomizer 定制器
		// 这个定制器的作用上面已经说了:在RestTemplate初始化完成后,应用此定制化器在**所有的实例上**
		// 这个匿名实现的逻辑超级简单:向所有的RestTemplate都塞入一个loadBalancerInterceptor 让其具备有负载均衡的能力
		
		// Tips:此处有注解@ConditionalOnMissingBean。也就是说如果调用者自己定义过RestTemplateCustomizer类型的Bean,此处是不会执行的
		// 请务必注意这点:容易让你的负载均衡不生效哦~~~~
		@Bean
		@ConditionalOnMissingBean
		public RestTemplateCustomizer restTemplateCustomizer(final LoadBalancerInterceptor loadBalancerInterceptor) {
			return restTemplate -> {
				List<ClientHttpRequestInterceptor> list = new ArrayList<>(restTemplate.getInterceptors());
				list.add(loadBalancerInterceptor);
				restTemplate.setInterceptors(list);
			};
		}
	}
	...
}

这段配置代码稍微有点长,我把流程总结为如下几步:

  1. LoadBalancerAutoConfiguration要想生效类路径必须有RestTemplate,以及Spring容器内必须有LoadBalancerClient的实现Bean
    1. LoadBalancerClient的唯一实现类是:org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient
  2. LoadBalancerInterceptor是个ClientHttpRequestInterceptor客户端请求拦截器。它的作用是在客户端发起请求之前拦截,进而实现客户端的负载均衡
  3. restTemplateCustomizer()返回的匿名定制器RestTemplateCustomizer它用来给所有的RestTemplate加上负载均衡拦截器(需要注意它的@ConditionalOnMissingBean注解~)

不难发现,负载均衡实现的核心就是一个拦截器,就是这个拦截器让一个普通的RestTemplate逆袭成为了一个具有负载均衡功能的请求器

LoadBalancerInterceptor

该类唯一被使用的地方就是LoadBalancerAutoConfiguration里配置上去~

public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {

	// 这个命名都不叫Client了,而叫loadBalancer~~~
	private LoadBalancerClient loadBalancer;
	// 用于构建出一个Request
	private LoadBalancerRequestFactory requestFactory;
	... // 省略构造函数(给这两个属性赋值)

	@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));
	}
}

当一个被@LoadBalanced注解修饰的RestTemplate对象向外发起HTTP请求时,会被LoadBalancerInterceptor类的intercept方法拦截,在这个方法中直接通过getHost方法就可以获取到服务名。

此拦截器拦截请求后把它的serviceName( 服务名 )委托给了LoadBalancerClient去执行,根据ServiceName可能对应N多个实际的Server,因此就可以从众多的Server中运用均衡算法,挑选出一个最为合适的Server做最终的请求(它持有真正的请求执行器ClientHttpRequestExecution)。


LoadBalancerClient

请求被拦截后,最终都是委托给了LoadBalancerClient处理。

// 由使用负载平衡器选择要向其发送请求的服务器的类实现
public interface ServiceInstanceChooser {

	// 从负载平衡器中为指定的服务选择Service服务实例。
	// 也就是根据调用者传入的serviceId,负载均衡的选择出一个具体的实例出来
	ServiceInstance choose(String serviceId);
}

// 它自己定义了三个方法
public interface LoadBalancerClient extends ServiceInstanceChooser {
	
	// 执行请求
	<T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;
	<T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException;
	
	// 重新构造url:把url中原来写的服务名 换掉 换成实际的
	URI reconstructURI(ServiceInstance instance, URI original);
}

它只有一个实现类RibbonLoadBalancerClient(ServiceInstanceChooser是有多个实现类的~)。

RibbonLoadBalancerClient

首先我们应当关注它的choose()方法:

public class RibbonLoadBalancerClient implements LoadBalancerClient {
	
	@Override
	public ServiceInstance choose(String serviceId) {
		return choose(serviceId, null);
	}
	// hint:你可以理解成分组。若指定了,只会在这个偏好的分组里面去均衡选择
	// 得到一个Server后,使用RibbonServer把server适配起来~~~
	// 这样一个实例就选好了~~~真正请求会落在这个实例上~
	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));
	}

        protected Server getServer(ILoadBalancer loadBalancer) {
             if (loadBalancer == null) {
                 return null;
             }
             return loadBalancer.chooseServer("default"); // TODO: betterhandling of key
    }


	// 根据ServiceId去找到一个属于它的负载均衡器
	protected ILoadBalancer getLoadBalancer(String serviceId) {
		return this.clientFactory.getLoadBalancer(serviceId);
	}

}

choose方法:传入serviceId,然后通过SpringClientFactory获取负载均衡器com.netflix.loadbalancer.ILoadBalancer,最终委托给它的chooseServer()方法选取到一个com.netflix.loadbalancer.Server实例,也就是说真正完成Server选取的是ILoadBalancer。

ILoadBalancer以及它相关的类是一个较为庞大的体系,本文不做更多的展开,而是只聚焦在我们的流程上


拓展:ILoadBalancer 接口:

public interface ILoadBalancer {
    public void addServers(List<Server> newServers);
    public Server chooseServer(Object key);
    public void markServerDown(Server server);
    public List<Server> getReachableServers();
    public List<Server> getAllServers();
}
  • 1.addServers表示向负载均衡器中维护的实例列表增加服务实例
  • 2.chooseServer表示通过某种策略,从负载均衡服务器中挑选出一个具体的服务实例
  • 3.markServerDown表示用来通知和标识负载均衡器中某个具体实例已经停止服务,否则负载均衡器在下一次获取服务实例清单前都会认为这个服务实例是正常工作的
  • 4.getReachableServers表示获取当前正常工作的服务实例列表
  • 5.getAllServers表示获取所有的服务实例列表,包括正常的服务和停止工作的服务

那么这里的几个接口都涉及到一个Server对象,这里的Server对象就是一个传统的服务端节点,这个对象中存储了服务端节点的一些元数据信息,包括host,port以及其他一些部署信息。通过下图我们可以一窥该接口的实现类:

那么在这些实现类中,BaseLoadBalancer类实现了基础的负载均衡,而DynamicServerListLoadBalancer和ZoneAwareLoadBalancer则在负载均衡的策略上做了一些功能的扩展。那么在和Ribbon整合的时候,Spring Cloud默认采用了哪个具体的实现呢?我们可以从RibbonClientConfiguration类中一窥究竟(这个类很长,我们这里只看我们关心的):

@Bean
@ConditionalOnMissingBean
public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
        ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,
        IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
    if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) {
        return this.propertiesFactory.get(ILoadBalancer.class, config, name);
    }
    return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList,
            serverListFilter, serverListUpdater);
}

OK,我们在这里看到系统默认采用了ZoneAwareLoadBalancer负载均衡器。

负载均衡器概览:

SpringCloud—负载均衡器概览


LoadBalancerInterceptor执行的时候是直接委托执行的loadBalancer.execute()这个方法:

RibbonLoadBalancerClient:

	// hint此处传值为null:一视同仁
	// 说明:LoadBalancerRequest是通过LoadBalancerRequestFactory.createRequest(request, body, execution)创建出来的
	// 它实现LoadBalancerRequest接口是用的一个匿名内部类,泛型类型是ClientHttpResponse
	// 因为最终执行的显然还是执行器:ClientHttpRequestExecution.execute()
	@Override
	public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
		return execute(serviceId, request, null);
	}
	// public方法(非接口方法)
	public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint) throws IOException {
		// 同上:拿到负载均衡器,然后拿到一个serverInstance实例
		ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
		Server server = getServer(loadBalancer, hint);
		if (server == null) { // 若没找到就直接抛出异常。这里使用的是IllegalStateException这个异常
			throw new IllegalStateException("No instances available for " + serviceId);
		}

		// 把Server适配为RibbonServer  isSecure:客户端是否安全
		// serverIntrospector内省  参考配置文件:ServerIntrospectorProperties
		RibbonServer ribbonServer = new RibbonServer(serviceId, server,
				isSecure(server, serviceId), serverIntrospector(serviceId).getMetadata(server));

		//调用本类的重载接口方法~~~~~
		return execute(serviceId, ribbonServer, request);
	}

	// 接口方法:它的参数是ServiceInstance --> 已经确定了唯一的Server实例~~~
	@Override
	public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException {
	
		// 拿到Server)(说白了,RibbonServer是execute时的唯一实现)
		Server server = null;
		if (serviceInstance instanceof RibbonServer) {
			server = ((RibbonServer) serviceInstance).getServer();
		}
		if (server == null) {
			throw new IllegalStateException("No instances available for " + serviceId);
		}

		// 说明:执行的上下文是和serviceId绑定的
		RibbonLoadBalancerContext context = this.clientFactory.getLoadBalancerContext(serviceId);
		... 
		// 真正的向server发送请求,得到返回值
		// 因为有拦截器,所以这里肯定说执行的是InterceptingRequestExecution.execute()方法
		// so会调用ServiceRequestWrapper.getURI(),从而就会调用reconstructURI()方法
			T returnVal = request.apply(serviceInstance);
			return returnVal;
		... // 异常处理
	}

returnVal是一个ClientHttpResponse,在 RestTemplate 类中 最后交给handleResponse()方法来处理异常情况(若存在的话),若无异常就交给提取器提值:responseExtractor.extractData(response),这样整个请求就算全部完成了。


详细:

此时我们重新回到RibbonLoadBalancerClient类中看我们的execute方法的执行情况,在execute方法中,当获取到一个Server对象之后,将之包装成一个RibbonServer对象(从包装的过程我们可以发现,RibbonServer对象中保存了Server的所有信息,同时还保存了服务名serviceId、是否需要HTTPS等其他信息),然后再调用另一个重载的execute方法,在另一个重载的execute方法中最终调用到了LoadBalancerRequest中的apply方法,该方法向一个具体的服务实例发送请求,从而实现了从http://服务名/hello到http://域名/hello的转换。apply方法接收了一个参数叫做ServiceInstance,这个实际上就是RibbonServer传进来的那个实例,我们查看RibbonServer,发现它其实就是ServiceInstance的一个子类,而ServiceInstance接口对象是对服务实例的抽象定义,ServiceInstance接口中暴露了服务治理体系中每个服务实例需要提供的一些基本信息,比如serviceId、host、port等,具体定义如下:

public interface ServiceInstance {
    String getServiceId();
    String getHost();
    int getPort();
    boolean isSecure();
    URI getUri();
    Map<String, String> getMetadata();
}

RibbonServer是ServiceInstance的一个子类,具体实现差不多,这里我就不贴出源码了。

这时候我们发现apply方法是LoadBalancerRequest接口中的一个方法,且LoadBalancerRequest接口没有实现类,那么apply方法的实现是在哪里实现的呢?此时我们发现LoadBalancerRequest中的apply方法在执行的时候,这个request是从LoadBalancerInterceptor拦截器里边传来的,我们再回到LoadBalancerInterceptor的intercept方法中,在这个方法中最终通过requestFactory.createRequest(request, body, execution)来创建一个LoadBalancerRequest,在这个方法中,我们找到了apply的实现:

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);
        }

    };
}

我们看到,在apply的实现中,重新创建了一个ServiceRequestWrapper,这个ServiceRequestWrapper实际上就是HttpRequestWrapper的一个子类,ServiceRequestWrapper重写了HttpRequestWrapper的getURI()方法,重写的URI实际上就是通过调用LoadBalancerClient接口的reconstructURI函数来重新构建一个URI进行访问,如下:

public class ServiceRequestWrapper extends HttpRequestWrapper {
    private final ServiceInstance instance;
    private final LoadBalancerClient loadBalancer;

    public ServiceRequestWrapper(HttpRequest request, ServiceInstance instance,
                                 LoadBalancerClient loadBalancer) {
        super(request);
        this.instance = instance;
        this.loadBalancer = loadBalancer;
    }

    @Override
    public URI getURI() {
        URI uri = this.loadBalancer.reconstructURI(
                this.instance, getRequest().getURI());
        return uri;
    }
}

此时,我们再回到RibbonLoadBalancerClient类的reconstructURI方法中,来详细的看看这里的重构过程:

@Override
public URI reconstructURI(ServiceInstance instance, URI original) {
    Assert.notNull(instance, "instance can not be null");
    String serviceId = instance.getServiceId();
    RibbonLoadBalancerContext context = this.clientFactory
            .getLoadBalancerContext(serviceId);
    Server server = new Server(instance.getHost(), instance.getPort());
    IClientConfig clientConfig = clientFactory.getClientConfig(serviceId);
    ServerIntrospector serverIntrospector = serverIntrospector(serviceId);
    URI uri = RibbonUtils.updateToHttpsIfNeeded(original, clientConfig,
            serverIntrospector, server);
    return context.reconstructURIWithServer(server, uri);
}

从reconstructURI函数中我们可以看到,首先获取到了一个serviceId,然后根据这个id获取到RibbonLoadBalancerContext对象(RibbonLoadBalancerContext类用来存储一些被负载均衡器使用的上下文内容和API操作),然后这里会根据ServiceInstance的信息来构造一个具体的服务实例信息的Server对象,最后再调用reconstructURIWithServer方法来构建服务实例的URI。好,我们再来看一看reconstructURIWithServer方法:

public URI reconstructURIWithServer(Server server, URI original) {
    String host = server.getHost();
    int port = server .getPort();
    if (host.equals(original.getHost()) 
            && port == original.getPort()) {
        return original;
    }
    String scheme = original.getScheme();
    if (scheme == null) {
        scheme = deriveSchemeAndPortFromPartialUri(original).first();
    }

    try {
        StringBuilder sb = new StringBuilder();
        sb.append(scheme).append("://");
        if (!Strings.isNullOrEmpty(original.getRawUserInfo())) {
            sb.append(original.getRawUserInfo()).append("@");
        }
        sb.append(host);
        if (port >= 0) {
            sb.append(":").append(port);
        }
        sb.append(original.getRawPath());
        if (!Strings.isNullOrEmpty(original.getRawQuery())) {
            sb.append("?").append(original.getRawQuery());
        }
        if (!Strings.isNullOrEmpty(original.getRawFragment())) {
            sb.append("#").append(original.getRawFragment());
        }
        URI newURI = new URI(sb.toString());
        return newURI;            
    } catch (URISyntaxException e) {
        throw new RuntimeException(e);
    }
}

reconstructURIWithServer函数的逻辑看起来很好理解,首先它从Server对象中获取host和port信息,然后根据以服务名为host的URI对象original中获取其他请求信息,将这两者的内容进行拼接整合,形成最终要访问的服务实例地址,至此,我们就拿到了一个组装之后的URI。

我们再回到LoadBalancerRequest类的createRequest方法,这里调用了execution.execute(serviceRequest, body)来创建了一个ClientHttpResponse对象,这里调用了ClientHttpRequestExecution接口中的execute方法,ClientHttpRequestExecution接口只有一个实现类,那就是InterceptingRequestExecution,在InterceptingRequestExecution中我们找到了execute方法的实现,如下:

@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 {
        ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), request.getMethod());
        for (Map.Entry<String, List<String>> entry : request.getHeaders().entrySet()) {
            List<String> values = entry.getValue();
            for (String value : values) {
                delegate.getHeaders().add(entry.getKey(), value);
            }
        }
        if (body.length > 0) {
            StreamUtils.copy(body, delegate.getBody());
        }
        return delegate.execute();
    }
}

小伙伴们看到,这里在创建ClientHttpRequest对象的时候,调用了request的getURI()方法,此时的getURI()已经是被重写过的URI了。

OK,至此,RestTemplate从一个简单的服务请求控件变成了具有客户端负载均衡功能的请求控件,也大概理清了Spring Cloud Ribbon中实现客户端负载均衡的基本套路了。简而言之,就是RestTemplate发起一个请求,这个请求被LoadBalancerInterceptor给拦截了,拦截后将请求的地址中的服务逻辑名转为具体的服务地址,然后继续执行请求,就是这么一个过程。


使用细节

针对@LoadBalanced下的RestTemplate的使用,我总结如下细节供以参考:

  1. 传入的String类型的url必须是绝对路径(http://...),否则抛出异常:java.lang.IllegalArgumentException: URI is not absolute
  2. serviceId不区分大小写(http://user/...效果同http://USER/...)
  3. serviceId后请不要跟port端口号了~~~

最后,需要特别指出的是:标注有@LoadBalanced的RestTemplate只能书写serviceId而不能再写IP地址/域名去发送请求了。若你的项目中两种case都有需要,请定义多个RestTemplate分别应对不同的使用场景~

本地测试

了解了它的执行流程后,若需要本地测试(不依赖于注册中心),可以这么来做:

// 因为自动配置头上有@ConditionalOnMissingBean注解,所以自定义一个覆盖它的行为即可
// 此处复写它的getServer()方法,返回一个固定的(访问百度首页)即可,方便测试
@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);
        }
    };
}

这么一来,下面这个访问结果就是百度首页的html内容喽。

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

此处my-serviceId肯定是不存在的,但得益于我上面自定义配置的LoadBalancerClient

什么,写死return一个Server实例不优雅?确实,总不能每次上线前还把这部分代码给注释掉吧,若有多个实例呢?还得自己写负载均衡算法吗?很显然Spring Cloud早早就为我们考虑到了这一点:脱离Eureka使用配置listOfServers进行客户端负载均衡调度(<clientName>.<nameSpace>.listOfServers=<comma delimited hostname:port strings>)

对于上例我只需要在主配置文件里这么配置一下:

# ribbon.eureka.enabled=false # 若没用euraka,此配置可省略。否则不可以
my-serviceId.ribbon.listOfServers=www.baidu.com # 若有多个实例请用逗号分隔

效果完全同上。

Tips:这种配置法不需要是完整的绝对路径,http://是可以省略的(new Server()方式亦可)

自己添加一个记录请求日志的拦截器可行吗?

显然是可行的,我给出示例如下:

@LoadBalanced
@Bean
public RestTemplate restTemplate() {
    RestTemplate restTemplate = new RestTemplate();
    List<ClientHttpRequestInterceptor> list = new ArrayList<>();
    list.add((request, body, execution) -> {
        System.out.println("当前请求的URL是:" + request.getURI().toString());
        return execution.execute(request, body);
    });
    restTemplate.setInterceptors(list);
    return restTemplate;
}

这样每次客户端的请求都会打印这句话:当前请求的URI是:http://my-serviceId,一般情况(缺省情况)自定义的拦截器都会在负载均衡拦截器前面执行(因为它要执行最终的请求)。若你有必要定义多个拦截器且要控制顺序,可通过Ordered系列接口来实现~

如本文“对您有用”,欢迎随意打赏作者,让我们坚持创作!

0 打赏
Enamiĝu al vi
不要为明天忧虑.因为明天自有明天的忧虑.一天的难处一天当就够了。
543文章 68评论 294点赞 594462浏览

随机文章
RabbitMQ—简单队列模式
5年前
SpringBoot—Redis缓存(Cache)
5年前
Java—数据库连接池
3年前
Kotlin-函数进阶—集合变换和序列(十七)
4年前
SpringCloud—OpenFeign(一)声明式服务调用
5年前
博客统计
  • 日志总数:543 篇
  • 评论数目:68 条
  • 建站日期:2020-03-06
  • 运行天数:1927 天
  • 标签总数:23 个
  • 最后更新:2024-12-20
Copyright © 2025 网站备案号: 浙ICP备20017730号 身体没有灵魂是死的,信心没有行为也是死的。
主页
页面
  • 归档
  • 摘要
  • 杂图
  • 问题随笔
博主
Enamiĝu al vi
Enamiĝu al vi 管理员
To be, or not to be
543 文章 68 评论 594462 浏览
测试
测试
看板娘
赞赏作者

请通过微信、支付宝 APP 扫一扫

感谢您对作者的支持!

 支付宝 微信支付