调用FeignClient的时候,SpringCloud到底干了哪些事情?——OpenFeign原理分析

- [1. 前言](#1前言)
- [2. 摘要](#2摘要)
- [3. 流程详解](#3流程详解)
  - [3.1. Spring容器中注入Client对应的Bean](#3-1-spring容器中注入client对应的bean)
    - [3.1.1. 扫描@FeignClient 对应的Bean,生成BeanDefinition 放进spring容器](#3-1-1扫描-feignclient对应的-bean,生成beandefinition放进-spring容器)
    - [3.1.2. 扫描到对应的BeanDefinition的时候,生成Client对应的Bean到spring容器中](#3-1-2扫描到对应的-beandefinition的时候,生成client对应的bean到spring容器中)
  - [3.2. 调用Client进行远程调用](#3-2调用-client进行远程调用)
    - [3.2.1. 调用Client对应的bean(实际上是动态代理)](#3-2-1调用-client对应的bean(实际上是动态代理))
    - [3.2.2. 动态代理调用FeignClient](#3-2-2动态代理调用-feignclient)
    - [3.2.3. FeignClient执行对应后续操作](#3-2-3-feignclient执行对应后续操作)
    - [3.2.4. 负载均衡](#3-2-4负载均衡)
    - [3.2.5. HttpClient进行请求](#3-2-5-httpclient进行请求)
- [4. 参考](#4参考)

1. 前言

调用FeignClient的时候,SpringCloud到底干了哪些事情?

这块工作是SpringCloud的OpenFeign模块做的,了解之后可以有几个用处:

  1. 高效排查各种问题。
  2. 定制Feign的各个子模块。
  3. 考察候选人,了解候选人的技术深度。

2. 摘要

  • Spring容器中注入Client对应的Bean
    • 扫描@FeignClient 对应的Bean,生成BeanDefinition 放进spring容器
    • 扫描到对应的BeanDefinition的时候,生成Client对应的Bean到spring容器中
  • 调用Client进行远程调用
    • 调用Client对应的bean(实际上是动态代理)
      • feign.ReflectiveFeign
    • 动态代理调用FeignClient
      • feign.SynchronousMethodHandler#invoke
      • feign.SynchronousMethodHandler#executeAndDecode
    • FeignClient执行对应后续操作
      • org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient#execute
    • 负载均衡
      • 获取对应的LoadBalanceClient实例
        • org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient#lbClient
      • 分配服务器,进行请求
        • com.netflix.client.AbstractLoadBalancerAwareClient#executeWithLoadBalancer(S, com.netflix.client.config.IClientConfig)
        • org.springframework.cloud.openfeign.ribbon.FeignLoadBalancer#execute
      • 其他附带知识
        • tsf根据泳道进行服务过滤 com.tencent.cloud.tsf.lane.instrument.loadbalancer.TsfRibbonLaneLoadbalancer#filterAllServer
    • HttpClient进行请求
      • feign.httpclient.ApacheHttpClient#execute
      • org.apache.http.impl.client.CloseableHttpClient#execute(org.apache.http.client.methods.HttpUriRequest)

3. 流程详解

3.1. Spring容器中注入Client对应的Bean

3.1.1. 扫描@FeignClient 对应的Bean,生成BeanDefinition 放进spring容器
  • @EnableFeignClients开启openfeign功能
  • @EnableFeignClients 会import FeignClientsRegistrar
  • FeignClientsRegistrar 找到所有FeignClient注解的Class,生成对应的BeanDefinition供下一步使用

	public void registerFeignClients(AnnotationMetadata metadata,
			BeanDefinitionRegistry registry) {
		// 省略代码……
		AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
				FeignClient.class);
		// 省略代码……

		for (String basePackage : basePackages) {
			Set<BeanDefinition> candidateComponents = scanner
					.findCandidateComponents(basePackage);
			for (BeanDefinition candidateComponent : candidateComponents) {
				if (candidateComponent instanceof AnnotatedBeanDefinition) {
				     // 省略代码……
					registerFeignClient(registry, annotationMetadata, attributes);
				}
			}
		}
	}
	private void registerFeignClient(BeanDefinitionRegistry registry,
			AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
		String className = annotationMetadata.getClassName();
		BeanDefinitionBuilder definition = BeanDefinitionBuilder
				.genericBeanDefinition(FeignClientFactoryBean.class);

          // 省略代码……
		BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
				new String[] { alias });
		BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
	}
3.1.2. 扫描到对应的BeanDefinition的时候,生成Client对应的Bean到spring容器中
  • 代码位置:org.springframework.cloud.openfeign.FeignClientFactoryBean
  • 说明:
    • 因为是FactoryBean所以在spring的生成bean阶段,会调用getObject方法生成bean丢进容器。
    • getObject 主要做的事情是生成一个动态代理对象
class FeignClientFactoryBean
		implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {
    // 省略代码……
    
	@Override
	public Object getObject() throws Exception {
		return getTarget();
	}
	<T> T getTarget() {
		// 省略代码……
		return (T) targeter.target(this, builder, context,
				new HardCodedTarget<>(this.type, this.name, url));
	}

3.2. 调用Client进行远程调用

3.2.1. 调用Client对应的bean(实际上是动态代理)
  • 代码位置:feign.ReflectiveFeign
3.2.2. 动态代理调用FeignClient
  • feign.SynchronousMethodHandler#invoke
final class SynchronousMethodHandler implements MethodHandler {
    // 省略代码……
  @Override
  public Object invoke(Object[] argv) throws Throwable {
    // 省略代码……
      response = client.execute(request, options);
    // 省略代码……
  • client对象在以下configuration中实例化(这里以 ribbon+httpclient的配置为例)
    • org.springframework.cloud.openfeign.ribbon.HttpClientFeignLoadBalancedConfiguration#feignClient
3.2.3. FeignClient执行对应后续操作
  • org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient#execute

3.2.4. 负载均衡
  • 获取对应的LoadBalanceClient实例
    • org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient#lbClient
    • 这里的设计也很巧妙。
      • 每个feignClient对应的ribbon配置分别放在一个springContext中
      • 不过,感觉有点过度设计,为了不同client的配置隔离,大大提升了学习成本。
      • 具体实现参考:springcloud @RibbonClients 与NamedContextFactory
  • 分配服务器,进行请求
    • com.netflix.client.AbstractLoadBalancerAwareClient#executeWithLoadBalancer(S, com.netflix.client.config.IClientConfig)
    • org.springframework.cloud.openfeign.ribbon.FeignLoadBalancer#execute

  • 其他附带知识
    • tsf根据泳道进行服务过滤 com.tencent.cloud.tsf.lane.instrument.loadbalancer.TsfRibbonLaneLoadbalancer#filterAllServer
3.2.5. HttpClient进行请求
  • feign.httpclient.ApacheHttpClient#execute
  • org.apache.http.impl.client.CloseableHttpClient#execute(org.apache.http.client.methods.HttpUriRequest)

4. 参考

Spring Cloud OpenFeign 原理浅析
springcloud @RibbonClients 与NamedContextFactory
Spring Cloud组件那么多超时设置,如何理解和运用?

/** * RECOMMENDED CONFIGURATION VARIABLES: EDIT AND UNCOMMENT THE SECTION BELOW TO INSERT DYNAMIC VALUES FROM YOUR PLATFORM OR CMS. * LEARN WHY DEFINING THESE VARIABLES IS IMPORTANT: https://disqus.com/admin/universalcode/#configuration-variables*/ /* var disqus_config = function () { this.page.url = PAGE_URL; // Replace PAGE_URL with your page's canonical URL variable this.page.identifier = PAGE_IDENTIFIER; // Replace PAGE_IDENTIFIER with your page's unique identifier variable }; */ (function() { // DON'T EDIT BELOW THIS LINE var d = document, s = d.createElement('script'); s.src = 'https://chenzz.disqus.com/embed.js'; s.setAttribute('data-timestamp', +new Date()); (d.head || d.body).appendChild(s); })();