Spring Cloud 原始碼分析之OpenFeign

跟著Mic學架構發表於2022-02-11

OpenFeign是一個遠端客戶端請求代理,它的基本作用是讓開發者能夠以面向介面的方式來實現遠端呼叫,從而遮蔽底層通訊的複雜性,它的具體原理如下圖所示。

image-20211215192443739

在今天的內容中,我們需要詳細分析OpenFeign它的工作原理及原始碼,我們繼續回到這段程式碼。

@Slf4j
@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    IGoodsServiceFeignClient goodsServiceFeignClient;
    @Autowired
    IPromotionServiceFeignClient promotionServiceFeignClient;
    @Autowired
    IOrderServiceFeignClient orderServiceFeignClient;

    /**
     * 下單
     */
    @GetMapping
    public String order(){
        String goodsInfo=goodsServiceFeignClient.getGoodsById();
        String promotionInfo=promotionServiceFeignClient.getPromotionById();
        String result=orderServiceFeignClient.createOrder(goodsInfo,promotionInfo);
        return result;
    }
}

從這段程式碼中,先引出對於OpenFeign功能實現的思考。

  1. 宣告@FeignClient註解的介面,如何被解析和注入的?
  2. 通過@Autowired依賴注入,到底是注入一個什麼樣的例項
  3. 基於FeignClient宣告的介面被解析後,如何儲存?
  4. 在發起方法呼叫時,整體的工作流程是什麼樣的?
  5. OpenFeign是如何整合Ribbon做負載均衡解析?

帶著這些疑問,開始去逐項分析OpenFeign的核心原始碼

OpenFeign註解掃描與解析

思考, 一個被宣告瞭@FeignClient註解的介面,使用@Autowired進行依賴注入,而最終這個介面能夠正常被注入例項。

從這個結果來看,可以得到兩個結論

  1. @FeignClient宣告的介面,在Spring容器啟動時,會被解析。
  2. 由於被Spring容器載入的是介面,而介面又沒有實現類,因此Spring容器解析時,會生成一個動態代理類。

EnableFeignClient

@FeignClient註解是在什麼時候被解析的呢?基於我們之前所有積累的知識,無非就以下這幾種

  • ImportSelector,批量匯入bean
  • ImportBeanDefinitionRegistrar,匯入bean宣告並進行註冊
  • BeanFactoryPostProcessor , 一個bean被裝載的前後處理器

在這幾個選項中,似乎ImportBeanDefinitionRegistrar更合適,因為第一個是批量匯入一個bean的string集合,不適合做動態Bean的宣告。 而BeanFactoryPostProcessor 是一個Bean初始化之前和之後被呼叫的處理器。

而在我們的FeignClient宣告中,並沒有Spring相關的註解,所以自然也不會被Spring容器載入和觸發。

那麼@FeignClient是在哪裡被宣告掃描的呢?

在整合FeignClient時,我們在SpringBoot的main方法中,宣告瞭一個註解@EnableFeignClients(basePackages = "com.gupaoedu.ms.api")。這個註解需要填寫一個指定的包名。

嗯,看到這裡,基本上就能猜測出,這個註解必然和@FeignClient註解的解析有莫大的關係。

下面這段程式碼是@EnableFeignClients註解的宣告,果然看到了一個很熟悉的面孔FeignClientsRegistrar

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
}

FeignClientsRegistrar

FeignClientRegistrar,主要功能就是針對宣告@FeignClient註解的介面進行掃描和注入到IOC容器。

class FeignClientsRegistrar
      implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
    
}

果然,這個類實現了ImportBeanDefinitionRegistrar介面

public interface ImportBeanDefinitionRegistrar {
    default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
        this.registerBeanDefinitions(importingClassMetadata, registry);
    }

    default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    }
}

這個介面有兩個過載的方法,用來實現Bean的宣告和註冊。

簡單給大家演示一下ImportBeanDefinitionRegistrar的作用。

gpmall-portal這個專案的com.gupaoedu目錄下,分別建立

  1. HelloService.java
  2. GpImportBeanDefinitionRegistrar.java
  3. EnableGpRegistrar.java
  4. TestMain
  1. 定義一個需要被裝載到IOC容器中的類HelloService
public class HelloService {
}
  1. 定義一個Registrar的實現,定義一個bean,裝載到IOC容器
public class GpImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
                                        BeanDefinitionRegistry registry) {
        BeanDefinition beanDefinition=new GenericBeanDefinition();
        beanDefinition.setBeanClassName(HelloService.class.getName());
        registry.registerBeanDefinition("helloService",beanDefinition);
    }
}
  1. 定義一個註解類
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(GpImportBeanDefinitionRegistrar.class)
public @interface EnableGpRegistrar {
}
  1. 寫一個測試類
@Configuration
@EnableGpRegistrar
public class TestMain {

    public static void main(String[] args) {
        ApplicationContext applicationContext=new AnnotationConfigApplicationContext(TestMain.class);
        System.out.println(applicationContext.getBean(HelloService.class));

    }
}
  1. 通過結果演示可以發現,HelloService這個bean 已經裝載到了IOC容器。

這就是動態裝載的功能實現,它相比於@Configuration配置注入,會多了很多的靈活性。 ok,再回到FeignClient的解析中來。

FeignClientsRegistrar.registerBeanDefinitions

  • registerDefaultConfiguration 方法內部從 SpringBoot 啟動類上檢查是否有 @EnableFeignClients, 有該註解的話, 則完成Feign框架相關的一些配置內容註冊。
  • registerFeignClients 方法內部從 classpath 中, 掃描獲得 @FeignClient 修飾的類, 將類的內容解析為 BeanDefinition , 最終通過呼叫 Spring 框架中的 BeanDefinitionReaderUtils.resgisterBeanDefinition 將解析處理過的 FeignClient BeanDeifinition 新增到 spring 容器中
//BeanDefinitionReaderUtils.resgisterBeanDefinition 
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
                                    BeanDefinitionRegistry registry) {
    //註冊@EnableFeignClients中定義defaultConfiguration屬性下的類,包裝成FeignClientSpecification,註冊到Spring容器。
    //在@FeignClient中有一個屬性:configuration,這個屬性是表示各個FeignClient自定義的配置類,後面也會通過呼叫registerClientConfiguration方法來註冊成FeignClientSpecification到容器。
//所以,這裡可以完全理解在@EnableFeignClients中配置的是做為兜底的配置,在各個@FeignClient配置的就是自定義的情況。
    registerDefaultConfiguration(metadata, registry);
    registerFeignClients(metadata, registry);
}

這裡面需要重點分析的就是registerFeignClients方法,這個方法主要是掃描類路徑下所有的@FeignClient註解,然後進行動態Bean的注入。它最終會呼叫registerFeignClient方法。

public void registerFeignClients(AnnotationMetadata metadata,
			BeanDefinitionRegistry registry) {
    registerFeignClient(registry, annotationMetadata, attributes);
}

FeignClientsRegistrar.registerFeignClients

registerFeignClients方法的定義如下。

//# FeignClientsRegistrar.registerFeignClients
public void registerFeignClients(AnnotationMetadata metadata,
      BeanDefinitionRegistry registry) {
   
   LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();
   //獲取@EnableFeignClients註解的後設資料
   Map<String, Object> attrs = metadata
         .getAnnotationAttributes(EnableFeignClients.class.getName());
    //獲取@EnableFeignClients註解中的clients屬性,可以配置@FeignClient宣告的類,如果配置了,則需要掃描並載入。
   final Class<?>[] clients = attrs == null ? null
         : (Class<?>[]) attrs.get("clients");
   if (clients == null || clients.length == 0) {
       //預設TypeFilter生效,這種模式會查詢出許多不符合你要求的class名
      ClassPathScanningCandidateComponentProvider scanner = getScanner();
      scanner.setResourceLoader(this.resourceLoader);
      scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class)); //新增包含過濾的屬性@FeignClient。
      Set<String> basePackages = getBasePackages(metadata); //從@EnableFeignClients註解中獲取basePackages配置。
      for (String basePackage : basePackages) {
          //scanner.findCandidateComponents(basePackage) 掃描basePackage下的@FeignClient註解宣告的介面
         candidateComponents.addAll(scanner.findCandidateComponents(basePackage)); //新增到candidateComponents,也就是候選容器中。
      }
   }
   else {//如果配置了clients,則需要新增到candidateComponets中。
      for (Class<?> clazz : clients) {
         candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
      }
   }
   //遍歷候選容器列表。
   for (BeanDefinition candidateComponent : candidateComponents) { 
      if (candidateComponent instanceof AnnotatedBeanDefinition) { //如果屬於AnnotatedBeanDefinition例項型別
         // verify annotated class is an interface
          //得到@FeignClient註解的beanDefinition
         AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
         AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();  //獲取這個bean的註解後設資料
         Assert.isTrue(annotationMetadata.isInterface(),
               "@FeignClient can only be specified on an interface"); 
         //獲取後設資料屬性
         Map<String, Object> attributes = annotationMetadata
               .getAnnotationAttributes(FeignClient.class.getCanonicalName());
         //獲取@FeignClient中配置的服務名稱。
         String name = getClientName(attributes);
         registerClientConfiguration(registry, name,
               attributes.get("configuration"));

         registerFeignClient(registry, annotationMetadata, attributes);
      }
   }
}

FeignClient Bean的註冊

這個方法就是把FeignClient介面註冊到Spring IOC容器,

FeignClient是一個介面,那麼這裡注入到IOC容器中的物件是什麼呢?

private void registerFeignClient(BeanDefinitionRegistry registry,
      AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
   String className = annotationMetadata.getClassName();  //獲取FeignClient介面的類全路徑
   Class clazz = ClassUtils.resolveClassName(className, null); //載入這個介面,得到Class例項
    //構建ConfigurableBeanFactory,提供BeanFactory的配置能力
   ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory 
         ? (ConfigurableBeanFactory) registry : null;
    //獲取contextId
   String contextId = getContextId(beanFactory, attributes);
   String name = getName(attributes);
    //構建一個FeignClient FactoryBean,這個是工廠Bean。
   FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();
    //設定工廠Bean的相關屬性
   factoryBean.setBeanFactory(beanFactory);
   factoryBean.setName(name);
   factoryBean.setContextId(contextId);
   factoryBean.setType(clazz);
    
    //BeanDefinitionBuilder是用來構建BeanDefinition物件的建造器
   BeanDefinitionBuilder definition = BeanDefinitionBuilder
         .genericBeanDefinition(clazz, () -> {
             //把@FeignClient註解配置中的屬性設定到FactoryBean中。
            factoryBean.setUrl(getUrl(beanFactory, attributes));
            factoryBean.setPath(getPath(beanFactory, attributes));
            factoryBean.setDecode404(Boolean
                  .parseBoolean(String.valueOf(attributes.get("decode404"))));
            Object fallback = attributes.get("fallback");
            if (fallback != null) {
               factoryBean.setFallback(fallback instanceof Class
                     ? (Class<?>) fallback
                     : ClassUtils.resolveClassName(fallback.toString(), null));
            }
            Object fallbackFactory = attributes.get("fallbackFactory");
            if (fallbackFactory != null) {
               factoryBean.setFallbackFactory(fallbackFactory instanceof Class
                     ? (Class<?>) fallbackFactory
                     : ClassUtils.resolveClassName(fallbackFactory.toString(),
                           null));
            }
            return factoryBean.getObject();  //factoryBean.getObject() ,基於工廠bean創造一個bean例項。
         });
   definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); //設定注入模式,採用型別注入
   definition.setLazyInit(true); //設定延遲華
   validate(attributes); 
   //從BeanDefinitionBuilder中構建一個BeanDefinition,它用來描述一個bean的例項定義。
   AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
   beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
   beanDefinition.setAttribute("feignClientsRegistrarFactoryBean", factoryBean);

   // has a default, won't be null
   boolean primary = (Boolean) attributes.get("primary");

   beanDefinition.setPrimary(primary);

   String[] qualifiers = getQualifiers(attributes);
   if (ObjectUtils.isEmpty(qualifiers)) {
      qualifiers = new String[] { contextId + "FeignClient" };
   }
   
   BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
         qualifiers);
   BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); //把BeanDefinition的這個bean定義註冊到IOC容器。
}

綜上程式碼分析,其實實現邏輯很簡單。

  1. 建立一個BeanDefinitionBuilder。
  2. 建立一個工廠Bean,並把從@FeignClient註解中解析的屬性設定到這個FactoryBean中
  3. 呼叫registerBeanDefinition註冊到IOC容器中

關於FactoryBean

在上述的邏輯中,我們重點需要關注的是FactoryBean,這個大家可能接觸得會比較少一點。

首先,需要注意的是FactoryBean和BeanFactory是不一樣的,FactoryBean是一個工廠Bean,可以生成某一個型別Bean例項,它最大的一個作用是:可以讓我們自定義Bean的建立過程。

而,BeanFactory是Spring容器中的一個基本類也是很重要的一個類,在BeanFactory中可以建立和管理Spring容器中的Bean。

下面這段程式碼是FactoryBean介面的定義。

public interface FactoryBean<T> {
    String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType";

    @Nullable
    T getObject() throws Exception;

    @Nullable
    Class<?> getObjectType();

    default boolean isSingleton() {
        return true;
    }
}

從上面的程式碼中我們發現在FactoryBean中定義了一個Spring Bean的很重要的三個特性:是否單例、Bean型別、Bean例項,這也應該是我們關於Spring中的一個Bean最直觀的感受。

FactoryBean自定義使用

下面我們來模擬一下@FeignClient解析以及工廠Bean的構建過程。

  1. 先定義一個介面,這個介面可以類比為我們上面描述的FeignClient.
public interface IHelloService {

    String say();
}

  1. 接著,定義一個工廠Bean,這個工廠Bean中主要是針對IHelloService生成動態代理。
public class DefineFactoryBean implements FactoryBean<IHelloService> {

    @Override
    public IHelloService getObject()  {
       IHelloService helloService=(IHelloService) Proxy.newProxyInstance(IHelloService.class.getClassLoader(),
               new Class<?>[]{IHelloService.class}, (proxy, method, args) -> {
           System.out.println("begin execute");
           return "Hello FactoryBean";
       });
        return helloService;
    }

    @Override
    public Class<?> getObjectType() {
        return IHelloService.class;
    }
}
  1. 通過實現ImportBeanDefinitionRegistrar這個介面,來動態注入Bean例項
public class GpImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
                                        BeanDefinitionRegistry registry) {

        DefineFactoryBean factoryBean=new DefineFactoryBean();
        BeanDefinitionBuilder definition= BeanDefinitionBuilder.genericBeanDefinition(
                IHelloService.class,()-> factoryBean.getObject());
        BeanDefinition beanDefinition=definition.getBeanDefinition();
        registry.registerBeanDefinition("helloService",beanDefinition);
    }
}
  1. 宣告一個註解,用來表示動態bean的注入匯入。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(GpImportBeanDefinitionRegistrar.class)
public @interface EnableGpRegistrar {
}
  1. 編寫測試類,測試IHelloService這個介面的動態注入
@Configuration
@EnableGpRegistrar
public class TestMain {

    public static void main(String[] args) {
        ApplicationContext applicationContext=new AnnotationConfigApplicationContext(TestMain.class);
        IHelloService is=applicationContext.getBean(IHelloService.class);
        System.out.println(is.say());

    }
}
  1. 執行上述的測試方法,可以看到IHelloService這個介面,被動態代理並且注入到了IOC容器。

FeignClientFactoryBean

由上述案例可知,Spring IOC容器在注入FactoryBean時,會呼叫FactoryBean的getObject()方法獲得bean的例項。因此我們可以按照這個思路去分析FeignClientFactoryBean

@Override
public Object getObject() {
   return getTarget();
}

構建物件Bean的實現程式碼如下,這個程式碼的實現較長,我們分為幾個步驟來看

Feign上下文的構建

先來看上下文的構建邏輯,程式碼部分如下。

<T> T getTarget() {
   FeignContext context = beanFactory != null
         ? beanFactory.getBean(FeignContext.class)
         : applicationContext.getBean(FeignContext.class);
   Feign.Builder builder = feign(context);
}

兩個關鍵的物件說明:

  1. FeignContext是全域性唯一的上下文,它繼承了NamedContextFactory,它是用來來統一維護feign中各個feign客戶端相互隔離的上下文,FeignContext註冊到容器是在FeignAutoConfiguration上完成的,程式碼如下!
//FeignAutoConfiguration.java

@Autowired(required = false)
private List<FeignClientSpecification> configurations = new ArrayList<>();
@Bean
public FeignContext feignContext() {
FeignContext context = new FeignContext();
context.setConfigurations(this.configurations);
return context;
}

在初始化FeignContext時,會把configurations放入到FeignContext中。configuration表示當前被掃描到的所有@FeignClient。

  1. Feign.Builder用來構建Feign物件,基於builder實現上下文資訊的構建,程式碼如下。
protected Feign.Builder feign(FeignContext context) {
    FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);
    Logger logger = loggerFactory.create(type);

    // @formatter:off
    Feign.Builder builder = get(context, Feign.Builder.class)
        // required values
        .logger(logger)
        .encoder(get(context, Encoder.class))
        .decoder(get(context, Decoder.class))
        .contract(get(context, Contract.class)); //contract協議,用來實現模版解析(後面再詳細分析)
    // @formatter:on

    configureFeign(context, builder);
    applyBuildCustomizers(context, builder);

    return builder;
}

從程式碼中可以看到,feign方法,主要是針對不同的服務提供者生成Feign的上下文資訊,比如loggerencoderdecoder等。因此,從這個分析過程中,我們不難猜測到它的原理結構,如下圖所示

image-20211215192527913

父子容器隔離的實現方式如下,當呼叫get方法時,會從context中去獲取指定type的例項物件。

//FeignContext.java
protected <T> T get(FeignContext context, Class<T> type) {
    T instance = context.getInstance(contextId, type);
    if (instance == null) {
        throw new IllegalStateException(
            "No bean found of type " + type + " for " + contextId);
    }
    return instance;
}

接著,呼叫NamedContextFactory中的getInstance方法。

//NamedContextFactory.java
public <T> T getInstance(String name, Class<T> type) {
    //根據`name`獲取容器上下文
    AnnotationConfigApplicationContext context = this.getContext(name);

    try {
        //再從容器上下文中獲取指定型別的bean。
        return context.getBean(type);
    } catch (NoSuchBeanDefinitionException var5) {
        return null;
    }
}

getContext方法根據namecontexts容器中獲得上下文物件,如果沒有,則呼叫createContext建立。

protected AnnotationConfigApplicationContext getContext(String name) {
    if (!this.contexts.containsKey(name)) {
        synchronized(this.contexts) {
            if (!this.contexts.containsKey(name)) {
                this.contexts.put(name, this.createContext(name));
            }
        }
    }

    return (AnnotationConfigApplicationContext)this.contexts.get(name);
}

生成動態代理

第二個部分,如果@FeignClient註解中,沒有配置url,也就是不走絕對請求路徑,則執行下面這段邏輯。

由於我們在@FeignClient註解中使用的是name,所以需要執行負載均衡策略的分支邏輯。

<T> T getTarget() {
    //省略.....
     if (!StringUtils.hasText(url)) { //是@FeignClient中的一個屬性,可以定義請求的絕對URL

      if (LOG.isInfoEnabled()) {
         LOG.info("For '" + name
               + "' URL not provided. Will try picking an instance via load-balancing.");
      }
      if (!name.startsWith("http")) {
         url = "http://" + name;
      }
      else {
         url = name;
      }
      url += cleanPath();
      //
      return (T) loadBalance(builder, context,
            new HardCodedTarget<>(type, name, url));
   }
    //省略....
}

loadBalance方法的程式碼實現如下,其中Client是Spring Boot自動裝配的時候實現的,如果替換了其他的http協議框架,則client則對應為配置的協議api。

protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
                            HardCodedTarget<T> target) {
    //Feign傳送請求以及接受響應的http client,預設是Client.Default的實現,可以修改成OkHttp、HttpClient等。
    Client client = getOptional(context, Client.class);
    if (client != null) {
        builder.client(client); //針對當前Feign客戶端,設定網路通訊的client
        //targeter表示HystrixTarger例項,因為Feign可以整合Hystrix實現熔斷,所以這裡會一層包裝。
        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-netflix-ribbon or spring-cloud-starter-loadbalancer?");
}

HystrixTarget.target程式碼如下,我們沒有整合Hystrix,因此不會觸發Hystrix相關的處理邏輯。

//HystrixTarget.java
@Override
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign,
                    FeignContext context, Target.HardCodedTarget<T> target) {
    if (!(feign instanceof feign.hystrix.HystrixFeign.Builder)) { //沒有配置Hystrix,則走這部分邏輯
        return feign.target(target);
    }
   //省略....

    return feign.target(target);
}

進入到Feign.target方法,程式碼如下。

//Feign.java
public <T> T target(Target<T> target) {
    return this.build().newInstance(target);  //target.HardCodedTarget
}

public Feign build() {
    //這裡會構建一個LoadBalanceClient
    Client client = (Client)Capability.enrich(this.client, this.capabilities);
    Retryer retryer = (Retryer)Capability.enrich(this.retryer, this.capabilities);
    List<RequestInterceptor> requestInterceptors = (List)this.requestInterceptors.stream().map((ri) -> {
        return (RequestInterceptor)Capability.enrich(ri, this.capabilities);
    }).collect(Collectors.toList());
    //OpenFeign Log配置
    Logger logger = (Logger)Capability.enrich(this.logger, this.capabilities);
    //模版解析協議(這個介面非常重要:它決定了哪些註解可以標註在介面/介面方法上是有效的,並且提取出有效的資訊,組裝成為MethodMetadata元資訊。)
    Contract contract = (Contract)Capability.enrich(this.contract, this.capabilities);
    //封裝Request請求的 連線超時=預設10s ,讀取超時=預設60
    Options options = (Options)Capability.enrich(this.options, this.capabilities);
    //編碼器
    Encoder encoder = (Encoder)Capability.enrich(this.encoder, this.capabilities);
    //解碼器
    Decoder decoder = (Decoder)Capability.enrich(this.decoder, this.capabilities);
    
    InvocationHandlerFactory invocationHandlerFactory = (InvocationHandlerFactory)Capability.enrich(this.invocationHandlerFactory, this.capabilities);
    QueryMapEncoder queryMapEncoder = (QueryMapEncoder)Capability.enrich(this.queryMapEncoder, this.capabilities);
    //synchronousMethodHandlerFactory, 同步方法呼叫處理器(很重要,後續要用到)
    Factory synchronousMethodHandlerFactory = new Factory(client, retryer, requestInterceptors, logger, this.logLevel, this.decode404, this.closeAfterDecode, this.propagationPolicy, this.forceDecoding);
    //ParseHandlersByName的作用就是我們傳入Target(封裝了我們的模擬介面,要訪問的域名),返回這個介面下的各個方法,對應的執行HTTP請求需要的一系列資訊
    ParseHandlersByName handlersByName = new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder, this.errorDecoder, synchronousMethodHandlerFactory);
    
    return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
}

build方法中,返回了一個ReflectiveFeign的例項物件,先來看ReflectiveFeign中的newInstance方法。

public <T> T newInstance(Target<T> target) {    //修飾了@FeignClient註解的介面方法封裝成方法處理器,把指定的target進行解析,得到需要處理的方法集合。    Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);    //定義一個用來儲存需要處理的方法的集合    Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();    //JDK8以後,介面允許預設方法實現,這裡是對預設方法進行封裝處理。    List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();	    //遍歷@FeignClient介面的所有方法    for (Method method : target.type().getMethods()) {        //如果是Object中的方法,則直接跳過        if (method.getDeclaringClass() == Object.class) {            continue;                    } else if (Util.isDefault(method)) {//如果是預設方法,則把該方法繫結一個DefaultMethodHandler。            DefaultMethodHandler handler = new DefaultMethodHandler(method);            defaultMethodHandlers.add(handler);            methodToHandler.put(method, handler);        } else {//否則,新增MethodHandler(SynchronousMethodHandler)。            methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));        }    }    //建立動態代理類。    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;}

上述程式碼,其實也不難理解。

  1. 解析@FeignClient介面宣告的方法,根據不同方法繫結不同的處理器。

    1. 預設方法,繫結DefaultMethodHandler
    2. 遠端方法,繫結SynchronousMethodHandler
  2. 使用JDK提供的Proxy建立動態代理

MethodHandler,會把方法引數、方法返回值、引數集合、請求型別、請求路徑進行解析儲存,如下圖所示。

image-20211123152837678

FeignClient介面解析

介面解析也是Feign很重要的一個邏輯,它能把介面宣告的屬性轉化為HTTP通訊的協議引數。

執行邏輯RerlectiveFeign.newInstance

public <T> T newInstance(Target<T> target) {    Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target); //here}

targetToHandlersByName.apply(target);會解析介面方法上的註解,從而解析出方法粒度的特定的配置資訊,然後生產一個SynchronousMethodHandler
然後需要維護一個<method,MethodHandler>的map,放入InvocationHandler的實現FeignInvocationHandler中。

public Map<String, MethodHandler> apply(Target target) {    List<MethodMetadata> metadata = contract.parseAndValidateMetadata(target.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, queryMapEncoder, target);        } else if (md.bodyIndex() != null) {            buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);        } else {            buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder, target);        }        if (md.isIgnored()) {            result.put(md.configKey(), args -> {                throw new IllegalStateException(md.configKey() + " is not a method handled by feign");            });        } else {            result.put(md.configKey(),                       factory.create(target, md, buildTemplate, options, decoder, errorDecoder));        }    }    return result;}

為了更好的理解上述邏輯,我們可以藉助下面這個圖來理解!

階段性小結

通過上述過程分析,被宣告為@FeignClient註解的類,在被注入時,最終會生成一個動態代理物件FeignInvocationHandler。

當觸發方法呼叫時,會被FeignInvocationHandler#invoke攔截,FeignClientFactoryBean在例項化過程中所做的事情如下圖所示。

image-20211215192548769

總結來說就幾個點:

  1. 解析Feign的上下文配置,針對當前的服務例項構建容器上下文並返回Feign物件
  2. Feign根據上下圍配置把 log、encode、decoder、等配置項設定到Feign物件中
  3. 對目標服務,使用LoadBalance以及Hystrix進行包裝
  4. 通過Contract協議,把FeignClient介面的宣告,解析成MethodHandler
  5. 遍歷MethodHandler列表,針對需要遠端通訊的方法,設定SynchronousMethodHandler處理器,用來實現同步遠端呼叫。
  6. 使用JDK中的動態代理機制構建動態代理物件。

遠端通訊實現

在Spring啟動過程中,把一切的準備工作準備就緒後,就開始執行遠端呼叫。

在前面的分析中,我們知道OpenFeign最終返回的是一個#ReflectiveFeign.FeignInvocationHandler的物件。

那麼當客戶端發起請求時,會進入到FeignInvocationHandler.invoke方法中,這個大家都知道,它是一個動態代理的實現。

//FeignInvocationHandler.java@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {    if ("equals".equals(method.getName())) {        try {            Object otherHandler =                args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;            return equals(otherHandler);        } catch (IllegalArgumentException e) {            return false;        }    } else if ("hashCode".equals(method.getName())) {        return hashCode();    } else if ("toString".equals(method.getName())) {        return toString();    }    return dispatch.get(method).invoke(args);}

接著,在invoke方法中,會呼叫this.dispatch.get(method)).invoke(args)this.dispatch.get(method)會返回一個SynchronousMethodHandler,進行攔截處理。

this.dispatch,其實就是在初始化過程中建立的,private final Map<Method, MethodHandler> dispatch;例項。

SynchronousMethodHandler.invoke

這個方法會根據引數生成完成的RequestTemplate物件,這個物件是Http請求的模版,程式碼如下,程式碼的實現如下:

@Override
public Object invoke(Object[] argv) throws Throwable {    //argv,表示呼叫方法傳遞的引數。      
  RequestTemplate template = buildTemplateFromArgs.create(argv);  
  Options options = findOptions(argv); //獲取配置項,連線超時時間、遠端通訊資料獲取超時時間  
  Retryer retryer = this.retryer.clone(); //獲取重試策略  
  while (true) {    
    try {      
      return executeAndDecode(template, options);    
    } catch (RetryableException e) {
      try {        
        retryer.continueOrPropagate(e);      
      } catch (RetryableException th) {
        Throwable cause = th.getCause();        
        if (propagationPolicy == UNWRAP && cause != null) {
          throw cause;        
        } else {
          throw th;
        }      
      }      
      if (logLevel != Logger.Level.NONE) {
        logger.logRetry(metadata.configKey(), logLevel);      
      }      
      continue;    
    }  
  }
}

上述程式碼的執行流程中,需要先構造一個Request物件,然後呼叫client.execute方法執行網路通訊請求,整體實現流程如下。

image-20211215192557345

executeAndDecode

經過上述的程式碼,我們已經將RequestTemplate拼裝完成,上面的程式碼中有一個executeAndDecode()方法,該方法通過RequestTemplate生成Request請求物件,然後利用Http Client獲取response,來獲取響應資訊。

Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
  Request request = targetRequest(template);  //把template轉化為請求報文    
  if (logLevel != Logger.Level.NONE) { //設定日誌level      
    logger.logRequest(metadata.configKey(), logLevel, request); 
  }    
  Response response;   
  long start = System.nanoTime();  
  try {     
    //發起請求,此時client是LoadBalanceFeignClient,需要先對服務名稱進行解析負載    
    response = client.execute(request, options);      // ensure the request is set. TODO: remove in Feign 12          //獲取返回結果   
    response = response.toBuilder().request(request).requestTemplate(template).build(); 
  } catch (IOException e) { 
    if (logLevel != Logger.Level.NONE) {   
      logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));   
    }     
    throw errorExecuting(request, e);  
  }  
  long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);   
  if (decoder != null) //如果設定瞭解碼器,這需要對響應資料做解碼    
    return decoder.decode(response, metadata.returnType());   
  CompletableFuture<Object> resultFuture = new CompletableFuture<>();  
  asyncResponseHandler.handleResponse(resultFuture, metadata.configKey(), response,        metadata.returnType(),        elapsedTime);  
  try {  
    if (!resultFuture.isDone())  
      throw new IllegalStateException("Response handling not done"); 
    return resultFuture.join();   
  } catch (CompletionException e) {     
    Throwable cause = e.getCause();   
    if (cause != null)    
      throw cause;    
    throw e;  
  } 
}

LoadBalanceClient

@Overridepublic
Response execute(Request request, Request.Options options) throws IOException { 
  try {   
    URI asUri = URI.create(request.url()); //獲取請求uri,此時的地址還未被解析。 
    String clientName = asUri.getHost(); //獲取host,實際上就是服務名稱  
    URI uriWithoutHost = cleanUrl(request.url(), clientName);    
    FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(this.delegate, request, uriWithoutHost);		//載入客戶端的配置資訊     
    IClientConfig requestConfig = getClientConfig(options, clientName);       //建立負載均衡客戶端(FeignLoadBalancer),執行請求   
    return lbClient(clientName).executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();   
  } catch (ClientException e) {
    IOException io = findIOException(e); 
    if (io != null) {     
      throw io;    
    }     
    throw new RuntimeException(e); 
  }
}

從上面的程式碼可以看到,lbClient(clientName) 建立了一個負載均衡的客戶端,它實際上就是生成的如下所述的類:

public class FeignLoadBalancer extends		AbstractLoadBalancerAwareClient<FeignLoadBalancer.RibbonRequest, FeignLoadBalancer.RibbonResponse> {

整體總結

OpenFeign的整體通訊原理解析,如下圖所示。

image-20211215192455398

版權宣告:本部落格所有文章除特別宣告外,均採用 CC BY-NC-SA 4.0 許可協議。轉載請註明來自 Mic帶你學架構
如果本篇文章對您有幫助,還請幫忙點個關注和贊,您的堅持是我不斷創作的動力。歡迎關注「跟著Mic學架構」公眾號公眾號獲取更多技術乾貨!

相關文章