背景
OpenFeign 是 Spring Cloud 家族的一個成員, 它最核心的作用是為 HTTP 形式的 Rest API 提供了非常簡潔高效的 RPC 調用方式。
如果說 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 依賴添加)
implementation('org.springframework.cloud:spring-cloud-starter-openfeign')
@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 調用前所進行的操作:
- 添加了 Spring Cloud OpenFeign 的依賴
- 在 SpringBoot 啟動類上添加了注解
@EnableFeignCleints
- 按照 Feign 的規則定義接口
DemoService
, 添加@FeignClient
注解 - 在需要使用 Feign 接口 DemoService 的地方, 直接利用@Autowire 進行注入
- 使用接口完成對服務端的調用
可以根據上面使用 Feign 的步驟大致猜測出整體的工作流程:
- SpringBoot 應用啟動時, 由針對
@EnableFeignClient
這一注解的處理邏輯觸發程序掃描 classPath中所有被@FeignClient
注解的類, 這里以DemoService
為例, 將這些類解析為 BeanDefinition 注冊到 Spring 容器中 - Sping 容器在為某些用的 Feign 接口的 Bean 注入
DemoService
時, Spring 會嘗試從容器中查找 DemoService 的實現類 - 由於我們從來沒有編寫過
DemoService
的實現類, 上面步驟獲取到的 DemoService 的實現類必然是 feign 框架通過擴展 spring 的 Bean 處理邏輯, 為DemoService
創建一個動態接口代理對象, 這里我們將其稱為DemoServiceProxy
注冊到spring 容器中。 - Spring 最終在使用到
DemoService
的 Bean 中注入了DemoServiceProxy
這一實例。 - 當業務請求真實發生時, 對於
DemoService
的調用被統一轉發到了由 Feign 框架實現的InvocationHandler
中,InvocationHandler
負責將接口中的入參轉換為 HTTP 的形式, 發到服務端, 最后再解析 HTTP 響應, 將結果轉換為 Java 對象, 予以返回。
上面整個流程可以進一步簡化理解為:
- 我們定義的接口
DemoService
由於添加了注解@FeignClient
, 最終產生了一個虛假的實現類代理 - 使用這個接口的地方, 最終拿到的都是一個假的代理實現類
DemoServiceProxy
- 所有發生在
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),
這里簡單說明下, 工廠 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 的核心工作原理經上文探究可以非常簡單的總結為:
- 通過 @EnableFeignCleints 觸發 Spring 應用程序對 classpath 中 @FeignClient 修飾類的掃描
- 解析到 @FeignClient 修飾類后, Feign 框架通過擴展 Spring Bean Deifinition 的注冊邏輯, 最終注冊一個 FeignClientFacotoryBean 進入 Spring 容器
- Spring 容器在初始化其他用到 @FeignClient 接口的類時, 獲得的是 FeignClientFacotryBean 產生的一個代理對象 Proxy.
- 基於 java 原生的動態代理機制, 針對 Proxy 的調用, 都會被統一轉發給 Feign 框架所定義的一個 InvocationHandler , 由該 Handler 完成后續的 HTTP 轉換, 發送, 接收, 翻譯HTTP響應的工作
轉:https://blog.csdn.net/lengxiao1993/article/details/103511695/