天天看點

Spring Cloud OpenFeign 工作原了解析參考文章背景使用方式工作原理工作原理實作細節

參考文章

深入了解Feign之源碼解析

背景

OpenFeign 是 Spring Cloud 家族的一個成員, 它最核心的作用是為 HTTP 形式的 Rest API 提供了非常簡潔高效的 RPC 調用方式。 如果說 Spring Cloud 其他成員解決的是系統級别的可用性,擴充性問題, 那麼 OpenFeign 解決的則是與開發人員利益最為緊密的開發效率問題。

Spring Cloud OpenFeign 工作原了解析參考文章背景使用方式工作原理工作原理實作細節

使用方式

在介紹 OpenFeign 的工作原理之前, 首先值得說明的是使用了 Open Feign 後, 開發人員的效率是如何得到提升的。 下面展示在使用了 OpenFeign 之後, 一個接口的提供方和消費方是如何快速高效地完成代碼開發。

接口提供方

接口提供方的形式為 RestApi, 這個在 spring-web 架構的支援下, 編寫起來非常簡單

@RestController
@RequestMapping(value = "/api")
public class ApiController {

	@RequestMapping(value = "/demoQuery", method = {RequestMethod.POST}, consumes = MediaType.APPLICATION_JSON_VALUE)
    public ApiBaseMessage<DemoModel> demoQuery(@RequestBody DemoQryRequest request){
        return new ApiBaseMessage(new DemoModel());
    }
}
           

如上, 除去請求 DemoQryRequest 和響應 DemoModel 類的定義, 這個接口就已經快速地被完成了。 在這裡 Feign 不需要發揮任何作用。

注意該接口的入參是 json 格式, 架構會自動幫我們反序列化到對應的 DemoQryRequest 類型的入參對象裡。

傳回值

ApiBaseMessage<DemoModel>

也會被架構自動序列化為 json 格式

接口使用方

在接口的使用者一端, 首先需要引入 SpringFeign 依賴(為簡化篇幅, 隻展示 build.gradle 中添加 Feign 的依賴, 沒有展示其他的 spring cloud 依賴添加)

@Component
@FeignClient(name = "${feign.demoApp.name}")
@RequestMapping("/api")
public interface DemoService {
    @RequestMapping(value = "/demoQuery", method = RequestMethod.POST,
            consumes = MediaType.APPLICATION_JSON_VALUE)
    ApiBaseMessage<DemoModel> demoQuery(@RequestBody DemoQryRequest request);
}
           

再直接利用 spring 的自動注入功能, 就可以使用服務端的接口了

@Component
public class DemoServiceClient
{
    private final DemoService demoService;

    @Autowired
    public DemoServiceClient(DemoService demoService) {
        this.demoService= demoService;
    }

    public void useDemoService(DemoQryRequest request){
        // 直接像調用一個本地方法一樣, 調用遠端的 Rest API 接口, 完全是 RPC 形式
        ApiBaseMessage<DemoModel> result = demoService.demoQuery(request);
    }
}
           

通過上面的例子可以看到, Feign 正如同其英文含義"假裝"一樣, 能夠讓我們裝作調用一個本地 java 方法一樣去調用基于 HTTP 協定的 Rest API 接口。 省去了我們編寫 HTTP 連接配接,資料解析擷取等一系列繁瑣的操作

工作原理

在展開講解工作原理前, 首先捋一下上文中, 我們完成 Feign 調用前所進行的操作:

  1. 添加了 Spring Cloud OpenFeign 的依賴
  2. 在 SpringBoot 啟動類上添加了注解

    @EnableFeignCleints

  3. 按照 Feign 的規則定義接口

    DemoService

    , 添加

    @FeignClient

    注解
  4. 在需要使用 Feign 接口 DemoService 的地方, 直接利用@Autowire 進行注入
  5. 使用接口完成對服務端的調用

可以根據上面使用 Feign 的步驟大緻猜測出整體的工作流程:

  1. SpringBoot 應用啟動時, 由針對

    @EnableFeignClient

    這一注解的處理邏輯觸發程式掃描 classPath中所有被

    @FeignClient

    注解的類, 這裡以

    DemoService

    為例, 将這些類解析為 BeanDefinition 注冊到 Spring 容器中
  2. Sping 容器在為某些用的 Feign 接口的 Bean 注入

    DemoService

    時, Spring 會嘗試從容器中查找 DemoService 的實作類
  3. 由于我們從來沒有編寫過

    DemoService

    的實作類, 上面步驟擷取到的 DemoService 的實作類必然是 feign 架構通過擴充 spring 的 Bean 處理邏輯, 為

    DemoService

    建立一個動态接口代理對象, 這裡我們将其稱為

    DemoServiceProxy

    注冊到spring 容器中。
  4. Spring 最終在使用到

    DemoService

    的 Bean 中注入了

    DemoServiceProxy

    這一執行個體。
  5. 當業務請求真實發生時, 對于

    DemoService

    的調用被統一轉發到了由 Feign 架構實作的

    InvocationHandler

    中,

    InvocationHandler

    負責将接口中的入參轉換為 HTTP 的形式, 發到服務端, 最後再解析 HTTP 響應, 将結果轉換為 Java 對象, 予以傳回。

上面整個流程可以進一步簡化了解為:

  1. 我們定義的接口

    DemoService

    由于添加了注解

    @FeignClient

    , 最終産生了一個虛假的實作類代理
  2. 使用這個接口的地方, 最終拿到的都是一個假的代理實作類

    DemoServiceProxy

  3. 所有發生在

    DemoServiceProxy

    上的調用, 都被轉交給 Feign 架構, 翻譯成 HTTP 的形式發送出去, 并得到傳回結果, 再翻譯回接口定義的傳回值形式。

是以不難發現, Feign 的核心實作原理就是java 原生支援的基于接口的動态代理

工作原理實作細節

FeignClient 的掃描與注冊

FeignClient 的掃描與注冊是基于 Spring 架構的 Bean 管理機制實作的,不了解原理的同學可以考慮閱讀博文那些你應該掌握的 Spring 原理

這裡簡單叙述 SpringBoot 應用中的掃描觸發流程:

SpringApplication.run() -->

SpringApplication.refresh() -->

AbstractApplicationContext.refresh() --> AbstractApplicationContext.invokeBeanFactoryPostProcessors() -->

AbstractApplicationContext.invokeBeanDefinitionRegistryPostProcessors() -->

補充知識點: 上面的 invokeBeanFactoryPostProcessors() 能觸發invokeBeanDefinitionRegistryPostProcessors() 是因為 Spring 設計中, BeanDeifinitionRegistryPostProcessor 是 BeanFactoryPostProcessor 的繼承

PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors()–>

ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry()–>

ConfigurationClassPostProcessor.processConfigBeanDefinitions()–>

ConfigurationClassBeanDefinitionReader.loadBeanDefinitions()–>

ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsFromRegistrars -->

FeignClientsRegistrar.registerBeanDefinitions()

到這裡, 我們進入了 Feign 架構的邏輯 FeignClientsRegistrar.registerBeanDefinitions()

@Override
	public void registerBeanDefinitions(AnnotationMetadata metadata,
			BeanDefinitionRegistry registry) {
		// registerDefaultConfiguration 方法内部從 SpringBoot 啟動類上檢查是否有 @EnableFeignClients, 有該注解的話, 則完成 Feign 架構相關的一些配置内容注冊
		registerDefaultConfiguration(metadata, registry);
		// registerFeignClients 方法内部從 classpath 中, 掃描獲得 @FeignClient 修飾的類, 将類的内容解析為 BeanDefinition , 最終通過調用 Spring 架構中的 BeanDefinitionReaderUtils.resgisterBeanDefinition 将解析處理過的 FeignClient BeanDeifinition 添加到 spring 容器中
		registerFeignClients(metadata, registry);
	}
           

這裡值得進一步關注的是,

registerFeignClients

方法内部, 調用了一個

registerFeignClient

方法

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

public static BeanDefinitionBuilder genericBeanDefinition(Class<?> beanClass) {
	BeanDefinitionBuilder builder = new BeanDefinitionBuilder(new GenericBeanDefinition());
	builder.beanDefinition.setBeanClass(beanClass);
	return builder;
}
           

注意! 該方法的第二行通過調用

genericBeanDefinition

方法為 FeignClient 生成了一個 BeanDeifinition, 而該方法的入參是

FeignClientFactoryBean.class

檢視

genericBeanDefinition

的邏輯, 發現此處将 FeignClient 的 BeanDefinition 的 beanClass 設定成了

FeignClientFactoryBean.class

, 也就是說 FeignClient 被注冊成了一個工廠 bean(Factory Bean), 不熟悉 “Factory Bean”概念的同學可以閱讀 那些你應該掌握的 Spring 原理

這裡簡單說明下, 工廠 Bean 是一種特殊的 Bean, 對于 Bean 的消費者來說, 他邏輯上是感覺不到這個 Bean 是普通的 Bean 還是工廠 Bean, 隻是按照正常的擷取 Bean 方式去調用, 但工廠bean 最後傳回的執行個體不是工廠Bean 本身, 而是執行工廠 Bean 的 getObject 邏輯傳回的示例。

檢視一下 FeignClientFactoryBean 的

getObject

方法

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

	<T> T getTarget() {
		FeignContext context = applicationContext.getBean(FeignContext.class);
		Feign.Builder builder = feign(context);
	
		if (!StringUtils.hasText(this.url)) {
			... 省略代碼
			return (T) loadBalance(builder, context, new HardCodedTarget<>(this.type,
					this.name, url));
		}
		... 省略代碼
		return (T) targeter.target(this, builder, context, new HardCodedTarget<>(
				this.type, this.name, url));
	}
           

檢視上面兩個 return 所調用的方法, 最後發現都會統一使用到 Target.target() 方法, 該方法最終調用到 Feign.target 方法, 并進一步觸發 RefleactiveFeign.newInstance 的執行

public <T> T target(Target<T> target) {
      return 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);
	    ... 省略代碼
	  }

           

至此, 我們找到了對于 Java 原生的動态代理的使用, 整個 feign 的核心工作原理就基本清晰了, 後續就隻是 handler 如何把基于 Proxy 方法的調用轉換為 HTTP 請求發出以及翻譯回來的 HTTP 響應了, 屬于按部就班的工作, 有興趣的同學可以檢視源碼進行學習, 這裡不作贅述。

總結

Spring Cloud OpenFeign 的核心工作原理經上文探究可以非常簡單的總結為:

  1. 通過 @EnableFeignCleints 觸發 Spring 應用程式對 classpath 中 @FeignClient 修飾類的掃描
  2. 解析到 @FeignClient 修飾類後, Feign 架構通過擴充 Spring Bean Deifinition 的注冊邏輯, 最終注冊一個 FeignClientFacotoryBean 進入 Spring 容器
  3. Spring 容器在初始化其他用到 @FeignClient 接口的類時, 獲得的是 FeignClientFacotryBean 産生的一個代理對象 Proxy.
  4. 基于 java 原生的動态代理機制, 針對 Proxy 的調用, 都會被統一轉發給 Feign 架構所定義的一個 InvocationHandler , 由該 Handler 完成後續的 HTTP 轉換, 發送, 接收, 翻譯HTTP響應的工作
    Spring Cloud OpenFeign 工作原了解析參考文章背景使用方式工作原理工作原理實作細節