7

大写的Pecker的个人空间

 2 years ago
source link: https://my.oschina.net/u/4239117/blog/5284343
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Feign 从注册到调用原理分析

本文主要讲述 Feign 是如何注册到 Spring 容器、融合 Ribbon进行负载均衡、进行 RPC 调用。

简单提一下项目中一般都是如何使用 Feign 的,首先声明一个@FeignClient,定义 RPC 调用方法,然后像调用本地方法一样,调用远程服务的方法

// 定义 FeignClient
@FeignClient(value = "service-order",path = "/order")
public interface OrderFeignService {

    @RequestMapping("/findOrderByUserId/{userId}")
    R findOrderByUserId(@PathVariable("userId") Integer userId);
}

// 调用远程服务
   @Autowired
   OrderFeignService orderFeignService;

     R  findOrderByUserId(@PathVariable("id") Integer id) {
        //feign调用
        R result = orderFeignService.findOrderByUserId(id);
        return result;
    }

这样一来,我们省去了自己去配置 RestTemplate 或其他 HTTPClient 的麻烦,但是简单方便的同时你是否会有一些疑惑:

  • 我们使用@Autowired注入的 OrderFeignService,那么它一定是一个 Spring Bean,它是什么时候,如何被注入到 Spring 容器的?
  • Feign 是如何执行 findOrderByUserId()的?
  • @RequestMapping 是 SpringMVC 的注解呀,怎么在 Feign 中也会生效呢?
  • Feign 是如何整合 Ribbon 的?
  • Ribbon 是如何获取注册中心的服务的?
  • Ribbon 是如何进行负载均衡的?

理解了上面的这些问题,我们也就明白了 Feign 是如何进行调用的,那么带着这些问题我们来一步步分析

@FeignClient 注册

@EnableFeignClients 注解

根据 SpringBoot 自动装配的思想,先猜想下一定会有@Enablexxx ,然后再有@Import(xxx.class),来进行 Feign 的自动注入

    1. 要使用 Feign 首先要引入 Feign 的 Maven 依赖,接着一定要在启动类上添加注解 @EnableFeignClients
  • 2.@EnableFeignClients @Import(FeignClientsRegistrar.class)
  • 3.注册 @FeignClient
public void registerBeanDefinitions(AnnotationMetadata metadata,
      BeanDefinitionRegistry registry) {
   // 注册 Feign 配置信息
   registerDefaultConfiguration(metadata, registry);
   // 注册 @FeignClient
   registerFeignClients(metadata, registry);
}

扫描并注册 FeignClient 为 BeanDefinition

public void registerFeignClients(AnnotationMetadata metadata,
      BeanDefinitionRegistry registry) {
   
   // ...,省略了部分源码 扫描所有 @FeignClient 标注的类
   Map<String, Object> attrs = metadata
         .getAnnotationAttributes(EnableFeignClients.class.getName());
   AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
         FeignClient.class);
    // ... 
    
   for (String basePackage : basePackages) {
        // ...

        Map<String, Object> attributes = annotationMetadata
              .getAnnotationAttributes(
                    FeignClient.class.getCanonicalName());
        // ...
        
        // 注册 FeignClient 为 BeanDefinition
        registerFeignClient(registry, annotationMetadata, attributes);
         
      }
   }
}

将 FeignClient 包裹成 FeignClientFactoryBean

private void registerFeignClient(BeanDefinitionRegistry registry,
      AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
   String className = annotationMetadata.getClassName();
   
   // 将 FeignClient 包裹成 FeignClientFactoryBean
   BeanDefinitionBuilder definition = BeanDefinitionBuilder
         .genericBeanDefinition(FeignClientFactoryBean.class);
   // ... 一堆definition.addPropertyValue()
   definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
   definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
  
  // ...

  // 注册 FeignClientFactoryBean
   BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
         new String[] { alias });
   BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}

FeignClientFactoryBean#getObject(),生成FeignClient 的代理对象

public Object getObject() throws Exception {
   return getTarget();
}

/**
 * 根据指定的数据和上下文信息,生成一个 FeignClient 的代理对象 Client
 */
<T> T getTarget() {
   FeignContext context = applicationContext.getBean(FeignContext.class);
   // ...
   Client client = getOptional(context, Client.class);
  // ...
  
   Targeter targeter = get(context, Targeter.class);
   return (T) targeter.target(this, builder, context,
         new HardCodedTarget<>(type, name, url));
}

// 最终会调用生成一个 FeignInvocationHandler 的代理对象
public <T> T target(Target<T> target) {
  return build().newInstance(target);
}

  • 4.生成FeignClient 的动态代理 FeignInvocationHandler
public <T> T newInstance(Target<T> target) {
    // 解析请求为 MethodHandler
  Map<String, MethodHandler> nameToHandler = targetoHandlersByName.apply(target);
  Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
  List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
  // ... 代码省略  
  
  // 生成动态代理
  InvocationHandler handler = factory.create(target, methodToHandler);
  T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
      new Class<?>[] {target.type()}, handler);

  for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
    defaultMethodHandler.bindTo(proxy);
  }
  return proxy;
}

  • 5.FeignInvocationHandler 执行 invoke ,一步步调用,最终会调用 Client 的 execute()方法,执行远程调用
  • 被@FeignClient 标记的 Interface 会在开启了@EnableFeignClients 之后,被 Spring 扫描到容器中,并且生成一个 FeignInvocationHandler 的动态代理
  • 既然 FeignClient 会被生成一个动态代理,那么要执行到 findOrderByUserId() 方法,一定是通过FeignInvocationHandler 的 invoke() 方法被执行的,接着往下看

FeignClient 调用

先看一张图

由于没找到这张图的真正出处,引用该图暂未标明引用链接,如若该图作者发现可以联系本人,标明引用出处

上面我们已经分析完了FeignClient 是如何创建动态代理的,在我们按着FeignInvocationHandler 的 invoke() 方法往下分析之前,先看下上图中步骤 2,这一步就是 @RequestMapping如何在 Feign 中生效的

回头看下生成动态代理过程中有段代码

  // 解析请求为 MethodHandler
  Map<String, MethodHandler> nameToHandler = target``oHandlersByName.apply(target);

这里完成了 Contract 对 SpringMVC 的解析

public Map<String, MethodHandler> apply(Target key) {
  // 这就用contract完成了方法上的SpringMVC注解的转换
  // FeignClient标注的Interface 的每一个方法都会被解析成MethodMetadata
  // 对各种SpringMVC的注解进行解析,将解析出来的header,method,path,body,form param,返回值等等,放入了MethodMetadata中
  List<MethodMetadata> metadata = contract.parseAndValidatateMetadata(key.type());
  Map<String, MethodHandler> result = new LinkedHashMap<String, MethodHandler>();
  // 遍历方法元数据
  for (MethodMetadata md : metadata) {
    BuildTemplateByResolvingArgs buildTemplate;
    if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) {
      buildTemplate = new BuildFormEncodedTemplateFromArgs(md, encoder);
    } else if (md.bodyIndex() != null) {
      buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder);
    } else {
      buildTemplate = new BuildTemplateByResolvingArgs(md);
    }
    // 在这里就创建了SynchronousMethodHandler,key就是方法名
    // SynchronousMethodHandler就是所有的方法被代理后实际处理的处理器
    result.put(md.configKey(),
               factory.create(key, md, buildTemplate, options, decoder, errorDecoder));
  }
  return result;
}

所以我们可以看到,Feign 之所以能用 SpringMVC 注解是因为专门对这层注解做了解析。

FeignInvocationHandler#invoke()

FeignInvocationHandler#invoke() --> executeAndDecode(template, options)

Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
   // 处理所有Interceptor: RequestInterceptor
   Request request = targetRequest(template);

  // ...

  Response response;

  try {
      // 执行Client#execute()
    response = client.execute(request, options);
   // ...
  }   }

LoadBalancerFeignClient#execute()

public Response execute(Request request, Request.Options options) throws IOException {
   try {
       // ...  封装 request 成 ribbonRequest
      FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
            this.delegate, request, uriWithoutHost);

      // 配置 Client
      IClientConfig requestConfig = getClientConfig(options, clientName);
      return lbClient(clientName)
            .executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();
   }

lbClient(clientName) 整合 Ribbon 实例化出 FeignLoadBalancer

lbClient(String clientName) --> this.lbClientFactory.create(clientName)

public FeignLoadBalancer create(String clientName) {
    // FeignLoadBalancer 是 Feign 提供的
    FeignLoadBalancer client = this.cache.get(clientName);
   if (client != null) {
      return client;
   }
   // IClientConfig ILoadBalancer 是 Ribbon 提供的
   IClientConfig config = this.factory.getClientConfig(clientName);
   ILoadBalancer lb = this.factory.getLoadBalancer(clientName);
   ServerIntrospector serverIntrospector = this.factory.getInstance(clientName,
         ServerIntrospector.class);
   client = this.loadBalancedRetryFactory != null
         ? new RetryableFeignLoadBalancer(lb, config, serverIntrospector,
               this.loadBalancedRetryFactory)
         : new FeignLoadBalancer(lb, config, serverIntrospector);
   this.cache.put(clientName, client);
   // 该 client 本身封装了 Ribbon 的IClientConfig ILoadBalancer,在后面进行替换 url 和负载均衡发挥作用
   return client;
}

FeignLoadBalancer如何负载均衡选择 Server

public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
    LoadBalancerCommand<T> command = buildLoadBalancerCommand(request, requestConfig);

    try {
        // 这提交了一个匿名内部类进去,那么ServerOperation.call方法就一定会在submit方法里被调用
        return command.submit(
            new ServerOperation<T>() {
                @Override
                public Observable<T> call(Server server) {
                    URI finalUri = reconstructURIWithServer(server, request.getUri());
                    S requestForServer = (S) request.replaceUri(finalUri);
                    try {
                        return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
                    } 
                    catch (Exception e) {
                        return Observable.error(e);
                    }
                }
            })
            .toBlocking()
            .single();

在 submit() 中调用selectServer(),进行 url 替换,选择一个服务实例

// LoadBalancerCommand.java
public Observable<T> submit(final ServerOperation<T> operation) {
    final ExecutionInfoContext context = new ExecutionInfoContext();
    
    if (listenerInvoker != null) {
        try {
            listenerInvoker.onExecutionStart();
        } catch (AbortExecutionException e) {
            return Observable.error(e);
        }
    }
		// ribbon的重试参数
    final int maxRetrysSame = retryHandler.getMaxRetriesOnSameServer();
    final int maxRetrysNext = retryHandler.getMaxRetriesOnNextServer();

    // Use the load balancer
    // selectServer 负载均衡选择实例
    Observable<T> o = 
            (server == null ? selectServer() : Observable.just(server))
      ......省略部分代码
      // 选择出服务实例后,对operation进行回调,进行url的替换,然后发起真正的http请求
      return operation.call(server)...
      ......省略部分代码
      
// 选择一个服务实例
private Observable<Server> selectServer() {
    return Observable.create(new OnSubscribe<Server>() {
        @Override
        public void call(Subscriber<? super Server> next) {
            try {
                // 读取host信息,也就是服务名,然后调用负载均衡器chooseServer方法选择一个服务实例
                Server server = loadBalancerContext.getServerFromLoadBalancer(loadBalancerURI, loadBalancerKey);   
                next.onNext(server);
                next.onCompleted();
            } catch (Exception e) {
                next.onError(e);
            }
        }
    });
}

  • 构造了一个LoadBalancerCommand
  • 构造了一个ServerOperation,包含了发起http调用的逻辑,作为参数传入- - LoadBalancerCommand.submit方法,后面会进行回调
  • 在submit方法中,会调用selectServer方法,选择服务实例
  • selectServer方法调用loadBalancerContext.getServerFromLoadBalancer,最终调用负载均衡器chooseServer方法选择一个服务实例,
  • 拿到服务实例后,将Server对象传入ServerOperation的call方法进行回调
  • ServerOperation用server的信息替换host里的服务名,拿到真正的请求地址
  • 再调用子类也就是FeignLoadBalancer.execute方法执行http请求
  • 默认的connectTimeout和readTimeout都是1000毫秒
  • 响应结果封装为RibbonResponse

执行 http 请求

获取到了远程服务的真实地址,就可以采用 Http 方式调用了,问题是可提供Http 调用的方式有那么多(OkHttp 、ApacheHttpClient、RestTemplate 等等),Feign 怎么知道用哪个呢?

答案在 FeignAutoConfiguration 里

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ApacheHttpClient.class)
@ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
@ConditionalOnMissingBean(CloseableHttpClient.class)
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
protected static class HttpClientFeignConfiguration {
    // ...
}

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(OkHttpClient.class)
@ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
@ConditionalOnMissingBean(okhttp3.OkHttpClient.class)
@ConditionalOnProperty("feign.okhttp.enabled")
protected static class OkHttpFeignConfiguration {
    // ...
}

通过 @ConditionalOnClass 、@ConditionalOnProperty 等条件注解 来匹配用哪个 http 客户端,所以我们在使用过程中也是无感知的,只要我们引入 OkHttp 或者 ApacheHttpClient 相关的 Maven 依赖,就可以完成调用了

至此,FeignClient 被注册,被动态代理,被执行,整个流程已经清晰了,上面的问题也应该都有了答案,还有一个:Ribbon 是如何知道我们用到的是哪个注册中心,是 Eureka 是 Nacos ?他又是如何获取注册中心上的服务的?

Ribbon 获取注册中心服务路由

既然我们在使用过程中没有自己指定,那么我猜想是不是 Ribbon 自己注册的时候自己会选择呢?我们就从Ribbon 的注册入手 RibbonClientConfiguration ,果然

配置ILoadBalancer

在 RibbonClientConfiguration 中会配置 ILoadBalancer,会返回一个 ZoneAwareLoadBalancer 实例

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

更新注册中心列表

restOfInit() --> restOfInit() --> updateListOfServers --> servers = serverListImpl.getUpdatedListOfServers();

public void updateListOfServers() {
    List<T> servers = new ArrayList<T>();
    if (serverListImpl != null) {
        // 更新注册中心服务列表
        servers = serverListImpl.getUpdatedListOfServers();
        LOGGER.debug("List of Servers for {} obtained from Discovery client: {}",
                getIdentifier(), servers);

        if (filter != null) {
            servers = filter.getFilteredListOfServers(servers);
            LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}",
                    getIdentifier(), servers);
        }
    }
    updateAllServerList(servers);
}

ServerList<T> serverListImpl

获取服务列表是通过 serverListImpl 来获取的,serverListImpl 是一个interface,不同的注册中心服务商会实现 ServerLis, 比如 Nacos 的实现NacosServerList 。

这样我们就不用指定,Ribbon 就自己知道是从哪个注册中心获取了。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK