更多Spring与微服务相关的教程请戳这里 Spring与微服务教程合集
1、@EnableFeignClients注解的神秘之处
在使用Feign的时候,我们都知道,需要在springboot的启动类打上@EnableFeignClients注解,才能使Feign生效,那么@EnableFeignClients这个注解到底有哪些神秘之处呢,我们往下看。
首先看一下@EnableFeignClients注解的源码(去除了注释)
package org.springframework.cloud.openfeign; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.springframework.context.annotation.Import; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import(FeignClientsRegistrar.class) public @interface EnableFeignClients { String[] value() default {}; String[] basePackages() default {}; Class<?>[] basePackageClasses() default {}; Class<?>[] defaultConfiguration() default {}; Class<?>[] clients() default {}; }
可以很明显地看到@Import(FeignClientsRegistrar.class),接下来看看这个类的registerBeanDefinitions方法
@Override public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { //注册默认配置 registerDefaultConfiguration(metadata, registry); //注册所有的FeignClient registerFeignClients(metadata, registry); }
这个类实际上做了两件事情,下面我分别介绍
点进registerDefaultConfiguration方法中,如下图
再点进上图中红框框住的方法里面去
注册默认配置到此处就结束了
这一节主要来看看registerFeignClients方法,顾名思义,应该是注册所有的FeignClient
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { ClassPathScanningCandidateComponentProvider scanner = getScanner(); scanner.setResourceLoader(this.resourceLoader); Set<String> basePackages; // 获取@EnableFeignClients注解的属性 Map<String, Object> attrs = metadata .getAnnotationAttributes(EnableFeignClients.class.getName()); // 声明一个Filter,用于找出所有打了@FeignClient主机的类 AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter( FeignClient.class); // 获取@EnableFeignClients注解的clients属性的值 final Class<?>[] clients = attrs == null ? null : (Class<?>[]) attrs.get("clients"); // 如果没有配置clients属性,则走if if (clients == null || clients.length == 0) { // 将过滤器添加打扫描器上 scanner.addIncludeFilter(annotationTypeFilter); // 如果不做额外配置,则默认的basePackages就是启动类所在的包 basePackages = getBasePackages(metadata); } // 如果配置了clients属性,则走else 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))); } // 遍历所有的basePackage for (String basePackage : basePackages) { // 用扫描器在basePackage 下面并结合Filter找出BeanDefinition Set<BeanDefinition> candidateComponents = scanner .findCandidateComponents(basePackage); // 遍历所有的BeanDefinition for (BeanDefinition candidateComponent : candidateComponents) { if (candidateComponent instanceof AnnotatedBeanDefinition) { // 校验:@FeignClient注解必须作用在接口上 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()); // 获取FeignClient的name,具体实现细节请读者自行查看 String name = getClientName(attributes); // 注册FeignClient的配置 registerClientConfiguration(registry, name, attributes.get("configuration")); // 注册FeignClient registerFeignClient(registry, annotationMetadata, attributes); } } } }
可以看到,最后有两个方法,分别是:
下面分别介绍这两个方法
registerClientConfiguration方法挺简单的,就是声明了一个类型为FeignClientSpecification、name为(name + "." + FeignClientSpecification.class.getSimpleName())的bean
如果你的项目中有2个FeignClient,则最终有3个FeignClientSpecification,为什么?因为@EnableFeignCliens注解也会声明一个FeignClientSpecification,这个上面已经说过了
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()); }
下面主要看registerFeignClient方法
private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) { // @FeignClient注解所在的接口名 String className = annotationMetadata.getClassName(); // 生成一个FeignClientFactoryBean的代理类 BeanDefinitionBuilder definition = BeanDefinitionBuilder .genericBeanDefinition(FeignClientFactoryBean.class); // 校验fallback属性和fallbackFactory属性的值是否合格 validate(attributes); // 给FeignClientFactoryBean的代理类添加一系列属性 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); // bean的别名 String qualifier = getQualifier(attributes); if (StringUtils.hasText(qualifier)) { alias = qualifier; } BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[] { alias }); // 通过该工具类创建FeignClient BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); }
上述代码段中,有两行关键代码是下面要讲的
第二个简单点,先看第二个,点进BeanDefinitionReaderUtils.registerBeanDefinition方法里看看
public static void registerBeanDefinition( BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) throws BeanDefinitionStoreException { // Bean的名字 String beanName = definitionHolder.getBeanName(); // 注册bean,但不表示马上初始化Bean registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition()); // 注册bean的别名 String[] aliases = definitionHolder.getAliases(); if (aliases != null) { for (String alias : aliases) { registry.registerAlias(beanName, alias); } } }
这个没什么好说的,就是注册了一个Bean
接下来重点看看FeignClientFactoryBean这个类
该类的声明信息如下
class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware
这里科普一下:
那么先看看FeignClientFactoryBean类是如何实现afterPropertiesSet方法的
@Override public void afterPropertiesSet() throws Exception { Assert.hasText(this.contextId, "Context id must be set"); Assert.hasText(this.name, "Name must be set"); }
这个一看就懂,没什么好说的
接下来重要研究一下FeignClientFactoryBean类如何实现FactoryBean接口的
先来看标记1,FeignContext是在哪初始化的呢?
如下图所示,是在FeignAutoConfiguration中初始化的,这里的FeignContext是feign的全局上下文,并且@EnableConfigurationProperties注解将FeignClientProperties类给初始化了
再看标记2,点进feign方法里面
下图中的get方法,看样子好像是要根据上下文,获取一个FeignLoggerFactory实例
T instance = context.getInstance(this.contextId, type);对于这行代码,context是全局上下文,this.contextId是单个@FeignClient所声明的上下文,而type是FeignLoggerFactory类的class对象
下面红框中的代码,应该是根据@FeignClient的名称,获取由@FeignClient声明的单个上下文,并且上下文类型是AnnotationConfigApplicationContext
// 存储FeignClient的上下文 private Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>(); //存储配置信息 private Map<String, C> configurations = new ConcurrentHashMap<>(); // 根据FeignClient的name去查找,如果没有上下文则createContext,有则直接返回 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); }
然后看看createContext方法,这里面有个this.defaultConfigType属性,它的类型是FeignClientsConfiguration(注意,是FeignClients而不是FeignClient,所以这是个全局配置),关于FeignClientsConfiguration我后面再讲,但是现在我很明确的告诉你这里面配置了类型为FeignLoggerFactory的bean,刚才一系列的进栈不就是为了获取它么
protected AnnotationConfigApplicationContext createContext(String name) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); // 此处的this对象是FeignContext,所以如果项目中有两个FeignClient的话,则this.configurations的数量是3 if (this.configurations.containsKey(name)) { for (Class<?> configuration : this.configurations.get(name) .getConfiguration()) { 将feignClient的配置信息注册到feignContext的上下文中 context.register(configuration); } } // default.开头表示全局配置,将全局配置也注册到feignClient的上下文中 for (Map.Entry<String, C> entry : this.configurations.entrySet()) { if (entry.getKey().startsWith("default.")) { for (Class<?> configuration : entry.getValue().getConfiguration()) { context.register(configuration); } } } // this.defaultConfigType的类型是FeignClientsConfiguration,FeignClientsConfiguration是在这里注册的 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; }
刚才经过了一系列的方法调用,接下来一路出栈,直接到下图
可总算拿到FeignLoggerFactory的实例了,接下来看下图
上图中红框中有个configureFeign方法,该方法用于对单个feignClient进行JavaConfig配置或者yml配置文件配置的
protected void configureFeign(FeignContext context, Feign.Builder builder) { // 获取FeignClientProperties 类型的Bean FeignClientProperties properties = this.applicationContext .getBean(FeignClientProperties.class); if (properties != null) { // 如果配置文件优先,则走if if (properties.isDefaultToProperties()) { // JavaConfig配置 configureUsingConfiguration(context, builder); // 以default开头的配置,即yml中的feign.client.config.default configureUsingProperties( properties.getConfig().get(properties.getDefaultConfig()), builder); // 以feignClient的name开头的配置,即yml中的feign.client.config.name configureUsingProperties(properties.getConfig().get(this.contextId), builder); } // 如果JavaConfig优先,则走else else { configureUsingProperties( properties.getConfig().get(properties.getDefaultConfig()), builder); configureUsingProperties(properties.getConfig().get(this.contextId), builder); configureUsingConfiguration(context, builder); } } else { // 如果没有FeignClientProperties 这个Bean,则使用JavaConfig进行配置 configureUsingConfiguration(context, builder); } }
feign方法出栈后,再完整的看一下getTarget方法
<T> T getTarget() { FeignContext context = this.applicationContext.getBean(FeignContext.class); Feign.Builder builder = feign(context); // 如果@FeignClient注解没有手动指定url,则走if if (!StringUtils.hasText(this.url)) { if (!this.name.startsWith("http")) { this.url = "http://" + this.name; } else { this.url = this.name; } //最终的url类似于http://service-a/service-a,两个service-a分别为name属性值和path属性值 this.url += cleanPath(); // 因为没有手动指定url,此时的请求是负载均衡的,所以用loadBalance方法套了一层,但loadBalance方法最终返回的还是targeter.target return (T) loadBalance(builder, context, new HardCodedTarget<>(this.type, this.name, this.url)); } // 如果手动指定url,则走下面的代码 if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) { this.url = "http://" + this.url; } // 最终的string url为http://localhost:8080/service-a,localhost:8080是url属性的值,而service-a是path属性的值 String url = this.url + cleanPath(); // 获取发起http请求的客户端,默认为null Client client = getOptional(context, Client.class); if (client != null) { //下面的代码虽然出现了loadBalancer的字眼,但是分别是因为ribbon和spring cloud loadBalancer在classpath上才打开它的,但是并不能提供负载均衡,因为是手动指定的url if (client instanceof LoadBalancerFeignClient) { client = ((LoadBalancerFeignClient) client).getDelegate(); } if (client instanceof FeignBlockingLoadBalancerClient) { client = ((FeignBlockingLoadBalancerClient) client).getDelegate(); } builder.client(client); } //默认为HystrixTargeter Targeter targeter = get(context, Targeter.class); // 最终的返回值,即FeignClientFactoryBean的生产目标对象 return (T) targeter.target(this, builder, context, new HardCodedTarget<>(this.type, this.name, url)); }
3、FeignClientsConfiguration和FeignClientProperties
这两个类分别对应全局配置和单个配置,那么何时加载的呢?
在本文中全局搜索这两个类的名字,答案就在上面。