【SpringCloud原理】萬字剖析OpenFeign之FeignClient動態代理生成原始碼
前面時候我釋出兩篇關於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模組驅動實現原理講解
接下來看一下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來獲取物件,這裡我就不展開講了