前言
在 上篇 介紹了 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 註解有如下功能(具體功能可見 官方 Java Doc):
- 聲明一個 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 等組件的協作,感興趣的朋友可以自行下載源碼學習瞭解。