Spring Cloud 源碼分析之OpenFeign

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
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,367評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,001評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,213評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,535評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,317評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,868評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,963評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,090評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,599評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,549評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,712評論 1 367
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,233評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,961評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,353評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,607評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,321評論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,686評論 2 370

推薦閱讀更多精彩內容