前面時候我釋出兩篇關於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 // https://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 = "http://" + 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 = "http://" + 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)); }