【SpringCloud原理】萬字剖析OpenFeign之FeignClient動態代理生成源碼
- 2022 年 5 月 24 日
- 筆記

前面時候我發佈兩篇關於nacos源碼的文章,一篇是聊一聊nacos是如何進行服務註冊的,另一篇是一文帶你看懂nacos是如何整合springcloud — 註冊中心篇。今天就繼續接着剖析SpringCloud中OpenFeign組件的源碼,來聊一聊OpenFeign是如何工作的。
一、@EnableFeignClinets作用源碼剖析
我們都知道,要使用feign,必須要使用@EnableFeignClinets來激活,這個註解其實就是整個feign的入口,接下來我們着重分析一下這個註解幹了什麼事。
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import(FeignClientsRegistrar.class) public @interface EnableFeignClients { }
這個註解通過@Import註解導入一個配置類FeignClientsRegistrar.class,FeignClientsRegistrar實現了ImportBeanDefinitionRegistrar接口,所以Spring Boot在啟動的時候,會去調用FeignClientsRegistrar類中的registerBeanDefinitions來動態往spring容器中注入bean。如果有不懂小夥伴可以看一下我以前寫過的一篇文章 看Spring源碼不得不會的@Enable模塊驅動實現原理講解,這裡詳細講解了@Import註解的作用。
接下來看一下registerBeanDefinitions的實現
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry)
//這個方式是注入一些配置,就是對EnableFeignClients註解屬性的解析
registerDefaultConfiguration(metadata, registry);
//這個方法是掃秒加了@FeignClient註解
registerFeignClients(metadata, registry);
}
這裡我們着重分析registerFeignClients,看一看是如何掃描@FeignClient註解的,然後掃描到之後又做了什麼。
public void registerFeignClients(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
ClassPathScanningCandidateComponentProvider scanner = getScanner();
scanner.setResourceLoader(this.resourceLoader);
Set<String> basePackages;
Map<String, Object> attrs = metadata
.getAnnotationAttributes(EnableFeignClients.class.getName());
AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
FeignClient.class);
final Class<?>[] clients = attrs == null ? null
: (Class<?>[]) attrs.get("clients");
if (clients == null || clients.length == 0) {
scanner.addIncludeFilter(annotationTypeFilter);
basePackages = getBasePackages(metadata);
}
else {
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)));
}
for (String basePackage : basePackages) {
Set<BeanDefinition> candidateComponents = scanner
.findCandidateComponents(basePackage);
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");
Map<String, Object> attributes = annotationMetadata
.getAnnotationAttributes(
FeignClient.class.getCanonicalName());
String name = getClientName(attributes);
registerClientConfiguration(registry, name,
attributes.get("configuration"));
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
}
這段代碼我分析一下,先獲取到了一個ClassPathScanningCandidateComponentProvider這個對象,這個對象是按照一定的規則來掃描指定目錄下的類的,符合這個規則的每個類,會生成一個BeanDefinition,不知道BeanDefinition的小夥伴可以看我之前寫的關於bean生命周期的文章 Spring bean到底是如何創建的?(上)和 Spring bean到底是如何創建的?(下),裏面有過對BeanDefinition的描述。
獲取到ClassPathScanningCandidateComponentProvider對象,配置這個對象,指定這個對象需要掃描出來標有@FeignClient註解的類;隨後解析EnableFeignClients註解,獲取內部的屬性,獲取到指定的需要掃描包路徑下,如果沒有指定的,那麼就默認是當前註解所在類的所在目錄及子目錄。
然後就遍歷每個目錄,找到每個標有@FeignClient註解的類,對每個類就生成一個BeanDefinition,可以把BeanDefinition看成對每個標有@FeignClient註解的類信息的封裝。
拿到一堆BeanDefinition之後,會遍歷BeanDefinition,然後調用registerClientConfiguration和registerFeignClient方法。
接下來我分別剖析一下這兩個方法的作用
registerClientConfiguration:
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());
}
這裡的作用就是拿出你再@FeignClient指定的配置類,也就是configuration屬性,然後構建一個bean class為FeignClientSpecification,傳入配置。這個類的最主要作用就是將每個Feign的客戶端的配置類封裝成一個FeignClientSpecification的BeanDefinition,註冊到spring容器中。記住這個FeignClientSpecification,後面會有用。
registerFeignClient:
private void registerFeignClient(BeanDefinitionRegistry registry,
AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();
BeanDefinitionBuilder definition = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientFactoryBean.class);
validate(attributes);
definition.addPropertyValue("url", getUrl(attributes));
definition.addPropertyValue("path", getPath(attributes));
String name = getName(attributes);
definition.addPropertyValue("name", name);
String contextId = getContextId(attributes);
definition.addPropertyValue("contextId", contextId);
definition.addPropertyValue("type", className);
definition.addPropertyValue("decode404", attributes.get("decode404"));
definition.addPropertyValue("fallback", attributes.get("fallback"));
definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
String alias = contextId + "FeignClient";
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
boolean primary = (Boolean) attributes.get("primary"); // has a default, won't be
// null
beanDefinition.setPrimary(primary);
String qualifier = getQualifier(attributes);
if (StringUtils.hasText(qualifier)) {
alias = qualifier;
}
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
new String[] { alias });
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
registerFeignClient這個方法很重要,我來說一下大概做了哪些事。重新構造了一個BeanDefinition,這個BeanDefinition的指定的class類型是FeignClientFactoryBean,這個類實現了FactoryBean接口,對spring有一定了解的小夥伴應該知道,spring在生成bean的時候,判斷BeanDefinition中bean的class如果是FactoryBean的實現的話,會調用這個實現類的getObject來獲取對象,這裡我就不展開講了,不了解的同學可以記住這個結論。
到這一步,@EnableFeignClinets的作用就說完了。這個類的主要作用是掃描指定(不指定就默認路徑下的)所有加了@FeignClient註解的類,然後每個類都會生成一個BeanDefinition,隨後遍歷每個BeanDefinition,然後取出每個@FeignClient註解的屬性,構造新的BeanDefinition,傳入FeignClientFactoryBean的class,隨後注入到spring容器中;同時有配置類的也會將配置類構件出一個bean class為FeignClientSpecification的BeanDefinition注入到spring容器中。
為了便於理解,我這裡畫個圖來總結一下這個註解幹了什麼事。

二、Feign客戶端接口動態代理的生成源碼剖析
(1)FeignAutoConfiguration源碼剖析
FeignAutoConfiguration是feign在整個springcloud的配置類,我拎出這裏面比較核心的代碼。
@Autowired(required = false)
private List<FeignClientSpecification> configurations = new ArrayList<>();
@Bean
public FeignContext feignContext() {
FeignContext context = new FeignContext();
context.setConfigurations(this.configurations);
return context;
}
注入了一堆FeignClientSpecification,FeignClientSpecification這玩意就是上文提到的調用registerClientConfiguration的時候注入到spring容器中的,一個Feign客戶端的配置一個FeignClientSpecification,所以是個集合,然後封裝到FeignContext中,最後將FeignContext注入到spring容器中。
FeignContext也是很重要的一個東西,我們來分析一下它的源碼
public class FeignContext extends NamedContextFactory<FeignClientSpecification> {
public FeignContext() {
super(FeignClientsConfiguration.class, "feign", "feign.client.name");
}
}
FeignContext繼承了NamedContextFactory,構造的時候,傳入了FeignClientsConfiguration,這個玩意也很重要,別急,我們慢慢來分析它們的作用。
(2)NamedContextFactory源碼剖析
我先來說結論,NamedContextFactory的作用是用來進行配置隔離的,ribbon和feign的配置隔離都依賴這個抽象類。
何為配置隔離,因為每個Feign客戶端都有可能有自己的配置,從@FeignClient註解的屬性configuration可以看出,所以寫了這個類,用來隔離每個客戶端的配置,這就是為什麼在構造FeignContext傳入一堆FeignClientSpecification的原因,這裡封裝了每個客戶端的配置類。
那是怎麼實現的呢,我拎出來一部分核心的源碼,不重要的我就忽略了。
public abstract class NamedContextFactory<C extends NamedContextFactory.Specification>
implements DisposableBean, ApplicationContextAware {
private final String propertySourceName;
private final String propertyName;
private Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>();
private Map<String, C> configurations = new ConcurrentHashMap<>();
//父類 ApplicationContext ,也就是springboot所使用的ApplicationContext
private ApplicationContext parent;
// 這個是默認的額配置類
private Class<?> defaultConfigType;
public NamedContextFactory(Class<?> defaultConfigType, String propertySourceName,
String propertyName) {
this.defaultConfigType = defaultConfigType;
this.propertySourceName = propertySourceName;
this.propertyName = propertyName;
}
@Override
public void setApplicationContext(ApplicationContext parent) throws BeansException {
this.parent = parent;
}
public void setConfigurations(List<C> configurations) {
for (C client : configurations) {
this.configurations.put(client.getName(), client);
}
}
public Set<String> getContextNames() {
return new HashSet<>(this.contexts.keySet());
}
protected AnnotationConfigApplicationContext getContext(String name) {
if (!this.contexts.containsKey(name)) {
synchronized (this.contexts) {
if (!this.contexts.containsKey(name)) {
this.contexts.put(name, createContext(name));
}
}
}
return this.contexts.get(name);
}
protected AnnotationConfigApplicationContext createContext(String name) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
if (this.configurations.containsKey(name)) {
for (Class<?> configuration : this.configurations.get(name)
.getConfiguration()) {
context.register(configuration);
}
}
for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
if (entry.getKey().startsWith("default.")) {
for (Class<?> configuration : entry.getValue().getConfiguration()) {
context.register(configuration);
}
}
}
context.register(PropertyPlaceholderAutoConfiguration.class,
this.defaultConfigType);
context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(
this.propertySourceName,
Collections.<String, Object>singletonMap(this.propertyName, name)));
if (this.parent != null) {
// Uses Environment from parent as well as beans
context.setParent(this.parent);
// jdk11 issue
// //github.com/spring-cloud/spring-cloud-netflix/issues/3101
context.setClassLoader(this.parent.getClassLoader());
}
context.setDisplayName(generateDisplayName(name));
context.refresh();
return context;
}
/**
* Specification with name and configuration.
*/
public interface Specification {
String getName();
Class<?>[] getConfiguration();
}
}
分析一下每個成員變量的作用:
contexts:一個客戶端一個對應的AnnotationConfigApplicationContext
configurations:一個客戶端一個配置類的封裝,對應到Feign的就是FeignClientSpecification
parent:springboot真正啟動的就是這個ApplicationContext
defaultConfigType:默認的配置類,對應Feign就是構造FeignContext是傳入的FeignClientsConfiguration
分析一下核心的方法:
getContext:這個方法很簡單,就是根據客戶端名稱從contexts獲取對應的AnnotationConfigApplicationContext,獲取不到就去創建一個,然後放入contexts
createContext:就是直接new了一個AnnotationConfigApplicationContext對象,然後按照按照配置的優先級順序,一步步放入配置類,最後放入parent容器,也就是說每個客戶端對應的容器,都有一個共同的父容器,同時如果每個客戶端對應的容器獲取不到的配置,都會再次從父容器中獲取。這個結論還是很重要的。
其實所謂的配置隔離就是為每個客戶端構建一個AnnotationConfigApplicationContext,然後基於這個ApplicationContext來解析配置類,這樣就實現了配置隔離。
不知道大家有么有遇到過這個坑,就是在spring cloud環境中,監聽類似ContextRefreshedEvent這種事件的時候,這個事件會無緣無故地觸發很多次,其實就是這個原因就在這,因為spring的事件是有傳播機制的,每個客戶端對應的容器都要進行refresh,refresh完就會發這個事件,然後這個事件就會傳給parent容器,也就是springboot啟動的容器,就會再次觸發,所以如果客戶端很多,那麼就會觸發很多次。解決辦法就是進行唯一性校驗,只能啟動一次就行了。
(3)FeignClientsConfiguration源碼剖析
說完NamedContextFactory,接下來我們說一下FeignClientsConfiguration的作用。
這是一個默認的配置類,裏面配置了很多bean,這些bean都是生成Feign客戶端動態代理的需要的,我說幾個重要的。
@Bean
@ConditionalOnMissingBean
public Contract feignContract(ConversionService feignConversionService) {
return new SpringMvcContract(this.parameterProcessors, feignConversionService);
}
這個的主要作用是用來解析@FeignClient接口中每個方法使用的springmvc的註解的,這也就是為什麼FeignClient可以識別springmvc註解的原因。
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
public Feign.Builder feignBuilder(Retryer retryer) {
return Feign.builder().retryer(retryer);
}
用來構建動態代理的類,通過這個類的target方法,就能生成Feign動態代理
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class })
protected static class HystrixFeignConfiguration {
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "feign.hystrix.enabled")
public Feign.Builder feignHystrixBuilder() {
return HystrixFeign.builder();
}
}
這個是FeignClientsConfiguration的內部類,是用來整合hystrix的,@ConditionalOnProperty(name = "feign.hystrix.enabled"),當在配置文件配置了feign.hystrix.enabled=true的時候,就開啟了hystrix整合了Feign,然後調用Feign的接口就有了限流、降級的功能。其實hystrix整合Feign很簡單,就是在構造動態代理的時候加了點東西而已。其實不光是hystrix,spring cloud alibaba中的sentinel在整合Feign的適合也是按照這個套路來的。
(4)構建動態代理的過程源碼剖析
說完了前置的內容,接下來我們就來看一看動態代理是如何生成的。從上面我們已經知道了,@EnableFeignClinets會掃描出每個加了@FeignClient註解的接口,然後生成對應的BeanDefinition,最後重新生成一個bean class為FeignClientFactoryBean的BeanDefinition,註冊到spring容器。
接下來就會根據BeanDefinition來生成feign客戶端的代理對象了。上面我提到,是通過FeignClientFactoryBean的getObject方法來獲取到代理對象,接下來,我們就來着重分析一下getObject方法的實現。
@Override
public Object getObject() throws Exception {
return getTarget();
}
getObject是調用getTarget()來獲取代理對象的。
getTarget方法
<T> T getTarget() {
FeignContext context = this.applicationContext.getBean(FeignContext.class);
Feign.Builder builder = feign(context);
if (!StringUtils.hasText(this.url)) {
if (!this.name.startsWith("http")) {
this.url = "//" + this.name;
}
else {
this.url = this.name;
}
this.url += cleanPath();
return (T) loadBalance(builder, context,
new HardCodedTarget<>(this.type, this.name, this.url));
}
if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
this.url = "//" + this.url;
}
String url = this.url + cleanPath();
Client client = getOptional(context, Client.class);
if (client != null) {
if (client instanceof LoadBalancerFeignClient) {
// not load balancing because we have a url,
// but ribbon is on the classpath, so unwrap
client = ((LoadBalancerFeignClient) client).getDelegate();
}
if (client instanceof FeignBlockingLoadBalancerClient) {
// not load balancing because we have a url,
// but Spring Cloud LoadBalancer is on the classpath, so unwrap
client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
}
builder.client(client);
}
Targeter targeter = get(context, Targeter.class);
return (T) targeter.target(this, builder, context,
new HardCodedTarget<>(this.type, this.name, url));
}