Spring Cloud 整合 Feign 的原理
前言
在 上篇 介紹了 Feign 的核心實現原理,在文末也提到了會再介紹其和 Spring Cloud 的整合原理,Spring 具有很強的擴充套件性,會把一些常用的解決方案通過 starter 的方式開放給開發者使用,在引入官方提供的 starter 後通常只需要新增一些註解即可使用相關功能(通常是 @EnableXXX)。下面就一起來看看 Spring Cloud 到底是如何整合 Feign 的。
整合原理淺析
在 Spring 中一切都是圍繞 Bean 來展開的工作,而所有的 Bean 都是基於 BeanDefinition 來生成的,可以說 BeanDefinition 是整個 Spring 帝國的基石,這個整合的關鍵也就是要如何生成 Feign 對應的 BeanDefinition。
要分析其整合原理,我們首先要從哪裡入手呢?如果你看過 上篇 的話,在介紹結合 Spring Cloud 使用方式的例子時,第二步就是要在專案的 XXXApplication 上加新增 @EnableFeignClients 註解,我們可以從這裡作為切入點,一步步深入分析其實現原理(通常相當一部分的 starter 一般都是在啟動類中添加了開啟相關功能的註解)。
進入 @EnableFeignClients 註解中,其原始碼如下:
從註解的原始碼可以發現,該註解除了定義幾個引數(basePackages、defaultConfiguration、clients 等)外,還通過 @Import 引入了 FeignClientsRegistrar 類,一般 @Import 註解有如下功能(具體功能可見
- 宣告一個 Bean
- 匯入 @Configuration 註解的配置類
- 匯入 ImportSelector 的實現類
- 匯入 ImportBeanDefinitionRegistrar 的實現類(這裡使用這個功能)
到這裡不難看出,整合實現的主要流程就在 FeignClientsRegistrar 類中了,讓我們繼續深入到類 FeignClientsRegistrar 的原始碼,
通過原始碼可知 FeignClientsRegistrar 實現 ImportBeanDefinitionRegistrar 介面,該介面從名字也不難看出其主要功能就是將所需要初始化的 BeanDefinition 注入到容器中,介面定義兩個方法功能都是用來注入給定的 BeanDefinition 的,一個可自定義 beanName(通過實現 BeanNameGenerator 介面自定義生成 beanName 的邏輯),另一個使用預設的規則生成 beanName(類名首字母小寫格式)。介面原始碼如下所示:
對 Spring 有一些瞭解的朋友們都知道,Spring 會在容器啟動的過程中根據 BeanDefinition 的屬性資訊完成對類的初始化,並注入到容器中。所以這裡 FeignClientsRegistrar 的終極目標就是將生成的代理類注入到 Spring 容器中。
雖然 FeignClientsRegistrar 這個類的原始碼看起來比較多,但是從其終結目標來看,我們主要是看如何生成 BeanDefinition 的,通過原始碼可以發現其實現了 ImportBeanDefinitionRegistrar 介面,並且重寫了 registerBeanDefinitions(AnnotationMetadata, BeanDefinitionRegistry) 方法,在這個方法裡完成了一些 BeanDefinition 的生成和註冊工作。原始碼如下:
整個過程主要分為如下兩個步驟:
- 給 @EnableFeignClients 的全域性預設配置(註解的 defaultConfiguration 屬性)建立 BeanDefinition 物件並注入到容器中(對應上圖中的第 ① 步)
- 給標有了 @FeignClient 的類建立 BeanDefinition 物件並注入到容器中(對應上圖中的第 ② 步)
下面分別深入方法原始碼實現來看其具體實現原理,首先來看看第一步的方法 registerDefaultConfiguration(AnnotationMetadata, BeanDefinitionRegistry),原始碼如下:
可以看到這裡只是獲取一下註解 @EnableFeignClients 的預設配置屬性 defaultConfiguration 的值,最終的功能實現交給了 registerClientConfiguration(BeanDefinitionRegistry, Object, Object) 方法來完成,繼續跟進深入該方法,其原始碼如下:
可以看到,全域性預設配置的 BeanClazz 都是 FeignClientSpecification,然後這裡將全域性預設配置 configuration 設定為 BeanDefinition 構造器的輸入引數,然後當呼叫構造器例項化時將這個引數傳進去。到這裡就已經把 @EnableFeignClients 的全域性預設配置(註解的 defaultConfiguration 屬性)創建出 BeanDefinition 物件並注入到容器中了,第一步到此完成,整體還是比較簡單的。
下面再來看看第二步 給標有了 @FeignClient 的類建立 BeanDefinition 物件並注入到容器中 是如何實現的。深入第二步的方法 registerFeignClients(AnnotationMetadata, BeanDefinitionRegistry) 實現中,由於方法實現程式碼較多,使用截圖會比較分散,所以用貼出原始碼並在相關位置新增必要註釋的方式進行:
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
// 最終獲取到有 @FeignClient 註解類的集合
LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();
// 獲取 @EnableFeignClients 註解的屬性 map
Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
// 獲取 @EnableFeignClients 註解的 clients 屬性
final Class<?>[] clients = attrs == null ? null : (Class<?>[]) attrs.get("clients");
if (clients == null || clients.length == 0) {
// 如果 @EnableFeignClients 註解未指定 clients 屬性則掃描新增(掃描過濾條件為:標註有 @FeignClient 的類)
ClassPathScanningCandidateComponentProvider scanner = getScanner();
scanner.setResourceLoader(this.resourceLoader);
scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
Set<String> basePackages = getBasePackages(metadata);
for (String basePackage : basePackages) {
candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
}
}
else {
// 如果 @EnableFeignClients 註解已指定 clients 屬性,則直接新增,不再掃描(從這裡可以看出,為了加快容器啟動速度,建議都指定 clients 屬性)
for (Class<?> clazz : clients) {
candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
}
}
// 遍歷最終獲取到的 @FeignClient 註解類的集合
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");
// 獲取 @FeignClient 註解的屬性值
Map<String, Object> attributes = annotationMetadata
.getAnnotationAttributes(FeignClient.class.getCanonicalName());
// 獲取 clientName 的值,也就是在構造器的引數值(具體獲取邏輯可以參見 getClientName(Map<String, Object>) 方法
String name = getClientName(attributes);
// 同上文第一步最後呼叫的方法,注入 @FeignClient 註解的配置物件到容器中
registerClientConfiguration(registry, name, attributes.get("configuration"));
// 注入 @FeignClient 物件,該物件可以在其它類中通過 @Autowired 直接引入(e.g. XXXService)
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
通過原始碼可以看到最後是通過方法 registerFeignClient(BeanDefinitionRegistry, AnnotationMetadata, Map<String, Object>) 注入的 @FeignClient 物件,繼續深入該方法,原始碼如下:
方法實現比較長,最終目標是構造出 BeanDefinition 物件,然後通過 BeanDefinitionReaderUtils.registerBeanDefinition(BeanDefinitionHolder, BeanDefinitionRegistry) 注入到容器中。其中關鍵的一步是從 @FeignClient 註解中獲取資訊並設定到 BeanDefinitionBuilder 中,BeanDefinitionBuilder 中註冊的類是 FeignClientFactoryBean,這個類的功能正如它的名字一樣是用來創建出 FeignClient 的 Bean 的,然後 Spring 會根據 FeignClientFactoryBean 生成物件並注入到容器中。
需要明確的一點是,實際上這裡最終注入到容器當中的是 FeignClientFactoryBean 這個類,Spring 會在類初始化的時候會根據這個類來生成例項物件,就是呼叫 FeignClientFactoryBean.getObject() 方法,這個生成的物件就是我們實際使用的代理物件。下面再進入到類 FeignClientFactoryBean 的 getObject() 這個⽅法,原始碼如下:
可以看到這個方法是直接呼叫的類中的另一個方法 getTarget() 的,在繼續跟進該方法,由於該方法實現程式碼較多,使用截圖會比較分散,所以用貼出原始碼並在相關位置新增必要註釋的方式進行:
/**
* @param <T> the target type of the Feign client
* @return a {@link Feign} client created with the specified data and the context
* information
*/
<T> T getTarget() {
// 從 Spring 容器中獲取 FeignContext Bean
FeignContext context = beanFactory != null ? beanFactory.getBean(FeignContext.class)
: applicationContext.getBean(FeignContext.class);
// 根據獲取到的 FeignContext 構建出 Feign.Builder
Feign.Builder builder = feign(context);
// 註解 @FeignClient 未指定 url 屬性
if (!StringUtils.hasText(url)) {
// url 屬性是固定訪問某一個例項地址,如果未指定協議則拼接 http 請求協議
if (!name.startsWith("http")) {
url = "http://" + name;
}
else {
url = name;
}
// 格式化 url
url += cleanPath();
// 生成代理和我們之前的代理一樣,註解 @FeignClient 未指定 url 屬性則返回一個帶有負載均衡功能的客戶端物件
return (T) loadBalance(builder, context, new HardCodedTarget<>(type, name, url));
}
// 註解 @FeignClient 已指定 url 屬性
if (StringUtils.hasText(url) && !url.startsWith("http")) {
url = "http://" + url;
}
String url = this.url + cleanPath();
// 獲取一個 client
Client client = getOptional(context, Client.class);
if (client != null) {
if (client instanceof FeignBlockingLoadBalancerClient) {
// not load balancing because we have a url,
// but Spring Cloud LoadBalancer is on the classpath, so unwrap
// 這裡沒有負載是因為我們有指定了 url
client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
}
builder.client(client);
}
// 生成代理和我們之前的代理一樣,最後被注入到 Spring 容器中
Targeter targeter = get(context, Targeter.class);
return (T) targeter.target(this, builder, context, new HardCodedTarget<>(type, name, url));
}
通過原始碼得知 FeignClientFactoryBean 繼承了 FactoryBean,其方法 FactoryBean.getObject 返回的就是 Feign 的代理物件,最後這個代理物件被注入到 Spring 容器中,我們就通過 @Autowired 可以直接注入使用了。同時還可以發現上面的程式碼分支最終都會走到如下程式碼:
Targeter targeter = get(context, Targeter.class);
return targeter.target(this, builder, context, target);
點進去深入 targeter.target 的原始碼,可以看到實際上這裡建立的就是一個代理物件,也就是說在容器啟動的時候,會為每個 @FeignClient 建立了一個代理物件。至此,Spring Cloud 和 Feign 整合原理的核心實現介紹完畢。
總結
本文主要介紹了 Spring Cloud 整合 Feign 的原理。通過上文介紹,你已經知道 Srpring 會我們的標註的 @FeignClient 的介面建立了一個代理物件,那麼有了這個代理物件我們就可以做增強處理(e.g. 前置增強、後置增強),那麼你知道是如何實現的嗎?感興趣的朋友可以再翻翻原始碼尋找答案(溫馨提示:增強邏輯在 InvocationHandler 中)。還有 Feign 與 Ribbon 和 Hystrix 等元件的協作,感興趣的朋友可以自行下載原始碼學習瞭解。