前言
上文中彤悔,我們搭建了兩個服務嘉抓,一個user-service,一個order-service晕窑,來模擬利用FeignCliet發(fā)起遠程調用的實現(xiàn)
本文會基于Feign源碼抑片,看看Feign到底是怎么實現(xiàn)遠程調用
源碼
@EnableFeignClients注解
上文中,我們的user-service
服務需要調用遠程的order-service
服務完成一定的業(yè)務邏輯杨赤,而基本實現(xiàn)是order-service提供一個spi的jar包給user-service依賴敞斋,并且在user-service的啟動類上添加了一個注解
這個注解就是@EnableFeignClients截汪,接下來我們就從這個注解入手,一步一步解開Feign的神秘面紗
- 類描述
/**
* Scans for interfaces that declare they are feign clients (via
* {@link org.springframework.cloud.openfeign.FeignClient} <code>@FeignClient</code>).
* Configures component scanning directives for use with
* {@link org.springframework.context.annotation.Configuration}
* <code>@Configuration</code> classes.
*
* @author Spencer Gibb
* @author Dave Syer
* @since 1.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
...
}
該注解類上的注釋大概的意思就是:
掃描那些被聲明為Feign Clients
(只要有org.springframework.cloud.openfeign.FeignClient
注解修飾的接口都是Feign Clients接口)的接口
- 重要屬性
-
basePackages
值類型:String[]
規(guī)定了掃描的基礎包位置 -
clients
值類型:Class<?>[]
規(guī)定了掃描帶有@FeignClient注解的指定類全路徑Class的集合
下面我們繼續(xù)追蹤源碼植捎,看看到底什么地方用到了這個注解
利用IDEA的查找調用鏈快捷鍵衙解,可以發(fā)現(xiàn)在.class類型的文件中只有一個文件用到了這個注解
OK,下面主要就是看這個類做了什么
FeignClientsRegistrar.class
- UML類圖
通過UML圖我們發(fā)現(xiàn)該類分別實現(xiàn)了ImportBeanDefinitionRegistrar
焰枢, ResourceLoaderAware
以及EnvironmentAware
接口
這三個接口均是spring-framework框架的spring-context模塊下的接口蚓峦,都是和spring上下文相關,具體作用下文會分析
- 重要屬性(
property
)
// 資源加載器济锄,可通過該資源加載器加載classpath下的所有文件
private ResourceLoader resourceLoader;
// 上下文環(huán)境暑椰,可通過該環(huán)境獲取當前應用配置屬性等
private Environment environment;
總結下來就是利用這兩個重要屬性,一個獲取應用配置屬性荐绝,一個可以加載classpath下的文件一汽,那么FeignClientsRegistrar持有這兩個東西之后要做什么呢?
- 重要方法(
Method
)
// 1. 初始化當前上下文環(huán)境屬性
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
// 2. 初始化資源加載器屬性
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
// 3. 最重要的一個來了低滩,注冊bean定義
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
// 注冊默認配置
registerDefaultConfiguration(metadata,registry);
// 注冊FeignClients
registerFeignClients(metadata, registry);
}
private void registerDefaultConfiguration(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
Map<String, Object> defaultAttrs = metadata
.getAnnotationAttributes(EnableFeignClients.class.getName(), true);
if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
String name;
if (metadata.hasEnclosingClass()) {
name = "default." + metadata.getEnclosingClassName();
}
else {
// name 以default拼接開頭
name = "default." + metadata.getClassName();
}
registerClientConfiguration(registry, name,
defaultAttrs.get("defaultConfiguration"));
}
}
- 這里的注冊默認配置方法角虫,讀取啟動類上面 @EnableFeignClients注解中的
defaultConfiguration
,默認name為default委造,一般情況下無需配置戳鹅。用默認的FeignAutoConfiguration即可。 上面有個比較重要的方法:注冊配置registerClientConfiguration(...)
昏兆,啟動流程一共有兩處讀取feign的配置枫虏,這是第一處。根據(jù)該方法看一下
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
Object configuration) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientSpecification.class);
builder.addConstructorArgValue(name);
builder.addConstructorArgValue(configuration);
registry.registerBeanDefinition(
name + "." + FeignClientSpecification.class.getSimpleName(),
builder.getBeanDefinition());
}
上面將bean配置類包裝成FeignClientSpecification
爬虱,注入到容器隶债。該對象非常重要,包含F(xiàn)eignClient需要的重試策略
跑筝,超時策略
死讹,日志
等配置,如果某個FeignClient服務沒有設置獨立的配置類曲梗,則讀取默認的配置赞警,可以將這里注冊的bean理解為整個應用中所有feign的默認配置
- 題外話:
由于FeignClientsRegistrar
實現(xiàn)了ImportBeanDefinitionRegistrar
接口,這里簡單提下這個接口的作用
我們知道在spring框架中虏两,我們如果想注冊一個bean的話主要由兩種方式:自動注冊/手動注冊
類上增加@Component愧旦,@Service,@Controller等注解定罢,由Spring
自動
幫我們注冊
成bean配置文件(.xml中增加<bean>...</bean>標簽)或配置類(@Configuration)中增加@Bean注解的類笤虫,也會由Spring幫我們
自動注冊
成bean通過實現(xiàn)
ImportBeanDefinitionRegistrar
接口,并實現(xiàn)其registerBeanDefinitions方法,手動注冊
bean
知道了ImportBeanDefinitionRegistrar
接口的作用琼蚯,下面就來看下FeignClientsRegistrar
類是何時被加載實例化的
通過IDEA工具搜索引用鏈酬凳,發(fā)現(xiàn)該類是在注解@EnableFeignClients上被import進來的,文章開始的圖片中有
@Import(FeignClientsRegistrar.class)
這里提下@Import注解的作用
/**
* Indicates one or more {@link Configuration @Configuration} classes to import.
*
* <p>Provides functionality equivalent to the {@code <import/>} element in Spring XML.
* Allows for importing {@code @Configuration} classes, {@link ImportSelector} and
* {@link ImportBeanDefinitionRegistrar} implementations, as well as regular component
* classes (as of 4.2; analogous to {@link AnnotationConfigApplicationContext#register}).
*
* <p>{@code @Bean} definitions declared in imported {@code @Configuration} classes should be
* accessed by using {@link org.springframework.beans.factory.annotation.Autowired @Autowired}
* injection. Either the bean itself can be autowired, or the configuration class instance
* declaring the bean can be autowired. The latter approach allows for explicit, IDE-friendly
* navigation between {@code @Configuration} class methods.
*
* <p>May be declared at the class level or as a meta-annotation.
*
* <p>If XML or other non-{@code @Configuration} bean definition resources need to be
* imported, use the {@link ImportResource @ImportResource} annotation instead.
*
* @author Chris Beams
* @author Juergen Hoeller
* @since 3.0
* @see Configuration
* @see ImportSelector
* @see ImportResource
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {
/**
* {@link Configuration}, {@link ImportSelector}, {@link ImportBeanDefinitionRegistrar}
* or regular component classes to import.
*/
Class<?>[] value();
}
該注解僅有一個屬性value遭庶,使用該注解表明導入一個或者多個@Configuration類宁仔,其作用和.xml文件中的<import>等效,其允許導入@Configuration類罚拟,ImportSelector接口/ImportBeanDefinitionRegistrar接口的實現(xiàn)台诗,也同樣可以導入一個普通的組件類
注意完箩,如果是XML或非@Configuration的bean定義資源需要被導入的話赐俗,需要使用@ImportResource注解代替
這里我們導入的FeignClientsRegistrar類正是一個ImportBeanDefinitionRegistrar接口的實現(xiàn)
FeignClientsRegistrar重寫了該接口的registerBeanDefinitions
方法,該方法有兩個參數(shù)注解元數(shù)據(jù)metadata
和bean定義注冊表registry
該方法會由spring負責調用弊知,繼而注冊所有標注為@FeignClient注解的bean定義
registerFeignClients(...)
下面看registerBeanDefinitions方法中的第二個方法阻逮,在該方法中完成了所有@FeignClient注解接口的掃描工作,以及注冊到spring中秩彤,注意這里注冊bean的類型為FeignClientFactoryBean
叔扼,下面細說
public void registerFeignClients(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
// 獲取ClassPath掃描器
ClassPathScanningCandidateComponentProvider scanner = getScanner();
// 為掃描器設置資源加載器
scanner.setResourceLoader(this.resourceLoader);
Set<String> basePackages;
// 1. 從@EnableFeignClients注解中獲取到配置的各個屬性值
Map<String, Object> attrs = metadata
.getAnnotationAttributes(EnableFeignClients.class.getName());
// 2. 注解類型過濾器,只過濾@FeignClient
AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
FeignClient.class);
// 3. 從1. 中的屬性值中獲取clients屬性的值
final Class<?>[] clients = attrs == null ? null
: (Class<?>[]) attrs.get("clients");
if (clients == null || clients.length == 0) {
// 掃描器設置過濾器且獲取需要掃描的基礎包集合
scanner.addIncludeFilter(annotationTypeFilter);
basePackages = getBasePackages(metadata);
}
else {
// clients屬性值不為null漫雷,則將其clazz路徑轉為包路徑
final Set<String> clientClasses = new HashSet<>();
basePackages = new HashSet<>();
for (Class<?> clazz : clients) {
basePackages.add(ClassUtils.getPackageName(clazz));
clientClasses.add(clazz.getCanonicalName());
}
AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
@Override
protected boolean match(ClassMetadata metadata) {
String cleaned = metadata.getClassName().replaceAll("\\$", ".");
return clientClasses.contains(cleaned);
}
};
scanner.addIncludeFilter(
new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
}
// 3. 掃描基礎包瓜富,且滿足過濾條件下的接口封裝成BeanDefinition
for (String basePackage : basePackages) {
Set<BeanDefinition> candidateComponents = scanner
.findCandidateComponents(basePackage);
// 遍歷掃描到的bean定義
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition) {
// 并校驗掃描到的bean定義類是一個接口
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());
String name = getClientName(attributes);
// 可以看到這里也注冊了一個FeignClient的配置bean
registerClientConfiguration(registry, name,
attributes.get("configuration"));
// 注冊bean定義到spring中
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
}
總結一下該方法,就是掃描@EnableFeignClients注解上指定的basePackage或clients值降盹,獲取所有@FeignClient注解標識的接口,然后將這些接口一一調用以下兩個重要方法
完成注冊configuration配置bean
和注冊FeignClient bean
// ...省略部分代碼
Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(FeignClient.class.getCanonicalName());
// 獲取@FeignClient注解中指定的name
String name = getClientName(attributes);
// 1. 獲取@FeignClient注解中指定的configuration,并以FeignClientSpecification類注冊bean
registerClientConfiguration(registry, name, attributes.get("configuration"));
// 2. 注冊FeignClient bean垄提,以FeignClientFactoryBean類注冊
registerFeignClient(registry, annotationMetadata, attributes);
- 可以理解為每個單獨的FeignClient接口都會注冊一個自己的configuration bean定義到spring的beanDefinitionMap中枫慷,key是@FeignClient注解上指定的serviceId/name/value值,value則是一個類型為
FeignClientSpecification
的BeanDefinition
- 中完成了每個FeignClient接口的client bean的注冊涡戳,beanDefinitionMap中的key是@FeignClient注解的每個接口的全限定名结蟋,value則是類型是
FeignClientFactoryBean
的BeanDefinition
斷點位置相當重要
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
這里是利用了spring的代理工廠來生成代理類,即這里將所有的 feignClient的描述信息BeanDefinition
設定為 FeignClientFactoryBean
類型渔彰,該類繼承自FactoryBean嵌屎,因此這是一個代理類,F(xiàn)actoryBean是一個工廠bean恍涂,用作創(chuàng)建代理bean编整,所以得出結論,feign將所有的feignClient
bean定義的類型包裝成 FeignClientFactoryBean
最終其實就是存入了BeanFactory的beanDefinitionMap中
那么代理類什么時候會觸發(fā)生成呢乳丰? 在spring刷新容器時
掌测,會根據(jù)beanDefinition去實例化bean,如果beanDefinition的beanClass類型為代理bean,則會調用其T getObject() throws Exception;
方法生成代理bean汞斧,而我們實際利用注入進來的FeignClient接口就是這些一個個代理類
總結
這里有坑...
這里有一個需要注意的點夜郁,也是開發(fā)中會遇到的一個啟動報錯點
如果我們同時定義了兩個不同名稱的接口
(同一個包下/或依賴方指定全部掃描我們提供的@FeignClient
),且這兩個@FeignClient
接口注解的value/name/serviceId
值一樣的話粘勒,依賴方拿到我們的提供的spi依賴竞端,啟動類上@EnableFeignClients
注解掃描能同時掃描到這兩個接口,就會啟動報錯
原因就是Feign會為每個@FeignClient注解標識的接口都注冊一個以serviceId/name/value為key庙睡,F(xiàn)eignClientSpecification類型的bean定義為value去spring注冊bean定義事富,又默認不允許覆蓋bean定義,所以報錯
官方提示給出的解決方法要么改個@FeignClient注解的serviceId乘陪,name统台,value屬性值,要么就開啟spring允許bean定義覆寫
spring.main.allow-bean-definition-overriding=true
至此我們知道利用在springboot的啟動類上添加的@EnableFeignClients注解啡邑,該注解中import進來了一個手動注冊bean的FeignClientsRegistrar注冊器
贱勃,該注冊器會由spring加載其registerBeanDefinitions方法
,由此來掃描所有@EnableFeignClients注解定義的basePackages包路徑下的所有標注為@FeignClient注解的接口谤逼,并將其注冊到spring的bean定義Map中贵扰,并實例化bean
下一篇博文中,我會分析為什么我們在調用(@Resource)這些由@FeignClient注解的bean的方法時會發(fā)起遠程調用