Spring Cloud教程 第八弹 Feign源码解读

更多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);
}

这个类实际上做了两件事情,下面我分别介绍

 

1.1、registerDefaultConfiguration注册默认配置

点进registerDefaultConfiguration方法中,如下图

Spring Cloud教程 第八弹 Feign源码解读

 

再点进上图中红框框住的方法里面去

Spring Cloud教程 第八弹 Feign源码解读

 

注册默认配置到此处就结束了

 

1.2、registerFeignClients注册所有的FeignClient

这一节主要来看看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(注册FeignClient的配置) 
  • registerFeignClient(注册FeignClient)

下面分别介绍这两个方法

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);
	}

上述代码段中,有两行关键代码是下面要讲的

  • 第一个关键代码是BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
  • 第二个关键代码是最后一行代码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

这里科普一下:

  • Spring 中有两种类型的Bean,一种是普通Bean,另一种是工厂Bean 即 FactoryBean。FactoryBean跟普通Bean不同,其返回的对象不是指定类的一个实例,而是该FactoryBean的getObject方法所返回的对象,创建出来的对象是否属于单例由isSingleton中的返回决定。
  • 而实现了InitializingBean接口为bean提供了初始化方法的方式,它只包括afterPropertiesSet方法,凡是继承该接口的类,在初始化bean的时候会执行该方法

那么先看看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接口的

Spring Cloud教程 第八弹 Feign源码解读

 

先来看标记1,FeignContext是在哪初始化的呢?

如下图所示,是在FeignAutoConfiguration中初始化的,这里的FeignContext是feign的全局上下文,并且@EnableConfigurationProperties注解将FeignClientProperties类给初始化了

Spring Cloud教程 第八弹 Feign源码解读

 

再看标记2,点进feign方法里面

下图中的get方法,看样子好像是要根据上下文,获取一个FeignLoggerFactory实例

Spring Cloud教程 第八弹 Feign源码解读

 

T instance = context.getInstance(this.contextId, type);对于这行代码,context是全局上下文,this.contextId是单个@FeignClient所声明的上下文,而type是FeignLoggerFactory类的class对象

Spring Cloud教程 第八弹 Feign源码解读

 

下面红框中的代码,应该是根据@FeignClient的名称,获取由@FeignClient声明的单个上下文,并且上下文类型是AnnotationConfigApplicationContext

Spring Cloud教程 第八弹 Feign源码解读

 

// 存储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;
	}

刚才经过了一系列的方法调用,接下来一路出栈,直接到下图

Spring Cloud教程 第八弹 Feign源码解读

 

可总算拿到FeignLoggerFactory的实例了,接下来看下图

Spring Cloud教程 第八弹 Feign源码解读

 

上图中红框中有个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

这两个类分别对应全局配置和单个配置,那么何时加载的呢?

在本文中全局搜索这两个类的名字,答案就在上面。

 

Spring Cloud教程 第八弹 Feign源码解读

 

上一篇:2021-06-09


下一篇:Spring Cloud Feign的文件上传实现