天天看點

聲明式服務調用-Feign 源碼分析

聲明式服務調用-Feign 源碼分析

文章目錄

    • 聲明式服務調用-Feign 源碼分析
    • 前言
    • 項目環境
    • 1.Feign 主要元件
    • 2.Feign 執行過程
    • 3.Feign & Hystrix 內建
      • 3.1 FeignClient 配置類 FeignClientsConfiguration
      • 3.2 自動裝配 FeignAutoConfiguration
    • 4.Feign & Ribbon 內建
    • [email protected] 工作原理
      • 5.1 @FeignClient 處理過程
      • 5.2 代理類如何處理?
    • 6.Spring MVC 注解內建
    • 7.參考

前言

上一章 《聲明式服務調用-Feign 基礎篇》 我們介紹了 Feign 的基本概念和使用方式;本章我們主要介紹 Feign 的一些主要元件和整個元件的執行過程,重點讨論 @FeignClient 注解的工作原理,在其處理的過程中如何內建 Hystrix、Ribbon 等分布式元件,最後會簡單的看看 Spring Cloud 如何使用 Spring MVC 的注解擴充相容 Feign。

項目環境

  • Java 8
  • Spring Cloud Greenwich.SR2
  • Spring Boot 2.1.6.RELEASE
  • 項目位址:https://github.com/huajiexiewenfeng/deep-in-spring-cloud-netflix

1.Feign 主要元件

Feign 主要元件如圖:

聲明式服務調用-Feign 源碼分析

相關源碼的位置 feign.Feign.Builder 中

  • Contract 契約元件

在 Feign 中可以通過定義 API 接口的方式來調用遠端的 Http API,在定義調用 Client 的時候需要增加一些注解來描述整個調用 API 的基本資訊,比如請求類型,請求 URI 等等。

Contract 允許使用者自定義契約去解析注解資訊,最典型的應用場景就是在 Spring Cloud 中使用 Feign,我們可以使用 Spring MVC 的注解來定義 Feign 的用戶端,這是因為 Spring Cloud OpenFeign 中實作了自己的 SpringMvcContract。

  • Encoder 編碼元件

通過該元件我們可以将請求資訊采用指定的編碼方式進行編碼後傳輸。

  • Decoder 解碼元件

Decoder 将相應資料解碼成對象。

  • ErrorDecoder 異常解碼器

當被調用方發生異常後,我們可以在 ErrorDecoder 中将響應的資料轉換成具體的異常傳回給調用方,适合内部服務之間調用,但不想通過指定的字段來判斷是否成功的場景,直接用自定義異常代替。

  • Logger 日志記錄

Logger 元件是負責 Feign 中記錄日志的,可以指定 Logger 的級别以及自定義日志的輸出。

  • Client 請求執行元件

Client 是負責 HTTP 請求執行的元件,Feign 将請求資訊封裝好後會交由 Client 來執行,Feign 中預設的 Client 是通過 JDK 的 HttpURLConnection 來發起請求的,在每次發送請求的時候,都會建立新的 HttpURLConnection 連結,使用預設的方式,Feign 的性能會很差,原因就是使用了 HttpURLConnection。你可以通過擴充該接口,使用 Apache HttpClient 等基于連接配接池的高性能 HTTP 用戶端。

  • Retryer 重試元件

Retryer 是負責重試的元件,Feign 内置了重試器,當 HTTP 請求出現 IO 異常時,Feign 會限定一個最大重試次數來進行重試操作。

  • InvocationHandler 代理

InvocationHandlerFactory 采用 JDK 的動态代理方式生成代理對象,我們定義的 Feign Client 是接口,當我們調用這個接口中定義的方法時,實際上是要去調用遠端的 HTTP API,這裡用了動态代理方式,當調用某個方法時,會進入代理中真正的去調用遠端 HTTP API。

  • RequestInterceptor 請求攔截器

可以為 Feign 添加多個攔截器,在請求執行前設定一些擴充的參數資訊。

  • QueryMapEncoder 參數查詢

QueryMapEncoder 是針對實體類參數查詢的編碼器,可以基于 QueryMapEncoder 将實體類生成對應的查詢參數。

2.Feign 執行過程

聲明式服務調用-Feign 源碼分析

在 Spring Cloud 中使用 Feign 時,我們會在接口類上使用 Feign 自帶的注解 @FeignClient 來辨別需要通路服務的名稱,并且在相關的接口方法上标注 URL 資訊以及請求參數資訊,當調用接口對應的方法時,Feign 内部會基于面向接口的動态代理方式生成實作類,将請求調用委托到動态代理實作類,負責動态代理的元件是 InvocationHandlerFactory。

根據 Contract 規則,解析接口類的注解資訊,翻譯成 Feign 内部能識别的資訊。Feign 預設有一套自己的協定規範,我們也可以自定義其他的規範來進行擴充,在 Spring Cloud OpenFeign 中就擴充了 SpringMvcContract,這樣做的目的是為了降低學習和使用成本,用戶端和服務端使用同一個接口定義,釋出成 SDK 給調用方使用。

MethodHandler 在執行的時候會生成 Request 對象,在建構 Request 對象的時候會為其設定攔截器,交由 Client 執行前記錄一些日志,Client 執行完成後也記錄一些日志,然後使 Decoder 進行相應結果的解碼操作,并傳回結果。

3.Feign & Hystrix 內建

3.1 FeignClient 配置類 FeignClientsConfiguration

  • FeignClientsConfiguration.HystrixFeignConfiguration#feignHystrixBuilder
@Configuration
	@ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class })
	protected static class HystrixFeignConfiguration {

		@Bean
		@Scope("prototype")
		@ConditionalOnMissingBean
		@ConditionalOnProperty(name = "feign.hystrix.enabled")
		public Feign.Builder feignHystrixBuilder() {
			return HystrixFeign.builder();
		}

	}
           

條件裝配

  • 設定 feign.hystrix.enabled = true 就會裝配 Feign.Builder 類

HystrixFeign 看名稱就是內建了 Hystrix 的 Feign 實作,我們進入 HystrixFeign.builder 中可以看到繼承了 Feign 的 Builder,對 invocationHandlerFactory 進行了重寫,在 create 的時候傳回的是 HystrixInvocationHandler,HystrixInvocationHandler 中在 invoke 的時候會将請求包裝成 HystrixCommand 去執行,這裡就自然的內建了 Hystrix。

3.2 自動裝配 FeignAutoConfiguration

  • FeignAutoConfiguration.HystrixFeignTargeterConfiguration
@Configuration
	@ConditionalOnClass(name = "feign.hystrix.HystrixFeign")
	protected static class HystrixFeignTargeterConfiguration {

		@Bean
		@ConditionalOnMissingBean
		public Targeter feignTargeter() {
			return new HystrixTargeter();
		}

	}
           

條件裝配

  • feign.hystrix.HystrixFeign 存在 ClassPath 中
  • 如果 Targeter 不存在 Spring IoC 容器中

滿足上面兩個條件,就會自動裝配 HystrixTargeter 對象,而在 @FeignClient 處理過程中,封裝代理 Feign Client 代理對象時,會通過 ApplicationContext.getBean 依賴查找的方式從 Spring IoC 容器擷取 HystrixTargeter 對象來進行封裝。

4.Feign & Ribbon 內建

Feign & Ribbon 內建的過程和 Hystrix 內建的過程如出一轍,同樣是通過自動裝配+依賴查找

  • 通過 @Bean + 條件注解方式注入 Client 對象,看名稱這是一個 LoadBalancer 也就是內建了 Ribbon 的對象
@Configuration
class DefaultFeignLoadBalancedConfiguration {

	@Bean
	@ConditionalOnMissingBean
	public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
			SpringClientFactory clientFactory) {
		return new LoadBalancerFeignClient(new Client.Default(null, null), cachingFactory,
				clientFactory);
	}

}
           

依賴查找的過程封裝代理 Feign Client 代理對象的過程下面會具體分析。

代碼調試如下:

  • org.springframework.cloud.openfeign.FeignClientFactoryBean#loadBalance 236 行
聲明式服務調用-Feign 源碼分析

可以看到封裝的過程中

getOptional(context, Client.class)

拿到的 Client 就是之前配置好的 LoadBalancerFeignClient 對象。

[email protected] 工作原理

5.1 @FeignClient 處理過程

這一小節,我們來一起看看标注 @FeignClient 注解的接口,如何被替換成代理類,并且注入到 Spring IoC 容器中。

首先第一步 @EnableFeignClients 用來激活 Feign 相關的元件,這部分内容不熟悉的可以參考 《@Enable 子產品驅動》 來了解相關的細節,這裡我們隻關注 feign 相關實作。

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

FeignClientsRegistrar 類采用的是實作 ImportBeanDefinitionRegistrar 接口的模式,其中的核心代碼

@Override
	public void registerBeanDefinitions(AnnotationMetadata metadata,
			BeanDefinitionRegistry registry) {
		registerDefaultConfiguration(metadata, registry);
		registerFeignClients(metadata, registry);
	}
           
  • registerDefaultConfiguration 注冊預設的配置
  • registerFeignClients 注冊 FeignClient 對象

這裡我們看 registerFeignClients 方法的調用鍊路

1.FeignClientsRegistrar#registerBeanDefinitions

  • 擷取 @FeignClient 注解相關的參數資訊,比如 value,fallback 等等
  • registerFeignClient(registry, annotationMetadata, attributes);

2.FeignClientsRegistrar#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);

		String alias = contextId + "FeignClient";
		AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();

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

		beanDefinition.setPrimary(primary);

		String qualifier = getQualifier(attributes);
		if (StringUtils.hasText(qualifier)) {
			alias = qualifier;
		}

		BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
				new String[] { alias });
		BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
	}
           

通過

BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);

的方式來建構BeanDefinition,并設定相關的屬性資訊,最後将 BeanDefinition 注冊到 Spring IoC 容器中。

  • BeanDefinition 定義和注冊方式可以參考 《Spring Bean 定義》

總結 @FeignClient 處理過程

@EnableFeignClients 激活 -> FeignClientsRegistrar -> registerBeanDefinitions -> registerFeignClient,最終将相關的元資訊封裝成 BeanDefinition,通過 registerBeanDefinition 的方式注冊到 Spring IoC 容器。

然後我們在業務代碼中使用 FeignClient 對象,隻需要通過 @Autowired 依賴注入的方式就可以從 Spring IoC 容器中拿到這個對象。

5.2 代理類如何處理?

上面的處理過程有一個小細節,就是

BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);

這個代理類的處理過程并不是通過 Proxy.newProxyInstance 這種傳統方式

直接

來建構的,而是通過 FactoryBean 的方式,我們來看看 FeignClientFactoryBean 的實作細節。

第一個是 feign API 的封裝

  • 可以看到 FeignClientFactoryBean 幫我們進行了 feign 原生 API 的封裝
protected Feign.Builder feign(FeignContext context) {
		FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);
		Logger logger = loggerFactory.create(this.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));
		// @formatter:on

		configureFeign(context, builder);

		return builder;
	}
           

第二個是擷取代理對象

  • Factorybean 是通過 getObject 的方式來延遲擷取對象
@Override
	public Object getObject() throws Exception {
		return getTarget();
	}

	<T> T getTarget() {
        // 依賴查找的方式擷取 FeignContext 對象
		FeignContext context = this.applicationContext.getBean(FeignContext.class);
        // 封裝成 Feign 對象
		Feign.Builder builder = feign(context);
	    // 如果 URL 為空
		if (!StringUtils.hasText(this.url)) {
			if (!this.name.startsWith("http")) {
				this.url = "http://" + this.name;
			}
			else {
				this.url = this.name;
			}
			this.url += cleanPath();
			return (T) loadBalance(builder, context,
					new HardCodedTarget<>(this.type, this.name, this.url));
		}
    ...// 本示例啟動時不會走這段邏輯
	}
           

啟動時我們在 FeignClientFactoryBean#getTarget 259 行打上斷點

聲明式服務調用-Feign 源碼分析

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-netflix-ribbon?");
	}
           

其實通過

getOptional(context, Client.class);

拿到的是一個 LoadBalancerFeignClient,因為

DefaultFeignLoadBalancedConfiguration

預設自動配置內建了 Ribbon,是以這裡根據類型擷取的就是一個 LoadBalancerFeignClient 的對象,具體的代碼如下:

  • FeignRibbonClientAutoConfiguration#feignRequestOptions
@Bean
	@ConditionalOnMissingBean
	public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
			SpringClientFactory clientFactory) {
		return new LoadBalancerFeignClient(new Client.Default(null, null), cachingFactory,
				clientFactory);
	}
           

同樣的這裡的 Targeter 也是通過自動配置的方式注入到了 Spring IoC 容器中

FeignAutoConfiguration.HystrixFeignTargeterConfiguration#feignTargeter

繼續

targeter.target

方法

class HystrixTargeter implements Targeter {

	@Override
	public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign,
			FeignContext context, Target.HardCodedTarget<T> target) {
		if (!(feign instanceof feign.hystrix.HystrixFeign.Builder)) {
			return feign.target(target);
		}
		feign.hystrix.HystrixFeign.Builder builder = (feign.hystrix.HystrixFeign.Builder) feign;
		SetterFactory setterFactory = getOptional(factory.getName(), context,
				SetterFactory.class);
		if (setterFactory != null) {
			builder.setterFactory(setterFactory);
		}
		Class<?> fallback = factory.getFallback();
		if (fallback != void.class) {
			return targetWithFallback(factory.getName(), context, target, builder,
					fallback);
		}
		Class<?> fallbackFactory = factory.getFallbackFactory();
		if (fallbackFactory != void.class) {
			return targetWithFallbackFactory(factory.getName(), context, target, builder,
					fallbackFactory);
		}

		return feign.target(target);
	}
    ...
           

feign.Feign.Builder#target 方法代碼如下:

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

feign.ReflectiveFeign#newInstance 方法代碼如下:

@Override
  public <T> T newInstance(Target<T> target) {
    Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
    Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
    List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();

    for (Method method : target.type().getMethods()) {
      if (method.getDeclaringClass() == Object.class) {
        continue;
      } else if (Util.isDefault(method)) {
        DefaultMethodHandler handler = new DefaultMethodHandler(method);
        defaultMethodHandlers.add(handler);
        methodToHandler.put(method, handler);
      } else {
        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;
  }
           

這裡我們斷點調試一下代碼,将斷點打在 feign.ReflectiveFeign#newInstance 53 行

聲明式服務調用-Feign 源碼分析

可以看到 nameToHandler 對象中存放的是 com.csdn.feign.user.api.service.UserServiceFeignApi 對應的三個方法,繼續往下執行代碼,最後會将 nameToHandler 對象轉化為 methodToHandler 對象

聲明式服務調用-Feign 源碼分析

通過 feign.InvocationHandlerFactory 來建立 InvocationHandler 對象,代碼如下:

最後還是通過 Proxy.newProxyInstance 這種方式來建立代理對象,然後傳回 proxy 代理對象。

聲明式服務調用-Feign 源碼分析

總結代理類如何處理

整個的調用鍊路如下:

BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class); -> FeignClientFactoryBean -> getObject -> getTarget -> loadBalance -> targeter.target -> feign.Feign.Builder#target -> feign.ReflectiveFeign#newInstance -> feign.InvocationHandlerFactory.create -> Proxy.newProxyInstance

  • 首先通過 BeanDefinitionBuilder 來建構 BeanDefinition (每個 FeignClient 對象 Bean 的定義資訊);
  • 然後在 FeignClientFactoryBean 中來封裝 feign 對象以及相關配置;
  • 再調用 getObject 方法,在方法中內建 Ribbon、Hystrix 等分布式元件;
  • 最後調用 ReflectiveFeign#newInstance 來建立代理對象,通過 feign.InvocationHandlerFactory.create 來建立 InvocationHandler 對象;
  • 最終還是通過 Proxy.newProxyInstance 這種 JDK 動态代理的方式來建立的最後的 proxy 代理對象。

補充:FactoryBean#getObject 方法在何時被調用?

在 Spring 的 ApplicationContext 應用上下文啟動時,finishBeanFactoryInitialization 方法也就是 BeanFactory 初始化完成階段,通過 beanDefinitionNames 來周遊我們所有的 BeanDefintion,逐一進行 getBean(beanName) 操作,通過我們的 BeanDefinition 建立 bean 對象,并緩存到 DefaultSingletonBeanRegistry#singletonObjects 中。

其中 getBean 的過程就會觸發 FactoryBean#getObject 來擷取代理對象。

6.Spring MVC 注解內建

  • org.springframework.cloud.openfeign.FeignClientsConfiguration

在這個類中,Spring cloud 預設配置了很多相關的 feign 元件,其中和 Spring Mvc 注解相關的如下:

@Bean
	@ConditionalOnMissingBean
	public Contract feignContract(ConversionService feignConversionService) {
		return new SpringMvcContract(this.parameterProcessors, feignConversionService);
	}
           

SpringMvcContract 繼承了 Contract.BaseContract ,我們再看看 Contract.BaseContract,它的實作類隻有兩個

  • SpringMvcContract
  • feign.Contract.Default 預設的解析注解資訊處理類

我們隻需要對比這兩個實作類其中幾個主要的方法即可

  • processAnnotationOnClass
  • processAnnotationOnMethod
  • processAnnotationsOnParameter

三個方法分别對應類,方法,參數,SpringMvcContract 中通過 @RequestMapping 注解來重新實作相關的功能,并增加了四種對應參數的處理者,分别對應 SpringMvc 中四種參數類型的處理,具體細節這裡就不展開了。

private List<AnnotatedParameterProcessor> getDefaultAnnotatedArgumentsProcessors() {

		List<AnnotatedParameterProcessor> annotatedArgumentResolvers = new ArrayList<>();

		annotatedArgumentResolvers.add(new PathVariableParameterProcessor());
		annotatedArgumentResolvers.add(new RequestParamParameterProcessor());
		annotatedArgumentResolvers.add(new RequestHeaderParameterProcessor());
		annotatedArgumentResolvers.add(new QueryMapParameterProcessor());

		return annotatedArgumentResolvers;
	}
           

7.參考

  • 《深入了解 Spring Cloud 與微服務架構》 方志朋
  • 《300分鐘搞懂 Spring Cloud》尹吉歡

繼續閱讀