FeignClient

一、介紹

一個普通的java接口,加上FeignClient註解,就可以完成整合hystrix,ribbon的http調用,使用上很是方便,也感覺很神奇,內部實現有必要探究一下。

二、問題

  1. 第一個問題:被@FeignClient標註的類,在spring中最終注入的是什麼?
  2. 第二個問題:如何跟hystrix結合的?
  3. 第三個問題:如何跟ribbon結合的?
  4. 第四個問題:調用過程?

帶着這四個問題,看下下面的實現

三、實現

一切都從EnableFeignClients這個註解開始

  1. 入口,EnableFeignClients的部分代碼如下:

    @Import(FeignClientsRegistrar.class)
    public @interface EnableFeignClients {
    

    也就是說,EnableFeignClients會Import FeignClientsRegistrar,Import註解是spring3.0就有的註解,其作用是引入配置類。

    參考FeignClientsRegistrar定義:

    class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, BeanClassLoaderAware {
    

    FeignClientsRegistrar實現了ImportBeanDefinitionRegistrar,而ImportBeanDefinitionRegistrar從spring3.1就有了,其作用是增加額外的bean定義,同時可以使用@Configuration的配置項。

    也就是說FeignClientsRegistrar能夠使用相關配置項向spring註冊被@FeignClient標註的bean,其重要的方法如下:

    public void registerBeanDefinitions(AnnotationMetadata metadata,
            BeanDefinitionRegistry registry) {
        registerDefaultConfiguration(metadata, registry);
        registerFeignClients(metadata, registry);
    }
    

    上面的代碼就是整個入口了,首先注入默認配置,接着注入FeignClient,注入FeignClient時同樣會注入用戶自己的配置。

  2. 工廠

    接着上面的代碼段-registerFeignClients,其部分簡化如下:

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

    其中關鍵的部分就是向spring注入了一個工廠bean-FeignClientFactoryBean,bean的類型就是被@FeignClient標註的我們自己定義的業務類。

    也就是說,被@FeignClient標註的類,在使用@Autowired注入時,其實注入的是FeignClientFactoryBean生成的一個實例。

    說起工廠,其關鍵作用只有一個,就是創建對象,那麼它創建的具體對象是什麼呢?代碼如下:

    public Object getObject() throws Exception {
        FeignContext context = applicationContext.getBean(FeignContext.class);
        Feign.Builder builder = feign(context);
        if (!StringUtils.hasText(this.url)) {
            String url;
            if (!this.name.startsWith("http")) {
                url = "http://" + this.name;
            }
            else {
                url = this.name;
            }
            url += cleanPath();
            return loadBalance(builder, context, new HardCodedTarget<>(this.type,
                    this.name, url));
        }
        ...其餘省略...
    }
    

    FeignContext即包含有feign配置的上下文,其被FeignAutoConfiguration自動注入,這裏不再多說,只要知道其內包括了feign的各種配置項即可。

    而feign(context)則是根據配置項配置Feign.Builder對象。那麼Feign.Builder是在哪注入的呢?

    這個可以參考FeignClientsConfiguration可以發現如下代碼:

    @Configuration
    @ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class })
    protected static class HystrixFeignConfiguration {
        @Bean
        @Scope("prototype")
        @ConditionalOnMissingBean
        @ConditionalOnProperty(name = "feign.hystrix.enabled", matchIfMissing = true)
        public Feign.Builder feignHystrixBuilder() {
            return HystrixFeign.builder();
        }
    }
    
    @Bean
    @Scope("prototype")
    @ConditionalOnMissingBean
    public Feign.Builder feignBuilder(Retryer retryer) {
        return Feign.builder().retryer(retryer);
    }
    

    也就是說如果發現hystrix依賴,則使用HystrixFeign.builder()進行注入,如果沒有hystrix依賴,則使用Feign.builder()進行注入。在這裏,feign完成了與hystrix的結合(回答了第二個問題)。注:Camden版本中該參數feign.hystrix.enabled默認是打開的,但是到了Dalston中該參數已經默認關閉了,也就是說需要單獨配置打開。

    接着再來看loadBalance方法:

    protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
            HardCodedTarget<T> target) {
        Client client = getOptional(context, Client.class);
        if (client != null) {
            builder.client(client);
            Targeter targeter = get(context, Targeter.class);
            return targeter.target(this, builder, context, target);
        }
        throw new IllegalStateException(
                "No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-ribbon?");
    }
    

    此方法中會構建一個feign.Client的實例,在依賴ribbon的情況下,默認會注入LoadBalancerFeignClient,它爲feign提供了負載均衡的能力,在這裏,feign完成了和ribbon的結合(回答了第三個問題)。

    再往下看,targeter.target(this, builder, context, target)最終會調用到Feign.Builder的build方法:

    public <T> T target(Target<T> target) {
        return build().newInstance(target);
    }
    
    其中build方法如下:
    public Feign build() {
        SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
          new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
                               logLevel, decode404);
        ParseHandlersByName handlersByName =
          new ParseHandlersByName(contract, options, encoder, decoder,
                      errorDecoder, synchronousMethodHandlerFactory);
        return new ReflectiveFeign(handlersByName, invocationHandlerFactory);
    }
    

    到了這裏我們可以知道,其實工廠構造的對象就是ReflectiveFeign.newInstance返回的對象,接着查看newInstance方法:

    public <T> T newInstance(Target<T> target) {
        ...省略其餘方法...
        T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[]{target.type()}, handler);
        return proxy;
    }
    

    到了這裏可以看到,真正的實例其實就是我們的業務接口(被@FeignClient標註的接口),採用jdk動態代理生成的一個代理對象(回答了第一個問題)。

    處理反射調用時,觸發的其實是SynchronousMethodHandler

  3. 調用

    這裏我們看下SynchronousMethodHandler的invoke方法,即實際調用我們接口的某個方法時,會觸發invoke調用:

    public Object invoke(Object[] argv) throws Throwable {
      RequestTemplate template = buildTemplateFromArgs.create(argv);
      Retryer retryer = this.retryer.clone();
      while (true) {
        try {
          return executeAndDecode(template);
        } catch (RetryableException e) {
          retryer.continueOrPropagate(e);
          if (logLevel != Logger.Level.NONE) {
            logger.logRetry(metadata.configKey(), logLevel);
          }
          continue;
        }
      }
    }
    

    從上面的代碼中看到一個Retryer,feign默認情況下是Retryer.NEVER_RETRY,即不重試。

    而executeAndDecode最終會委託給LoadBalancerFeignClient的execute來發起調用(參考第三個問題的答案處注入的LoadBalancerFeignClient),代碼如下:

    public Response execute(Request request, Request.Options options) throws IOException {
        try {
            ...省略其他代碼...
            return lbClient(clientName).executeWithLoadBalancer(ribbonRequest,
                    requestConfig).toResponse();
        }
        catch (ClientException e) {
            IOException io = findIOException(e);
            if (io != null) {
                throw io;
            }
            throw new RuntimeException(e);
        }
    }
    

    核心的代碼是委託給一個LoadBalancer來執行調用。那麼LoadBalancer來自那裏,跟蹤lbClient方法可以看到如下代碼:

    public FeignLoadBalancer create(String clientName) {
        if (this.cache.containsKey(clientName)) {
            return this.cache.get(clientName);
        }
        IClientConfig config = this.factory.getClientConfig(clientName);
        ILoadBalancer lb = this.factory.getLoadBalancer(clientName);
        ServerIntrospector serverIntrospector = this.factory.getInstance(clientName, ServerIntrospector.class);
        FeignLoadBalancer client = enableRetry ? new RetryableFeignLoadBalancer(lb, config, serverIntrospector,
                loadBalancedRetryPolicyFactory) : new FeignLoadBalancer(lb, config, serverIntrospector);
        this.cache.put(clientName, client);
        return client;
    }
    

    這裏可以看到,使用的負載均衡器ILoadBalancer lb就是負載均衡器所介紹的。

    • 如果沒有啓用retry,那麼使用的是FeignLoadBalancer。
    • 如果啓用了retry,使用RetryableFeignLoadBalancer,RetryableFeignLoadBalancer中的retry使用的是spirng-retry實現的,這裏不再多說。

    那麼,現在有了LoadBalancer,其核心方法executeWithLoadBalancer,這個方法存在與其父類中AbstractLoadBalancerAwareClient(該類是ribbon-loadbalancer jar中的,即是ribbon的實現)。

    經過查看,該方法executeWithLoadBalancer使用RxJava來進行相關調用,代碼比較複雜,這裏不再列出。

    值得一說的是executeWithLoadBalancer裏面有相關的retry邏輯,但是經過測試,這些retry邏輯已經失效,不再使用(不知道爲啥netflix不把retry邏輯移除,也許在別的地方使用?查看github代碼,更新時間是3年前,也行netflix的人忙着其他的吧。。。)。

    到這,調用過程完畢(回答了第四個問題

  4. 這裏再提一點,關於retry

    關於retry,總共有三處:

    1. feign自身的retry,默認沒有開啓
    2. ribbon實現的retry,經過測試,已經失效了
    3. RetryableFeignLoadBalancer即採用spirng-retry實現,這裏和重試中的RestTemplate統一了。
發佈了62 篇原創文章 · 獲贊 23 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章