Feign 系列(05)Spring Cloud OpenFeign 源码解析

Posted binarylei

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Feign 系列(05)Spring Cloud OpenFeign 源码解析相关的知识,希望对你有一定的参考价值。

Feign 系列(05)Spring Cloud OpenFeign 源码解析

Spring Cloud 系列目录(https://www.cnblogs.com/binarylei/p/11563952.html#feign)

上一篇 文章中我们分析 Feign 参数解析的整个流程,Feign 原生已经支持 Feign、JAX-RS 1/2 声明式规范,本文着重关注 Spring Cloud 是如果整合 OpenFeign 的,使之支持 Spring MVC?

1. Spring Cloud OpenFeign 最简使用

1.1 引入 maven

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

1.2 @EnableFeignClients 注解扫描包

@SpringBootApplication
@EnableFeignClients  // 默认扫描 FeignApplication.class 包下 @FeignClient 注解
public class FeignApplication {

    public static void main(String[] args) {
        SpringApplication.run(FeignApplication.class);
    }
}

1.3 @FeignClient 配置

@FeignClient(value = "echo-server",url = "http://127.0.0.1:10010")
public interface EchoService {

    @GetMapping("/echo/{msg}")
    String echo(@PathVariable String msg);
}

总结: 至此,可以像使用普通接口一样调用 http 了

2. Feign 整体装配流程分析

图1:Feign 整体装配流程
sequenceDiagram participant FeignContext participant @EnableFeignClients participant FeignClientsRegistrar participant FeignClientFactoryBean participant DefaultTargeter participant Feign.Builder FeignContext ->> FeignContext: 1. FeignAutoConfiguration 自动注入 @EnableFeignClients ->> FeignClientsRegistrar: 2. import FeignClientsRegistrar ->> FeignClientFactoryBean: 3. 扫描 @FeignClient 注解 FeignClientFactoryBean ->> FeignClientFactoryBean: 4. getObject FeignContext ->> Feign.Builder: 5. 获取Feign.Builder:feign(context) FeignContext ->> DefaultTargeter: 6. 获取DefaultTargeter:get(context, Targeter.class) DefaultTargeter ->> Feign.Builder: 7. 创建代理对象:feign.target(target)

总结: OpenFeign 装配有两个入口:

  1. @EnableAutoConfiguration 自动装配(spring.factories)

    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\\
    org.springframework.cloud.openfeign.ribbon.FeignRibbonClientAutoConfiguration,\\
    org.springframework.cloud.openfeign.FeignAutoConfiguration
    
    • FeignAutoConfiguration 自动装配 FeignContext 和 Targeter,以及 Client 配置。
      • FeignContext 是每个 FeignClient 的装配上下文,默认的配置是 FeignClientsConfiguration
      • Targeter 有两种实现:一是 DefaultTargeter,直接调用 Feign.Builder; 二是 HystrixTargeter,调用 HystrixFeign.Builder,开启熔断。
      • Client :自动装配 ApacheHttpClient,OkHttpClient,装配条件不满足,默认是 Client.Default。但这些 Client 都没有实现负载均衡。
    • FeignRibbonClientAutoConfiguration 实现负载均衡,负载均衡是在 Client 这一层实现的。
      • HttpClientFeignLoadBalancedConfiguration ApacheHttpClient 实现负载均衡
      • OkHttpFeignLoadBalancedConfiguration OkHttpClient实现负载均衡
      • DefaultFeignLoadBalancedConfiguration Client.Default实现负载均衡
  2. @EnableFeignClients 自动扫描

    @EnableFeignClients 注入 FeignClientsRegistrar,FeignClientsRegistrar 开启自动扫描,将包下 @FeignClient 标注的接口包装成 FeignClientFactoryBean 对象,最终通过 Feign.Builder 生成该接口的代理对象。而 Feign.Builder 的默认配置是 FeignClientsConfiguration,是在 FeignAutoConfiguration 自动注入的。

注意: 熔断与限流是 FeignAutoConfiguration 通过注入 HystrixTargeter 完成的,而负载均衡是 FeignRibbonClientAutoConfiguration 注入的。

3. Feign 自动装配

3.1 FeignAutoConfiguration

3.1.1 FeignContext

// FeignAutoConfiguration 自动装配 FeignContext
@Autowired(required = false)
private List<FeignClientSpecification> configurations = new ArrayList<>();

@Bean
public FeignContext feignContext() {
    FeignContext context = new FeignContext();
    context.setConfigurations(this.configurations);
    return context;
}

FeignContext 是每个 Feign 客户端配置的上下文环境,会将初始化一个 Feign 的组件都在一个子 ApplicationContext 中初始化,从而隔离不同的 Feign 客户端。换名话说,不同名称的 @FeignClient 都会初始化一个子的 Spring 容器。

注意: 每个 Feign 客户端除了默认 FeignClientsConfiguration,还可以自定义配置类 FeignClientSpecification,这些配置是如何注入的,会在 @EnableFeignClients 和 @FeignClient 源码分析时具体说明。

public class FeignContext extends NamedContextFactory<FeignClientSpecification> {
	public FeignContext() {
		super(FeignClientsConfiguration.class, "feign", "feign.client.name");
	}
}

总结: FeignClientsConfiguration 是 Feign 的默认配置,可以通过 @EnableFeignClients 和 @FeignClient 修改默认配置。FeignClientsConfiguration 主要配置如下:

@Configuration
public class FeignClientsConfiguration {
    @Bean
	@ConditionalOnMissingBean
	public Decoder feignDecoder() {
		return new OptionalDecoder(
				new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)));
	}
    // 适配 Spring MVC 注解
	@Bean
	@ConditionalOnMissingBean
	public Contract feignContract(ConversionService feignConversionService) {
		return new SpringMvcContract(this.parameterProcessors, feignConversionService);
	}
    
    @Bean
	@Scope("prototype")
	@ConditionalOnMissingBean
	public Feign.Builder feignBuilder(Retryer retryer) {
		return Feign.builder().retryer(retryer);
	}
}

3.1.2 Targeter:是否熔断

// FeignAutoConfiguration 自动装配 Targeter
@Configuration
@ConditionalOnClass(name = "feign.hystrix.HystrixFeign")
protected static class HystrixFeignTargeterConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public Targeter feignTargeter() {
        return new HystrixTargeter();
    }
}

@Configuration
@ConditionalOnMissingClass("feign.hystrix.HystrixFeign")
protected static class DefaultFeignTargeterConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public Targeter feignTargeter() {
        return new DefaultTargeter();
    }
}

总结: Targeter 有两种实现:一是 DefaultTargeter,直接调用 Feign.Builder; 二是 HystrixTargeter,调用 HystrixFeign.Builder,开启熔断。

class DefaultTargeter implements Targeter {
	@Override
	public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign,
			FeignContext context, Target.HardCodedTarget<T> target) {
		return feign.target(target);
	}
}

3.1.3 Client

// FeignClientsConfiguration 不实现负载均衡的 Client。OkHttpFeignConfiguration 类似
@Configuration
@ConditionalOnClass(ApacheHttpClient.class)
@ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
@ConditionalOnMissingBean(CloseableHttpClient.class)
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
protected static class HttpClientFeignConfiguration {
    @Bean
    @ConditionalOnMissingBean(Client.class)
    public Client feignClient(HttpClient httpClient) {
        return new ApacheHttpClient(httpClient);
    }
}

从装配条件就可以知道,HttpClientFeign 没有实现负载均衡。

3.2 FeignRibbonClientAutoConfiguration

@ConditionalOnClass({ ILoadBalancer.class, Feign.class })
@Configuration
@AutoConfigureBefore(FeignAutoConfiguration.class)
@EnableConfigurationProperties({ FeignHttpClientProperties.class })
@Import({ HttpClientFeignLoadBalancedConfiguration.class,
		OkHttpFeignLoadBalancedConfiguration.class,
		DefaultFeignLoadBalancedConfiguration.class })
public class FeignRibbonClientAutoConfiguration {
    // CachingSpringLoadBalancerFactory 用于组装 FeignLoadBalancer
    @Bean
	@Primary
	@ConditionalOnMissingBean
	@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
	public CachingSpringLoadBalancerFactory cachingLBClientFactory(
			SpringClientFactory factory) {
		return new CachingSpringLoadBalancerFactory(factory);
	}
}

总结: FeignRibbonClientAutoConfiguration 实现了负载均衡。SpringClientFactory 实际是 RibbonClientFactory,功能等同于 FeignContext,用于装配 Ribbon 的基本组件,SpringClientFactory 这个名称太误导人了。

注意在 FeignRibbonClientAutoConfiguration 之上 import 了三个配置类,HttpClientFeignLoadBalancedConfiguration、OkHttpFeignLoadBalancedConfiguration、DefaultFeignLoadBalancedConfiguration。

@Configuration
class DefaultFeignLoadBalancedConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
                              SpringClientFactory clientFactory) {
        // cachingFactory 用于组装 FeignLoadBalancer
        return new LoadBalancerFeignClient(new Client.Default(null, null), 
                   cachingFactory, clientFactory);
    }
}

4. 源码分析

@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
    String[] basePackages() default {};			 // 包扫描路径
    Class<?>[] defaultConfiguration() default {};// 默认配置
}

总结: 从 @EnableFeignClients 的属性大致可以推断出,FeignClientsRegistrar 会扫描 basePackages 包下的所有 @FeignClient 注解的类,用 Feign.Builder 生成动态代理的 Bean 注入到 Spring 容器中。是不是这样呢?我们看一下。

4.2.1 FeignClientsRegistrar

class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, 	
	@Override
	public void registerBeanDefinitions(AnnotationMetadata metadata,
			BeanDefinitionRegistry registry) {
        // 注册 @EnableFeignClients#defaultConfiguration 默认配置类
		registerDefaultConfiguration(metadata, registry);
        // 扫描所有的 @FeignClient 注解
		registerFeignClients(metadata, registry);
	}
}

(1) registerDefaultConfiguration

registerDefaultConfiguration 最终调用 registerClientConfiguration(registry, name,defaultAttrs.get("defaultConfiguration")) 将 @EnableFeignClients 的默认配置注入到容器中。

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

总结: 还记得 FeignAutoConfiguration 自动装配 FeignContext 时的 List<FeignClientSpecification> configurations 吗,就是将 @EnableFeignClients 和 @FeignClient 的 configuration 属性注册到 Spring 的容器中。

(2) registerFeignClients

registerFeignClients 将 @FeignClient 标注的接口装配成 FeignClientFactoryBean 注入到容器中。FeignClientFactoryBean#getObject 最终会调用 feign.target 生成最终的代理对象。

public void registerFeignClients(AnnotationMetadata metadata,
                                 BeanDefinitionRegistry registry) {
    ClassPathScanningCandidateComponentProvider scanner = getScanner();
    scanner.setResourceLoader(this.resourceLoader);

    // 扫描条件: @FeignClient
    Set<String> basePackages;
    Map<String, Object> attrs = metadata
        .getAnnotationAttributes(EnableFeignClients.class.getName());
    AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
        FeignClient.class);
        scanner.addIncludeFilter(annotationTypeFilter);
        basePackages = getBasePackages(metadata);
    ...

    // 扫描 basePackage 下的 @FeignClient 注解
    for (String basePackage : basePackages) {
        Set<BeanDefinition> candidateComponents = scanner
            .findCandidateComponents(basePackage);
        for (BeanDefinition candidateComponent : candidateComponents) {
            if (candidateComponent instanceof AnnotatedBeanDefinition) {
                AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
                AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
                Map<String, Object> attributes = annotationMetadata
                    .getAnnotationAttributes(
                    FeignClient.class.getCanonicalName());
                String name = getClientName(attributes);
                // 注册 @FeignClient 的配置
                registerClientConfiguration(registry, name,
                                            attributes.get("configuration"));
                // 将该接口通过 FeignClientFactoryBean 注入到容器中
                registerFeignClient(registry, annotationMetadata, attributes);
            }
        }
    }
}

// 注册 FeignClientFactoryBean
private void registerFeignClient(BeanDefinitionRegistry registry,
			AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
    String className = annotationMetadata.getClassName();
    BeanDefinitionBuilder definition = BeanDefinitionBuilder
        .genericBeanDefinition(FeignClientFactoryBean.class);
    ...
}

总结: 至此,我们总算看到 Bean 的注册了,但还没有看到 feign.target 生成动态代理。我们知道 FeignClientFactoryBean 是 Spring 的 Bean 工厂类,通过其 getObject 方法可以获取真正的 Bean。所以在 getObject 中一定可以看到类似 feign.target 的代码。

4.2 FeignClientFactoryBean

@Override
public Object getObject() throws Exception {
    return getTarget();
}

<T> T getTarget() {
    // 1. FeignAutoConfiguration 自动装配 FeignContext
    FeignContext context = this.applicationContext.getBean(FeignContext.class);
    Feign.Builder builder = feign(context);
    
	// 2. url不存在,则一定是负载均衡
    if (!StringUtils.hasText(this.url)) {
        if (!this.name.startsWith("http")) {
            this.url = "http://" + this.name;
        }
        else {
            this.url = this.name;
        }
        this.url += cleanPath();
        return (T) loadBalance(builder, context,
                               new HardCodedTarget<>(this.type, this.name, this.url));
    }
    
    // 3. 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) {
            client = ((LoadBalancerFeignClient) client).getDelegate();
        }
        builder.client(client);
    }
    // 4 FeignAutoConfiguration 自动装配 Targeter
    Targeter targeter = get(context, Targeter.class);
    // 调用 feign.target 生成动态代理
    return (T) targeter.target(this, builder, context,
                               new HardCodedTarget<>(this.type, this.name, url));
}

总结: 至此,@FeignClient 标注的接口,最终通过 targeter.target 生成最终的代理对象。在 FeignClientFactoryBean 中有 2 个重要的对象 FeignClient 和 Targeter,这两个对象都是通过 FeignAutoConfiguration 自动注入的。

  1. FeignClient 是所有 @FeignClient 的上下文环境,管理了 Feign.Builder 的所有配置。根据 @FeignClient(同一个服务)的 contextId 区分不同的上下文环境,每个环境都是一个子 Spring 容器,从而直到隔离不同 @FeignClient 的目的。@FeignClient 的默认配置是 FeignClientsConfiguration,但同时也可以通过 @EnableFeignClients 和 @FeignClient 的 configuration 属性进行修改。

    // NamedContextFactory#getContext 会根据 name 创建一个 ApplicationContext
    // FeignContext.getInstance(this.contextId, type),在本文中就是根据 contextId 区分
    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);
    }
    
  2. Targeter 可以实现整合 Hystrix,实现熔断与限流。


每天用心记录一点点。内容也许不重要,但习惯很重要!

以上是关于Feign 系列(05)Spring Cloud OpenFeign 源码解析的主要内容,如果未能解决你的问题,请参考以下文章

Spring Cloud Alibaba系列使用feign进行服务调用

SpringCloud系列——Feign 服务调用

Spring Cloud Alibaba微服务调用组件Feign原理+实战

Spring Cloud声名式服务调用:Feign的使用 (第一部分)

Spring Cloud核心组件:Ribbon,Feign

参数为空时的Spring Cloud feign行为