Feign 从注册到调用原理分析

原创
10/22 14:37
阅读数 1W

本文主要讲述 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 就自己知道是从哪个注册中心获取了。

展开阅读全文
打赏
1
50 收藏
分享
加载中
思路清晰 赞
10/25 20:14
回复
举报
很不错
10/24 14:03
回复
举报
更多评论
打赏
2 评论
50 收藏
1
分享
返回顶部
顶部