专栏名称: Java知音
专注于Java,推送技术文章,热门开源项目等。致力打造一个有实用,有情怀的Java技术公众号!
目录
相关文章推荐
募格学术  ·  DeepSeek王炸登场!AI的神级应用被发 ... ·  昨天  
小张聊科研  ·  2月19日(周三)晚20:00直播预告 | ... ·  2 天前  
国家知识产权局  ·  关于申报2025年度课题研究项目的通知 ·  4 天前  
51好读  ›  专栏  ›  Java知音

注解 @EnableFeignClients 工作原理

Java知音  · 公众号  ·  · 2021-02-24 09:45

正文

作者 :安迪源文

andyboke.blog.csdn.net/article/details/86680622

概述

Spring cloud 应用中,当我们要使用 feign 客户端时,一般要做以下三件事情 :

1.使用注解 @EnableFeignClients 启用 feign 客户端;

示例 :

@SpringBootApplication
@EnableFeignClients
public class TestApplication {
    public static void main(String[] args) {
        SpringApplication.run(TestApplication.classargs);
    }
}

2.使用注解 @FeignClient 定义 feign 客户端 ;

示例 : 该例子定义了一个 feign 客户端,将远程服务 http://test-service/test/echo 映射为一个本地 Java 方法调用。

@FeignClient(name = "test-service", path = "/test")
public interface TestService {
    @RequestMapping(value = "/echo", method = RequestMethod.GET)
    TestModel echo(@RequestParam("parameter") String parameter);
}

3.使用注解 @Autowired 使用上面所定义 feign 的客户端 ;

@Autowired   
TestService testService;

public void run()
{
    // 这里的使用本地Java API的方式调用远程的Restful接口
    TestModel dto = testService.echo("Hello,你好!");
    log.info("echo : {}", dto);
 }

上面的三个步骤,前两个步骤可以理解为定义 feign 客户端,第三步是使用所定义的 feign 客户端。通过调试发现,上面第三步所注入的 testService 是一个代理对象,如下所示 :

testService = {$Proxy66@5502
 "HardCodedTarget(type=TestService, name=test-service, url=http://test-service/test)"
 h = {ReflectiveFeign$FeignInvocationHandler@6924
  target = {Target$HardCodedTarget@6930
  dispatch = {LinkedHashMap@6931}  size = 1
   0 = {LinkedHashMap$Entry@6948
    "public abstract xxx.model.TestModel xxx.service.TestService.echo(java.lang.String)" 

该对象会代理客户端完成远程服务方法的调用,那么,该代理对象是如何生成的 ?这篇文章,我们通过源代码分析来回答这些问题。

源代码解析

源代码版本 : spring-cloud-openfeign-core-2.1.0.RELEASE , Spring Cloud Greenwich.RELEASE

注解 @EnableFeignClients :扫描和注册 feign 客户端 bean 定义

注解 @EnableFeignClients 告诉框架扫描所有使用注解 @FeignClient 定义的 feign 客户端。它又通过注解 @Import 导入了类 FeignClientsRegistrar ( feign 客户端注册器),如下所示:

@EnableFeignClients 
 => @Import(FeignClientsRegistrar.class)

那么 FeignClientsRegistrar 又是做什么的呢 ?我们继续。

FeignClientsRegistrar : feign 客户端注册器

FeignClientsRegistrar 实现了接口 ImportBeanDefinitionRegistrar 。而 ImportBeanDefinitionRegistrar 的设计目的,就是被某个实现类实现,配合使用 @Configuration 注解的使用者配置类,在配置类被处理时,用于额外注册一部分 bean 定义:

对于上面的例子,使用者配置类就是 TestApplication

public interface ImportBeanDefinitionRegistrar {

   /**
    * Register bean definitions as necessary based on the given annotation metadata of
    * the importing @Configuration class.
    * 根据使用者配置类的注解元数据注册bean定义
    * @param importingClassMetadata 使用者配置类的注解元数据
    * @param registry 当前bean定义注册表,一般指当前Spring应用上下文对象,当前Spring容器
    */

   public void registerBeanDefinitions(
     AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry)
;

}

#registerBeanDefinitions – 注册 feign 客户端配置和 feign 客户端

方法 FeignClientsRegistrar#registerBeanDefinitions 实现如下:

   @Override
   public void registerBeanDefinitions(AnnotationMetadata metadata,BeanDefinitionRegistry registry) {
    // 注册缺省配置到容器 registry
    registerDefaultConfiguration(metadata, registry);
    // 注册所发现的各个 feign 客户端到到容器 registry
    registerFeignClients(metadata, registry);
   }

#registerDefaultConfiguration – 注册 feign 客户端缺省配置

// 注册feign客户端的缺省配置,缺省配置信息来自注解元数据的属性 defaultConfiguration    
private void registerDefaultConfiguration(AnnotationMetadata metadata,BeanDefinitionRegistry registry) {
 // 获取注解@EnableFeignClients的注解属性     
 Map defaultAttrs = metadata
   .getAnnotationAttributes(EnableFeignClients.class.getName(), true);

 if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
  String name;
  // 下面是对所注册的缺省配置的的命名,格式如下 :
  // default.xxx.TestApplication
  if (metadata.hasEnclosingClass()) {
   //  针对注解元数据metadata对应一个内部类或者方法返回的方法本地类的情形
   name = "default." + metadata.getEnclosingClassName();
  }
  else {        
   // name 举例 : default.xxx.TestApplication
   // 这里 xxx.TestApplication 是注解@EnableFeignClients所在配置类的长名称   
   name = "default." + metadata.getClassName();
  }
  // 各种信息准备就绪,现在执行注册
  registerClientConfiguration(registry, name,
    defaultAttrs.get("defaultConfiguration"));
 }
}

#registerDefaultConfiguration 方法最终注册客户端缺省配置的动作交给方法 #registerClientConfiguration 执行。

#registerClientConfiguration – 注册 feign 客户端配置

// 将指定feign客户端配置configuration作为一个bean定义注册到容器:
// bean 定义对象类型 : GenericBeanDefinition
// bean class : FeignClientSpecification    
// bean name : default.xxx.TestApplication.FeignClientSpecification (缺省配置)
// bean name : test-service.FeignClientSpecification (针对某个feign client 的配置)
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
  Object configuration)
 
{
 BeanDefinitionBuilder builder = BeanDefinitionBuilder
   .genericBeanDefinition(FeignClientSpecification.class);
 // 设置构造函数参数  
 builder.addConstructorArgValue(name);
 builder.addConstructorArgValue(configuration);
 // 从bean定义构建器构造bean定义并注册到容器
 registry.registerBeanDefinition(
   name + "." + FeignClientSpecification.class.getSimpleName(),
   builder.getBeanDefinition())
;
}

#registerClientConfiguration 方法用于注册一个 feign 客户端配置 bean ,可以用于注册针对所有 feign 客户端的缺省配置的注册,也可以用于针对每个 feign 客户端的专有配置的注册。

针对所有 feign 客户端的缺省配置的 bean 名称类似于 : default.xxx.TestApplication.FeignClientSpecification

针对某个名称为 test-service feign 客户端的配置的 bean 名称类似于: test-service.FeignClientSpecification

#registerFeignClients – 注册各个 feign 客户端及其配置

// 参数 metadata : 注解@EnableFeignClients所在配置类的注解元数据
public void registerFeignClients(AnnotationMetadata metadata,BeanDefinitionRegistry registry) {
 // 定义一个基于classpath的组件扫描器,它会根据指定的扫描位置和@EnableFeignClients注解属性   
 // 找出开发人员定义的所有feign客户端,也就是那些使用了注解@FeignClient的所有接口定义
 ClassPathScanningCandidateComponentProvider scanner = getScanner();
 scanner.setResourceLoader(this.resourceLoader);

 Set basePackages;

 // attrs 用于表示注解@EnableFeignClients所在配置类的注解元数据中注解@EnableFeignClients
 // 的部分
 Map attrs = metadata
   .getAnnotationAttributes(EnableFeignClients.class .getName());
 AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
   FeignClient.class);
 final Class>[] clients = attrs == null ? null
   : (Class>[]) attrs.get("clients");
 if (clients == null || clients.length == 0) {
       // @EnableFeignClients 中没有指定 clients 属性的情况
  scanner.addIncludeFilter(annotationTypeFilter);
  basePackages = getBasePackages(metadata);
 }
 else {
       // @EnableFeignClients 中指定了 clients 属性的情况
  final Set 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)));
 }

 // 使用 scanner 扫描每一个 basePackage, 获取其中的 feign 客户端定义, 
 // 也就是 @FeignClient 定义的那些接口
 for (String basePackage : basePackages) {
  Set candidateComponents = scanner
    .findCandidateComponents(basePackage);
  for (BeanDefinition candidateComponent : candidateComponents) {
   if (candidateComponent instanceof AnnotatedBeanDefinition) {
    // verify annotated class is an interface
    AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
    AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
    Assert.isTrue(annotationMetadata.isInterface(),
      "@FeignClient can only be specified on an interface");

    // 获取所定义的feign客户端接口上的注解@FeignClient属性
    Map attributes = annotationMetadata
      .getAnnotationAttributes(
        FeignClient.class.getCanonicalName());

    String name = getClientName(attributes);
    // 将所定义的feign客户端上的配置属性作为一个bean注册到容器   
    // 此方法的逻辑我们上面已经分析过
    registerClientConfiguration(registry, name,
      attributes.get("configuration"));

    // 将所定义的feign客户端作为一个bean注册到容器:
    // bean 定义类型 : GenericBeanDefinition
    //  bean class : FeignClientFactoryBean
    //  autowire 模式 : 根据类型绑定
    // @FeignClient注解中的url,path,fallback等属性会设置为bean定义的属性
    registerFeignClient(registry, annotationMetadata, attributes);
   }
  }
 }
}   

// 辅助工具类,从@EnableFeignClients注解属性中获取basePackages属性:
// 参考以下@EnableFeignClients注解属性 :
// 1. value
// 2. basePackages
// 3. basePackageClasses
// 4. 配置类所在的包
// 参数 importingClassMetadata : 使用注解@EnableFeignClients的配置类的元数据
protected Set getBasePackages(AnnotationMetadata importingClassMetadata) {
 // 注解@EnableFeignClients的属性
 Map attributes = importingClassMetadata
   .getAnnotationAttributes(EnableFeignClients.class.getCanonicalName());

 Set basePackages = new HashSet<>();
 for (String pkg : (String[]) attributes.get("value")) {
  if (StringUtils.hasText(pkg)) {
   basePackages.add(pkg);
  }
 }
 for (String pkg : (String[]) attributes.get("basePackages")) {
  if (StringUtils.hasText(pkg)) {
   basePackages.add(pkg);
  }
 }
 for (Class> clazz : (Class[]) attributes.get("basePackageClasses")) {
  basePackages.add(ClassUtils.getPackageName(clazz));
 }

 if (basePackages.isEmpty()) {
  basePackages.add(
    ClassUtils.getPackageName(importingClassMetadata.getClassName()));
 }
 return basePackages;
}

#registerFeignClients 最终注册 feign 客户端配置的动作交给 #registerClientConfiguration 完成,而注册 feign 客户端的动作交给 #registerFeignClient 方法完成。

#registerFeignClient – 注册一个 feign 客户端

// 将所定义的feign客户端作为一个bean注册到容器:
// bean 定义类型 : GenericBeanDefinition
//  bean class : FeignClientFactoryBean -- 这是一个工厂bean,而不是最终bean实例的class
//  autowire 模式 : 根据类型绑定
// @FeignClient注解中的url,path,fallback等属性会设置为bean定义的属性
// 参数 registry : Spring 容器
// 参数 annotationMetadata : @FeignClient所注解的接口上的注解元数据
// 参数 attributes : @FeignClient 注解属性信息
private void registerFeignClient(BeanDefinitionRegistry registry,
  AnnotationMetadata annotationMetadata, Map attributes)
 
{
 String className = annotationMetadata.getClassName();
 BeanDefinitionBuilder definition = BeanDefinitionBuilder
   .genericBeanDefinition(FeignClientFactoryBean.class);
 validate(attributes);
 definition.addPropertyValue("url", getUrl(attributes));
 definition.addPropertyValue("path", getPath(attributes));
 String name = getName(attributes);
 definition.addPropertyValue("name", name);
 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 = name + "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;
 }

 BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
   new String[] { alias });
 BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}

从上面的代码分析可知, FeignClientsRegistrar 的主要作用如下 :

  1. 注册缺省 feign 客户端配置 bean 定义;

  2. 对于每个 @FeignClient 注解的 feign 客户端定义 :

    1. 注册一个针对该 feign 客户端的配置 bean 定义;

    2. 注册该 feign 客户端 bean 定义,指定生成 bean 实例采用工厂类 FeignClientFactoryBean ;

而且,上述功能实现在类方法 FeignClientsRegistrar#registerBeanDefinitions 中,这是接口 ImportBeanDefinitionRegistrar 所定义的方法。该方法会在 @EnableFeignClients 注解被处理时执行。具体的执行时调用栈如下所示:

    AbstractApplicationContext#invokeBeanFactoryPostProcessors
    => PostProcessorRegistrationDelegate#invokeBeanFactoryPostProcessors
    => foreach BeanDefinitionRegistryPostProcessor : #postProcessBeanDefinitionRegistry
    => ConfigurationClassPostProcessor#postProcessBeanDefinitionRegistry
    => #processConfigBeanDefinitions
    => ConfigurationClassBeanDefinitionReader#loadBeanDefinitions
    => foreach ConfigurationClass : #loadBeanDefinitionsForConfigurationClass
    => #loadBeanDefinitionsFromRegistrars
    => foreach ImportBeanDefinitionRegistrar : #registerBeanDefinitions
    => FeignClientsRegistrar#registerBeanDefinitions

FeignClientFactoryBean 生成 feign 客户端代理对象

基于上面的分析,我们可以得知,开发人员所定义的 feign 客户端和相关配置会以 bean 定义的形式注册到 bean 容器中,这样当使用 @Autowired 注入一个 feign 客户端时,容器会使用工厂类 FeignClientFactoryBean 为其生成一个实例。下面我们来看其具体工作过程。

FeignClientFactoryBean#getObject 生成 feign 客户端代理对象

// 该方法由接口FactoryBean约定
@Override
public Object getObject() throws Exception {
 return getTarget();
}


 getTarget() {
    //  从应用上下文中获取创建 feign 客户端的上下文对象 FeignContext
    // FeignContext 针对每个feign客户端定义会生成一个不同的 AnnotationConfigApplicationContext,
    // 这些应用上下文的parent都设置为当前应用的主应用上下文
    // 参考 : FeignAutoConfiguration
 FeignContext context = applicationContext.getBean(FeignContext.class);
    // 为目标feign客户端对象构建一个 builder,该builder最终生成的目标feign客户端是一个
    // 动态代理,使用 InvocationHandler :ReflectiveFeign$FeignInvocationHandler
 Feign.Builder builder = feign(context);

 if (!StringUtils.hasText(this.url)) {
       // @FeignClient 属性 url 属性没有指定的情况         
       // 根据属性 name , path 拼装一个 url,
       // 这种通常是需要在多个服务节点之间进行负载均衡的情况
  if (!this.name.startsWith("http")) {
   url = "http://" + this.name;
  }
  else {
   url = this.name;
  }
       // 方法cleanPath()加工属性path,使其以/开头,不以/结尾
  url += cleanPath();
       // 这里形成的url格式类似 :  http://test-service/test
       // 其中 test-service 是服务名,不是服务所在节点的IP,主机名或者域名
       
       // 函数 loadBalance 做如下动作 :
       // 1. 将builder和一个LoadBalancerFeignClient bean实例关联起来
       // 2. 使用一个HystrixTargeter将builder和一个 HardCodedTarget bean实例关联起来
       // 这里 HardCodedTarget 表示对应 url 为 http://test-service/test 的远程服务(可能
       // 包含多个服务方法)
       // 3. 生成最终的feign client 实例 : ReflectiveFeign$FeignInvocationHandler 的动态代理对象,
       // 使用 InvocationHandler :ReflectiveFeign$FeignInvocationHandler。
       // 每个远程服务方法会对应到一个@FeignClient注解的接口方法上(依据方法上的注解进行匹配)
  return (T) loadBalance(builder, context, new HardCodedTarget<>(this.type,
    this.name, url));
 }
    
    // @FeignClient 属性 url 属性被指定的情况 
    // 这种通常是明确指出了服务节点的url的情况,实际上不需要负载均衡
 if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
  this.url = "http://" + this.url;
 }
 String url = this.url + cleanPath();
    // 将builder和一个LoadBalancerFeignClient bean实例关联起来
 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,所以这里不需要负载均衡,
   // 所以这里尽管client是LoadBalancerFeignClient,所以
   // 实际上可以获取其所代理的对象作为最终的client,
   // 相当于去掉了LoadBalancerFeignClient这层的代理功能
   client = ((LoadBalancerFeignClient)client).getDelegate();
  }
  builder.client(client);
 }
    // 使用一个HystrixTargeter将builder和一个 HardCodedTarget bean实例关联起来
 Targeter targeter = get(context, Targeter.class);
    // 生成最终的feign client 实例 : ReflectiveFeign$FeignInvocationHandler 的动态代理对象,
    // 使用 InvocationHandler :ReflectiveFeign$FeignInvocationHandler。
    // 每个远程服务方法会对应到 一个@FeignClient注解的接口方法上(依据方法上的注解进行匹配)        
 return (T) targeter.target(this, builder, context, new HardCodedTarget<>(
   this.type, this.name, url));
}

方法 FeignClientFactoryBean#feign – 创建 feign 客户端构建器

protected Feign.Builder feign(FeignContext context) {
  FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);
  Logger logger = loggerFactory.create(this.type);

  // 从上下文获取一个 Feign.Builder 上,
  // 并从上下文获得 Encoder, Decoder, Contract 设置到该 builder 上
  Feign.Builder builder = get(context, Feign.Builder.class)
    // required values
    .logger(logger)
    .encoder(get(contextEncoder.class))






请到「今天看啥」查看全文