1. 程式人生 > 實用技巧 >博思軟體實訓總結(五)

博思軟體實訓總結(五)

博思軟體實訓總結(五)

基於微服務架構的系統,各模組之間的呼叫是通過openfeign進行遠端呼叫的,下面簡單瞭解一下openfeign的原理

背景

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

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

注意該介面的入參是 json 格式, 框架會自動幫我們反序列化到對應的 DemoQryRequest 型別的入參物件裡。

返回值 ApiBaseMessage 也會被框架自動序列化為 json 格式

介面使用方

在介面的使用者一端, 首先需要引入 SpringFeign 依賴(為簡化篇幅, 只展示 build.gradle 中新增 Feign 的依賴, 沒有展示其他的 spring cloud 依賴新增)

    implementation('org.springframework.cloud:spring-cloud-starter-openfeign')
1
@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);
}
12345678

再直接利用 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);
    }
}
123456789101112131415

通過上面的例子可以看到, 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);
	}
12345678

這裡值得進一步關注的是, 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;
}
1234567891011121314151617

注意! 該方法的第二行通過呼叫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));
	}
1234567891011121314151617

檢視上面兩個 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);
	    ... 省略程式碼
	  }

1234567891011

至此, 我們找到了對於 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響應的工作