Feign之EnableFeignClients引发的初步探索(一)

背景

(可以略过的废话)前段时间一个朋友在做spring security相关配置时遇到一些问题,请我帮忙解决一些问题,由于我自身并没有使用过security,所以花了一段时间去学习spring security的使用和解析了一下spring security源码。
时至今日,终于可以再次重新开始学习spring cloud相关的内容了~



1.@EnableFeignClients注解的默认配置

1.1 basePackages

扫描@FeignClient注解的包路径,扫描这些包下的所有类,获取被@FeignClient注解的接口,生成代理对象。

1.2 basePackageClasses

这个其实跟上面的很像,就是做一些标记类,然后这些标记类所在的包及其子包里的类都会被扫描。

1.3 defaultConfiguration

这个是对所有feign 客户端进行配置设定使用的。

可以覆写feign client的相关组件定义

feign.codec.Decoder : 解码器,也就是feign远程调用其他接口获取结果后进行解码成对应的对象

feign.codec.Encoder : 编码器,feign在远程调用之前,需要先将请求参数进行编码成对应的表单或者json之类的。

feign.Contract :注解解释器,也就是用来解读@FeignClient标注的interface上的所有注解信息,转化为对应的url和参数。因为web框架比较多。每种注解不一致。feign是Netflix内部使用的,所以他们并没有对springmvc进行支持。后来openFeign中才添加对spring mvc的支持(也就是SpringMvcContract)。

1.4 clients

会基于这几个类和类所在的包进行扫描的。



2.FeignClientsRegistrar之EnableFeignClients导入

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eA05YUGl-1626782964309)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/fc211fd6-1e44-4865-9252-f13355526e51/Untitled.png)]

FeignClientsRegistrar实现了ImportBeanDefinitionRegistrar接口。
而对于spring来说,实现了ImportBeanDefinitionRegistrar的类,会在org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsFromRegistrars方法中会进行registerBeanDefinitions方法的调用。

所在在FeignClientRegistrar中,我们只需要关注registerBeanDefinitions方法即可。



3.registerBeanDefinitions

@Override
	public void registerBeanDefinitions(AnnotationMetadata metadata,
			BeanDefinitionRegistry registry) {
		// 默认配置注册
		registerDefaultConfiguration(metadata, registry);
		// 注册FeignClient
		registerFeignClients(metadata, registry);
	}

3.1 registerDefaultConfiguration 设置默认配置

private void registerDefaultConfiguration(AnnotationMetadata metadata,
			BeanDefinitionRegistry registry) {
		// 获取EnableFeignClients注解里面的参数信息
		Map<String, Object> defaultAttrs = metadata
				.getAnnotationAttributes(EnableFeignClients.class.getName(), true);

		if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
			// 这个就是用来配置
			String name;
			if (metadata.hasEnclosingClass()) {
				内部类实现
				name = "default." + metadata.getEnclosingClassName();
			}
			else {
				// 顶层类实现 
				name = "default." + metadata.getClassName();
			}
			// 开始注册配置信息
			registerClientConfiguration(registry, name,
					defaultAttrs.get("defaultConfiguration"));
		}
	}

上面就是从注解中获取配置信息,然后将配置信息注入到spring容器中。


具体如何注入的呢?
首先通过BeanDefinitionBuilder构建一个FeignClientSpecification对象,然后注入上面生成的name和配置信息。

最后通过BeanDefinitionRegistry将由BeanDefinitionBuilder构建出的FeignClientSpecification注入到spring容器中去。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sEOhZETt-1626782964312)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/dcb6a956-997f-4222-9d28-36bcc5d8f1d3/Untitled.png)]

从上面我们知道,如果我们要自定义编码器Encoder,解码器Decoder,注解解释器Contract时,实际上配置是交给FeignClientSpecification管理的。


其实这里可以深究一下FeignClientSpecification 这个东西在哪用了,毕竟我们如果对组件进行了修改或者自定义,那么如何融合到feign的流程中,也是很有比较有探究价值的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vIzOUtE8-1626782964314)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e40a4aa4-8fd6-4701-8111-c6d99e29bfeb/Untitled.png)]

从上图我们可以看到,总共只有6处使用到这个类。其中两处需要关注的地方我已经标注出来。

第一个使用地:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FHrRpytg-1626782964318)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/63a6b710-f373-4e50-bfb5-95c957d3764d/Untitled.png)]
从这个类的名字可以看出,这是个自动装配类,还是Feign直接相关的自动装配。那么这个类所在的包的spring.factories就是我们后面探索的关键所在了。

这里可以看到,FeignClientSpecification 作为bean被Autowired到一个List集合中去了。那么后续的使用只需要关注configurations了。

这里就先不深入了,在后面探索spring.factories时,再深入研究。


第二个使用地:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fmsbMENK-1626782964321)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5a0bf43d-c151-45d2-af43-085cf89b53bf/Untitled.png)]

FeignContext,见名知意,就是Feign的上下文。而上下文是跟作为泛型的FeignClientSpecification 有关的。

显然FeignContext也是有关于FeignClientSpecification 的操作的。



3.2 registerFeignClients 扫描@FeignClient生成代理对象注册到spring容器中

1.获取要扫描的包或者类
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AezDP45k-1626782964323)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0621b825-c1db-43e4-b948-35a5239390e8/Untitled.png)]

2.基于@EnableFeignClient的clients配置

final Set<String> clientClasses = new HashSet<>();
basePackages = new HashSet<>();
for (Class<?> clazz : clients) {
	// 获取clients里面配置的class的包名,并加入到包扫描集合中
	basePackages.add(ClassUtils.getPackageName(clazz));
	// FeignClient类
	clientClasses.add(clazz.getCanonicalName());
}
// 测试使用的
AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
	@Override
	protected boolean match(ClassMetadata metadata) {
		String cleaned = metadata.getClassName().replaceAll("\\$", ".");
		return clientClasses.contains(cleaned);
	}
};
// 扫描器添加过滤器,一个是测试使用的,一个是基于FeignClient注解的过滤器
scanner.addIncludeFilter(
		new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));

所以上面其实就是干了一个事情,就是将clients配置的类所在的包路径提取出来,然后再走包扫描。

3.包扫描

for (String basePackage : basePackages) {
	// 扫描出包含@FeignClient注解的类
	Set<BeanDefinition> candidateComponents = scanner
			.findCandidateComponents(basePackage);
	for (BeanDefinition candidateComponent : candidateComponents) {
		if (candidateComponent instanceof AnnotatedBeanDefinition) {
			// 只要注解类bean声明
			// verify annotated class is an interface
			AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
			AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
			// 判断这个@FeignClient注解的这个class是不是interface
			Assert.isTrue(annotationMetadata.isInterface(),
					"@FeignClient can only be specified on an interface");
			// 获取注解配置的属性信息
			Map<String, Object> attributes = annotationMetadata
					.getAnnotationAttributes(
							FeignClient.class.getCanonicalName());
			// 这里是获取客户端名称,用于注册到spring中去
			String name = getClientName(attributes);
			// 在EnableFeignClient中可以配置全局FeignClient的配置
			// 在FeignClient中可以针对单个进行配置
			// 这也就是为啥我们在FeignAutoConfiguration中看到FeignClientSpecification
			// 是一个集合的原因了。
			registerClientConfiguration(registry, name,
					attributes.get("configuration"));
			
			// 注册FeignClientFactoryBean到spring容器中
			registerFeignClient(registry, annotationMetadata, attributes);
		}
	}
}

关于ClassPathScanningCandidateComponentProvider 如何扫描到@FeignClient类:

这里插入一个spring的知识点,ClassPathScanningCandidateComponentProvider 这个类就是用来基于路径来扫描合适的组件的。

这其中有个很重要的东西,就是我们如何通过类路径扫描类并获取到自己想要的组件呢?

就是通过ClassPathScanningCandidateComponentProviderisCandidateComponent方法来进行判断的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9aphUeOv-1626782964326)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/39594525-83f0-471c-9cd7-3e8d125ab279/Untitled.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-awqBm1IC-1626782964328)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/53377eea-a5bb-4dd6-953d-3d4705cdc6d4/Untitled.png)]

从上面我们可以看到,我们创建了一个注解类型过滤器,并注入到了ClassPathScanningCandidateComponentProviderincludeFilters中,然后在扫描的过程中通过通过AnnotationTypeFilter去匹配扫描出来的每个类。

至此,我们就知道如何获取@FeignClient注解的类了。


4.registerFeignClient 注册FeignClient到spring中

其实这里说将FeignClient注册到spring中其实,不是很正确。其实是注册了一个FeignClientFactoryBean到spring容器中去了。

private void registerFeignClient(BeanDefinitionRegistry registry,
			AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
		// 1.获取被@FeignClient注解的Interface的名称
		String className = annotationMetadata.getClassName();
		// 2.创建一个FeignClientFactoryBean的spring bean构建器
		BeanDefinitionBuilder definition = BeanDefinitionBuilder
				.genericBeanDefinition(FeignClientFactoryBean.class);
		// 3.参数校验:主要是关于失败回调的配置
		validate(attributes);
		// 4.设置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);
		// 远程调用404是否解码
		definition.addPropertyValue("decode404", attributes.get("decode404"));
		// 失败回调
		definition.addPropertyValue("fallback", attributes.get("fallback"));
		// 失败回调工厂
		definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
		// 设置注入类型,通过类型进行注册(也就是className)
		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;
		}
		// 通过holder,可以通过className,也可以通过别名来获取这个FeignClient
		BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
				new String[] { alias });
		// 5.注册FeignClientFactoryBean到spring容器中
		BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
	}

从上面我们可以看到,我们可以看到所有关于FeignClient的信息都被封装到了FeignClientFactoryBean中,其实这个时候并没有创建FeignClient对象。

真正的FeignClient对象是在FeignClientFactoryBean里通过getObject()方法创建的。

这里其实又涉及到Spring的一个知识点:

关于FactoryBean对象,在spring初始化bean过程中会通过org.springframework.beans.factory.support.FactoryBeanRegistrySupport#getObjectFromFactoryBean 方法来对实现了FactoryBean的bean进行getObject()方法调用。通过这个getObject就能获取这个工厂bean创建的对象。

5.FeignClientFactoryBean#getObject获取真正的FeignClient

@Override
	public Object getObject() throws Exception {
		return getTarget();
	}
<T> T getTarget() {
		
		FeignContext context = applicationContext.getBean(FeignContext.class);
		// 获取feign构建器
		Feign.Builder builder = feign(context);
		// 一般我们都是通过注册中心+feign来使用,所以基本都是进入下面这个判断
		// 然后就返回了。
		// 也就是说url我们都不会填的
		if (!StringUtils.hasText(this.url)) {
			// 这里就是判断是不是基于注册中心进行远程调用
			if (!this.name.startsWith("http")) {
				url = "http://" + this.name;
			}
			else {
				url = this.name;
			}
			url += cleanPath();
			// 将feign与负载均衡器和限流器进行结合,生成一个对象进行返回
			// 这里层层包裹
			// Targeter: 有两个实现DefaultTargeter,HystrixTargeter
			// 默认为HystrixTargeter,这个其实针对的是HystrixFeign来进行配置的
			// 最终这里会创建出一个this.type的代理对象,
			// 这个对象持有LoadBalanceFeignClient
			// 以及LoadBalancerFeignClient
			// 这个里面的东西比较复杂,稍后单独开一篇解析一下
			return (T) loadBalance(builder, context, new HardCodedTarget<>(this.type,
					this.name, 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
				// 指定了url,这个时候就不需要负载均衡了,所以要从负载均衡器中取出发送http请求的
				// client
				client = ((LoadBalancerFeignClient)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));
	}

基于建造者模式构建Feign

protected Feign.Builder feign(FeignContext context) {
		// feign客户端日志工厂
		FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);
		Logger logger = loggerFactory.create(this.type);

		// @formatter:off
		Feign.Builder builder = get(context, Feign.Builder.class)
				// required values
				.logger(logger) // feign独立日志配置
				.encoder(get(context, Encoder.class)) // 编码器
				.decoder(get(context, Decoder.class)) // 解码器
				.contract(get(context, Contract.class)); // 注解解释器
		// @formatter:on
		// 设置一下配置
		// 1.连接超时时间
		// 2.重试次数
		// 3.异常解码器
		// 4.请求拦截器
		// 5.404解码器是否开启设置
		// 6. 自定义编码器
		// 7. 自定义解码器
		// 8. 自定义注解解码器(就是解析注解为url)
		configureFeign(context, builder);

		return builder;
	}

4.小结

单从@EnableFeignClient注解引入,即可看到很多信息。其中最主要的就是EncoderDecoderContract这三个组件。

还有FeignClientFactoryBean如何将Feign构建器FeignContextHysterixFeignLoadBalancerFeignClient一起整合到带有@FeignClient注解的接口的代理类中。

举例说明:

@FeignClient
@RequestMapping("/user")
public interface ServiceAController{
	@RequestMapping("/hello")
	void sayHello();
}

流程:

1.在启动阶段首先会创建一个FeignClientFactoryBean,它包含ServiceAController的所有信息,其中包含@FeignClient注解的参数,类名等信息

2.FeignClientFactoryBean 会创建一个Feign.Builder ,也就是Feign的构建器,并往里面添加各种配置和组件

3.获取LoadBalancerFeignClient,添加到Feign.Builder

4.获取HystrixTargeter,如果feign开启了Hystrix,则进行相关失败回调设置。

5.通过Feign.Builder.build()创建一个ReflectiveFeign对象。此时ReflectiveFeign对象中包含LoadBalancerFeignClientFeignClientFactoryBeanhystrix配置等信息。

6.通过ReflectiveFeign.newInstance方法创建一个ServiceAController的代理对象,其中InvocationHandlerReflectiveFeign的内部类,它在创建时会带入ReflectiveFeign所携带的内容。

至此,我们就了解了被@FeignClient注解的接口的代理类的实现方式了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OmnSfWtW-1626782964330)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/03fbfc54-c0ef-4c16-a345-57df8d89d9fe/Untitled.png)]

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值