微服務通訊之feign的註冊、發現過程

泥粑發表於2020-09-30

前言

feign 是目前微服務間通訊的主流方式,是springCloud中一個非常重要的元件。他涉及到了負載均衡、限流等元件。真正意義上掌握了feign可以說就掌握了微服務。

一、feign的使用

feign 的使用和dubbo的使用本質上非常相似。dubbo的理念是:像呼叫本地方法一樣呼叫遠端方法。那麼套在feign上同樣適用:像呼叫本地介面一樣呼叫遠端介面。
使用feign只需要2步:定義一個介面並用FeignClient註解說明介面所在服務和路徑,服務啟動類上新增@EnableFeignClients。如下所示

1.1,定義一個feign介面

@FeignClient(contextId = "order", name = "order", path = "/app")
public interface OrderApiFeignClient {

   /**
    * 獲取訂單列表
    * @return
    */
   @RequestMapping("order/list")
   BaseResponse<List<OrderVO>> obtaining(@PathVariable("userId") Long userId);
}

1.2,再啟動類上新增註解


@EnableSwagger2
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients("com.xxx.*")
@ComponentScan(value={"com.xxx"})
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication .class, args);
    }
}

二、feign 介面如何被例項化到spring容器的?

首先按照一般的思路,我們會猜測基於介面生成代理類,然後對介面的呼叫實際上調的是代理物件,那真的是這樣麼? 我們帶著猜想往下看。

2.1 @EnableFeignClients 註解都做了些什麼?

可以看到註解本身主要定義了要掃描的feign介面包路徑以及配置,但是註解本身又有註解Import ,可以看到他引入了FeignClientsRegistrar到容器。從名字看這個類就應該是在將feign介面註冊到容器中,接下來我們具體看一下這個類幹了些什麼。

/**
 * @author Spencer Gibb
 * @author Jakub Narloch
 * @author Venil Noronha
 * @author Gang Li
 */
class FeignClientsRegistrar
		implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {

可以看到FeignClientsRegistrar實現了ImportBeanDefinitionRegistrar介面,但凡是實現了這個介面的類被注入到容器後,spring容器在啟用過程中都會去呼叫它的void registerBeanDefinitions(AnnotationMetadata var1, BeanDefinitionRegistry var2)方法,可以確定的是FeignClientsRegistrar肯定重寫了此方法,我們接下來看一下該方法的實現。

可以看到在這個方法中做了兩件事: 1)註冊feign配置, 2)註冊feign介面。我們這裡抓一下重點,看一下feign介面是怎麼註冊的?


public void registerFeignClients(AnnotationMetadata metadata,
			BeanDefinitionRegistry registry) {
		ClassPathScanningCandidateComponentProvider scanner = getScanner();
		scanner.setResourceLoader(this.resourceLoader);

		Set<String> basePackages;

		Map<String, Object> attrs = metadata
				.getAnnotationAttributes(EnableFeignClients.class.getName());
		AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
				FeignClient.class);
		final Class<?>[] clients = attrs == null ? null
				: (Class<?>[]) attrs.get("clients");
		if (clients == null || clients.length == 0) {
                        // 限定只掃描FeingClient註解
			scanner.addIncludeFilter(annotationTypeFilter);
			basePackages = getBasePackages(metadata);
		}
		else {
			final Set<String> clientClasses = new HashSet<>();
			basePackages = new HashSet<>();
			for (Class<?> clazz : clients) {
				basePackages.add(ClassUtils.getPackageName(clazz));
				clientClasses.add(clazz.getCanonicalName());
			}
			AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
				@Override
				protected boolean match(ClassMetadata metadata) {
					String cleaned = metadata.getClassName().replaceAll("\\$", ".");
					return clientClasses.contains(cleaned);
				}
			};
			scanner.addIncludeFilter(
					new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
		}

		for (String basePackage : basePackages) {
			Set<BeanDefinition> candidateComponents = scanner
					.findCandidateComponents(basePackage);
			for (BeanDefinition candidateComponent : candidateComponents) {
				if (candidateComponent instanceof AnnotatedBeanDefinition) {
					// verify annotated class is an interface
					AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
					AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
					Assert.isTrue(annotationMetadata.isInterface(),
							"@FeignClient can only be specified on an interface");

					Map<String, Object> attributes = annotationMetadata
							.getAnnotationAttributes(
									FeignClient.class.getCanonicalName());

					String name = getClientName(attributes);
					registerClientConfiguration(registry, name,
							attributes.get("configuration"));
                                        // 這裡生成bean並且註冊到容器
					registerFeignClient(registry, annotationMetadata, attributes);
				}
			}
		}
	}

上面這段程式碼概括起來就是: 先找了包路徑basePackages , 然後在從這些包路徑中查詢帶有FeignClient註解的介面,最後將註解的資訊解析出來作為屬性手動構建beanDefine注入到容器中。(這裡有一個類ClassPathScanningCandidateComponentProvider,它可以根據filter掃描指定包下面的class物件,十分好用,建議收藏)。包路徑的獲取以及掃描feign相對簡單,這裡不做闡述,我們看一下它生成bean的過程,關注上面程式碼中的registerFeignClient方法。


private void registerFeignClient(BeanDefinitionRegistry registry,
			AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
		String className = annotationMetadata.getClassName();
		BeanDefinitionBuilder definition = BeanDefinitionBuilder
				.genericBeanDefinition(FeignClientFactoryBean.class);
		validate(attributes);
		definition.addPropertyValue("url", getUrl(attributes));
		definition.addPropertyValue("path", getPath(attributes));
		String name = getName(attributes);
		definition.addPropertyValue("name", name);
		String contextId = getContextId(attributes);
		definition.addPropertyValue("contextId", contextId);
		definition.addPropertyValue("type", className);
		definition.addPropertyValue("decode404", attributes.get("decode404"));
		definition.addPropertyValue("fallback", attributes.get("fallback"));
		definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
		definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
                // 這裡省略部分程式碼
		BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
	}

程式碼中通過BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class)生成的BeanDefine(請記住這裡設定得FeignClientFactoryBean.type就是feign介面對應得class物件)。那麼所有的feign介面最終註冊到容器中的都是FeignClientFactoryBean對應的一個例項(注意實際上註冊到容器中壓根就不是FeignClientFactoryBean對應的例項化物件,具體原因看下文),到此feign介面對應的例項註冊過程已經完成。那麼回到一開始的問題為什麼我們呼叫介面的方法最終發起了請求? 是否有代理類的生成呢? 我們接下來看看FeignClientFactoryBean類的特殊之處

2.2 FeignClientFactoryBean 類,feign介面代理生成類

由上文知,每一個feign介面實際上最終都會生成FeignClientFactoryBean ,最終由FeignClientFactoryBean生成具體的bean例項註冊到容器中。


/**
 * @author Spencer Gibb
 * @author Venil Noronha
 * @author Eko Kurniawan Khannedy
 * @author Gregor Zurowski
 */
class FeignClientFactoryBean
		implements FactoryBean<Object>, InitializingBean, ApplicationContextAware 

可以看到該類實現了FactoryBean介面,這意味著當Spring註冊該bean例項到容器中時,實際是呼叫其getObject方法,那麼FeignClientFactoryBean一定是重寫了getObject()方法,接下來我們看一下getObject()幹了什麼事情:

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

我們繼續追蹤getTarget()方法:

	<T> T getTarget() {
		FeignContext context = this.applicationContext.getBean(FeignContext.class);
		Feign.Builder builder = feign(context);
  
                // 省略部分程式碼...
		 
		Targeter targeter = get(context, Targeter.class);
		return (T) targeter.target(this, builder, context,
				new HardCodedTarget<>(this.type, this.name, url));
	}

顯然最終的bean是通過target.target()方法生成,我們繼續往下看:

	@Override
	public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign,
			FeignContext context, Target.HardCodedTarget<T> target) {
		return feign.target(target);
	}

顯然最終的bean是通過feign.target(target)生成。我們繼續往下看:

    public <T> T target(Target<T> target) {
      return build().newInstance(target);
    }

顯然最終得bean是通過build().newInstance(target)生成。我們繼續往下看:

  public <T> T newInstance(Target<T> target) {
    // 省略部分程式碼
    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;
  }

可以看到Proxy.newProxyInstance這個熟悉得身影了,沒錯他就是基於JDK原生得動態代理生成了FeignClientFactoryBean.type屬性對應得class對應得代理類。從前文我們知道FeignClientFactoryBean.type就是feign介面得class物件。所以最終我們呼叫feign介面得方法實際上呼叫得是InvocationHandler方法。

三、小結

總結起來,就是常常我們掛在口頭的東西就是將feign介面生成代理類,然後呼叫代理介面方法其實呼叫的代理類得方法,具體是為什麼?不知道大家是否清楚。希望通過本文的閱讀能讓大家閱讀原始碼的能力得到提升,也不在對feign有一種黑盒子的感覺。可能篇幅看起來較少,其實feign的註冊過程牽涉到框架層面的知識還是蠻多的,包括springIoc、BeanDefine、動態代理等等,仔細看明白的話收穫應該還是有蠻多的。哈哈,懂得都懂。順手提一句:讀原始碼一定要對SPI等等特別熟悉,要不然你會無從下手,沒有方向,抓不到重點。後續會更新文章講feign怎麼實現負載均衡、熔斷等。

(本文原創、轉載請註明出處)

相關文章