探秘 Feign 核心注解:@FeignClient 和 @EnableFeignClients 是如何打通微服务通信的 “任督二脉” 的?

前两篇文章揭秘 Feign 调用机制:微服务通信的无缝集成微服务通信背后的秘密:Ribbon 如何选择最佳服务实例?,我们已经了解到 Feign 调用机制的一大优势 ------ 在不需要指定域名的情况下,能够借助 Ribbon 精准地找到并调用微服务。本文我们继续来分享Feign调用机制中两个最核心的注解。

一、 @FeignClient

@FeignClient 是 Spring Cloud Feign 提供的一个注解,用于声明一个 REST 客户端接口。这个注解的主要作用是告诉 Spring 框架在启动时创建一个实现了该接口的 Feign 客户端实例,并将其注册为 Spring 管理的 Bean。以下是该注解的各个属性及其功能:

  1. name / value属性:
  • 类型: String
  • 默认值: ""
  • 描述: 服务的名称,可以带有协议前缀。必填。如,@FeignClient(name = "myService"),这里的 "myService" 就是我们要调用的目标服务的名称,Feign 会依据它去服务注册中心查找对应的服务实例信息。
  1. contextId属性:
  • 类型: String
  • 默认值: ""
  • 描述: 用作 Bean 名称,但不会作为服务ID使用,如@FeignClient(contextId = "myContextId"),"myContextId" 就是我们给这个Bean 起的一个特定名称
  1. qualifier属性:
  • 类型: String
  • 默认值: ""
  • 描述: 用来指定 Feign 客户端的@Qualifier值。举个例子,@FeignClient(qualifier = "myQualifier"),"myQualifier" 就是我们为这个 Feign 客户端设置的@Qualifier值啦,在一些需要通过@Qualifier来区分不同 Bean 的场景下就会用到它。
  1. url属性:
  • 类型: String
  • 默认值: ""
  • 描述: 绝对URL或可解析的主机名(协议可选)。如@FeignClient(url = "http://cus.com"),这样 Feign 就会直接把请求发送到这个指定的地址啦,而不会再去服务注册中心查找服务实例信息
  1. decode404属性:
  • 类型: boolean
  • 默认值: false
  • 描述: 是否解码 404 错误而不是抛出 FeignException。如@FeignClient(decode404 = true),那么当遇到 404 错误时,Feign 就会尝试去解码这个错误,而不是像默认情况那样直接抛出异常给调用者。
  1. configuration属性:
  • 类型: Class<?>[]
  • 默认值: { }
  • 描述: 自定义配置类,可以覆盖默认的 Feign 配置,例如 Decoder, Encoder, Contract 等。如@FeignClient(configuration = MyFeignConfig.class),"MyFeignConfig" 就是我们自己写的配置类,通过它就能按照我们的需求灵活调整 Feign 客户端的行为。
  1. fallback属性:
  • 类型: Class<?>
  • 默认值: void.class
  • 描述: 指定的 Feign 客户端接口的容错类,必须实现该接口并是一个有效的 Spring Bean。如@FeignClient(fallback = MyFallback.class),当目标服务出现故障(比如不可用、超时等)时,Feign 就会调用 "MyFallback" 这个容错类中的对应方法来返回一个预设的结果,而不是直接抛出异常给调用者,这样就能保证系统的部分功能依然能够正常运行啦,大大提高了系统的容错。
  1. fallbackFactory属性:
  • 类型: Class<?>
  • 默认值: void.class
  • 描述: 指定的 Feign 客户端接口的容错工厂,必须生成实现该接口的实例,并是一个有效的 Spring Bean 。如@FeignClient(fallbackFactory = MyFallbackFactory.class),当目标服务出现故障时,"MyFallbackFactory" 这个容错工厂类就会被用来创建合适的容错对象并处理请求,而且通过这个工厂类还能在容错逻辑中获取到导致服务降级的原因(比如异常信息等),以便进行更精细的处理
  1. path属性:
  • 类型: String
  • 默认值: ""
  • 描述: 所有方法级别映射的路径前缀,可以与 @RibbonClient 一起使用。@FeignClient(path = "/customer-api"),这样在后续通过这个 Feign 客户端接口调用方法时,请求路径就会自动加上这个前缀。
  1. primary属性:
  • 类型: boolean
  • 默认值: true
  • 描述: 是否将 Feign 代理标记为主 Bean。如@FeignClient(primary = false),那么这个 Feign 代理就不会被当作主 Bean 来处理,在一些需要区分不同优先级的 Bean 的场景下会用到这个属性

二、@EnableFeignClients

@EnableFeignClients 也是 Spring Cloud Feign 的一个注解,用于启用 Feign 客户端。这个注解通常放在配置类或启动类上,Spring 会扫描标注了 @FeignClient 注解的接口,并生成对应的客户端代理类,以便在应用中通过这些接口调用远程服务。

核心注册方法 位于 FeignClientsRegistrar 类中,主要通过以下几个方法完成 Feign 客户端的注册。

  1. registerDefaultConfiguration(AnnotationMetadata metadata, BeanDefinitionRegistry registry)方法
  • 作用:注册默认的 Feign 配置,如果在 @EnableFeignClients 注解中指定了 defaultConfiguration 属性,则将其作为默认配置应用于所有 Feign 客户端。
  • 实现
java 复制代码
private void registerDefaultConfiguration(AnnotationMetadata metadata,
			BeanDefinitionRegistry registry) {
		Map<String, Object> defaultAttrs = metadata
				.getAnnotationAttributes(EnableFeignClients.class.getName(), true);
		// 键defaultConfiguration不为空的时候,会调用registerClientConfiguration方法把从defaultAttrs中获取到的默认配置信息注册到registry
		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"));
		}
	}
  1. registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry)方法
  • 作用: 扫描并注册所有标注了 @FeignClient 的接口,生成相应的 Feign 客户端代理类。
  • 实现
java 复制代码
public void registerFeignClients(AnnotationMetadata metadata,
			BeanDefinitionRegistry registry) {
		// 存储所有的Bean
		LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();
		// EnableFeignClients注解的5
		Map<String, Object> attrs = metadata
				.getAnnotationAttributes(EnableFeignClients.class.getName());
		// 过滤带有@FeignClient标记的接口
		AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
				FeignClient.class);
		final Class<?>[] clients = attrs == null ? null
				: (Class<?>[]) attrs.get("clients");
		if (clients == null || clients.length == 0) {
			ClassPathScanningCandidateComponentProvider scanner = getScanner();
			scanner.setResourceLoader(this.resourceLoader);
			scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
			Set<String> basePackages = getBasePackages(metadata);
			for (String basePackage : basePackages) {
			// scanner.findCandidateComponent会获取到所有@FeignClient标准的接口
		candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
			}
		}
		......

		for (BeanDefinition candidateComponent : candidateComponents) {
			if (candidateComponent instanceof AnnotatedBeanDefinition) {
				AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
				AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
				// 过滤接口,因为@FeignClient只标准在接口上
				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);
			}
		}
	}
  1. registerFeignClient(BeanDefinitionRegistry registry,
    AnnotationMetadata annotationMetadata, Map<String, Object> attributes)方法
  • 作用:这个方法的主要任务就是根据传入的参数信息,将一个标注了@FeignClient注解的接口注册为一个具体的 Bean 定义,包括设置各种属性值、指定 Bean 的类型、设置自动装配模式等等,以便后续能够正确生成客户端代理类。
  • 实现
java 复制代码
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("name", name);
		// 依次设置章节1提到的@FeignClient的10个属性
		...... 
		AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
        beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);

		BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
				new String[] { alias });
		BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
	}
  1. registerBeanDefinition(holder, registry) 方法
  • 作用:将 Bean 定义注册到 Spring 容器中,在注册过程中,它会进行一系列的检查和处理,比如验证 Bean 定义的有效性、处理已有相同名称 Bean 定义的情况、更新相关的缓存和列表等等,以确保 Bean 定义能够正确地注册和更新。
  • 实现
java 复制代码
    public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) throws BeanDefinitionStoreException {
        Assert.hasText(beanName, "Bean name must not be empty");
        Assert.notNull(beanDefinition, "BeanDefinition must not be null");
        ......
        BeanDefinition existingDefinition = (BeanDefinition)this.beanDefinitionMap.get(beanName);
        // 重复bean处理
        if (existingDefinition != null) {
            if (!this.isAllowBeanDefinitionOverriding()) {
                throw new BeanDefinitionOverrideException(beanName, beanDefinition, existingDefinition);
            }

            if (existingDefinition.getRole() < beanDefinition.getRole()) {
                if (this.logger.isInfoEnabled()) {
                    this.logger.info("Overriding user-defined bean definition for bean '" + beanName + "' with a framework-generated bean definition: replacing [" + existingDefinition + "] with [" + beanDefinition + "]");
                }
            } else if (!beanDefinition.equals(existingDefinition)) {
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Overriding bean definition for bean '" + beanName + "' with a different definition: replacing [" + existingDefinition + "] with [" + beanDefinition + "]");
                }
            } else if (this.logger.isTraceEnabled()) {
                this.logger.trace("Overriding bean definition for bean '" + beanName + "' with an equivalent definition: replacing [" + existingDefinition + "] with [" + beanDefinition + "]");
            }

            this.beanDefinitionMap.put(beanName, beanDefinition);
        } else {
            // 新增bean处理
            if (this.hasBeanCreationStarted()) {
                synchronized(this.beanDefinitionMap) {
                    this.beanDefinitionMap.put(beanName, beanDefinition);
                    List<String> updatedDefinitions = new ArrayList(this.beanDefinitionNames.size() + 1);
                    updatedDefinitions.addAll(this.beanDefinitionNames);
                    updatedDefinitions.add(beanName);
                    this.beanDefinitionNames = updatedDefinitions;
                    this.removeManualSingletonName(beanName);
                }
            } else {
                this.beanDefinitionMap.put(beanName, beanDefinition);
                this.beanDefinitionNames.add(beanName);
                this.removeManualSingletonName(beanName);
            }

            this.frozenBeanDefinitionNames = null;
        }
        // 缓存清理
        if (existingDefinition == null && !this.containsSingleton(beanName)) {
            if (this.isConfigurationFrozen()) {
                this.clearByTypeCache();
            }
        } else {
            this.resetBeanDefinition(beanName);
        }

    }

三、总结

@FeignClient 和 @EnableFeignClients 是 Feign 框架中至关重要的注解,通过剖析 @FeignClient @EnableFeignClients ,我们可以更好的理解 Feign 是如何自动扫描、完成注册操作。

这种自动化的机制,大大简化了微服务之间的通信,使开发者能够专注于业务逻辑,而无需关心底层的调用细节。

在下一篇文章中,我们将探讨已经注册的bean是如何生成以及什么时机生成客户端代理的,敬请期待!

相关推荐
三日看尽长安花5 小时前
【Redis:原理、架构与应用】
数据库·redis·架构
搬砖天才、6 小时前
自动化部署-02-jenkins部署微服务
微服务·自动化·jenkins
Flamesky7 小时前
dotnet core微服务框架Jimu ~ 会员授权微服务
微服务·dotnet·services·micro·jimu·积木
猫猫不是喵喵.8 小时前
【微服务】Feign 远程调用
spring cloud·微服务
AIGC绘画8 小时前
Spring微服务概述之spring cloud alibaba服务调用实践
java·spring·微服务
猫猫不是喵喵.10 小时前
【微服务】Nacos 注册中心
spring cloud·微服务·eureka
程序员大佬超10 小时前
Consul微服务配置中心部署(在线安装)
微服务·consul
斯普信专业组10 小时前
K8s企业应用之容器化迁移
云原生·容器·kubernetes
颜淡慕潇10 小时前
【K8S系列】Kubernetes 中 Service IP 分配 问题及解决方案【已解决】
后端·云原生·容器·kubernetes
摇曳 *11 小时前
Kubernetes:(三)Kubeadm搭建K8s 1.20集群
云原生·容器·kubernetes