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