盘点 Cloud : Feign 初始化配置流程

1,586 阅读5分钟

这是我参与更文挑战的第5天,活动详情查看: 更文挑战

总文档 :文章目录
Github : github.com/black-ant

一 . 前言

文章目的 :

  • Feign 主要流程源码分析
  • Feign 的要点分析
  • Feign 的设计思路及扩散思考

二 . 源码梳理

以一个最基础的 OpenFeign 的案例为例 , 我们在使用时通常会有如下操作 :

// Step 1: 开启 Feign 客户端
@EnableFeignClients
@SpringBootApplication
public class ComGangCloudTemplateOrderApplication {..........}


// Step 2 : 准备 FeignClient 对象
@Component
@FeignClient("product-server")
public interface ProductFeignClient {
    @GetMapping("/template/get")
    CloudTemplateEntity get(@RequestParam("desc") String desc);
}

// Step 3 : 调用 FeignClient 对象
@Autowired
private ProductFeignClient productFeignClient;
productFeignClient.get("order-server")

光看这三个步骤 , 大概可以得出几个问题 :

  1. EnableFeignClients 的作用 ?
  2. @FeignClient 的扫描

2.1 通过 @EnableFeignClients 开启 FeignClients

主要的配置集中在 FeignClientsRegistrar 中 , 主要包括以下内容 :

首先看一下 FeignClientsRegistrar 的调用流程:

  • Step 1 : Bean 加载时调用 registerBeanDefinitions 完成 BeanDefinition 的注册
  • Step 2 : registerDefaultConfiguration 注册 config
  • Step 3 : 扫描 FeignClient , 并且进行注册
  • Step 4 : 注册扫描的所有的 FeignClient

Step 1 : Bean 加载时调用 registerBeanDefinitions 完成 BeanDefinition 的注册

public void registerBeanDefinitions(AnnotationMetadata metadata,
			BeanDefinitionRegistry registry) {
    // 其中涉及到2个主要的步骤 : 
    // 1.注册Configuration
    registerDefaultConfiguration(metadata, registry);
    // 2. 注册 Feign Client
    registerFeignClients(metadata, registry);
}

Step 2 : registerDefaultConfiguration 注册 config

// 1. 获取注解上面的属性 -> PS:001
Map<String, Object> defaultAttrs = 
    metadata.getAnnotationAttributes(EnableFeignClients.class.getName(), true);

// 2. 注册 BeanDefinitionBuilder (PS : 这一步骤实际上构建了一个 BeanDefinitionBuilder)
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
    Object configuration) {
    
    // BeanDefinitionBuilder是使用构建器模式构建BeanDefinitions
    BeanDefinitionBuilder builder = BeanDefinitionBuilder
        .genericBeanDefinition(FeignClientSpecification.class);
    // name : default.com.gang.cloud.template.demo.ComGangCloudTemplateOrderApplication
    builder.addConstructorArgValue(name);
    builder.addConstructorArgValue(configuration);
    // default.com.gang.cloud.template.demo.ComGangCloudTemplateOrderApplication.FeignClientSpecification
    registry.registerBeanDefinition(
        name + "." + FeignClientSpecification.class.getSimpleName(),builder.getBeanDefinition()
    );
        
}


PS:001 defaultAttrs 参数

image.png

Step 3 : 扫描 FeignClient

整体大纲就是 :

  1. 从 EnableFeignClients 获取属性 attrs
  2. 通过属性 attrs 获取 基础扫描路径 basePackages
  3. 扫描 basePackages 下面的所有标注 @Component 的类
  4. 获取其中的 @FeignClient
  5. 对注解标注的类进行 registry
// 注解类的使用主要是 FeignClientsRegistrar
C05- FeignClientsRegistrar(AnnotationMetadata metadata,BeanDefinitionRegistry registry)
    M5_01- registerFeignClients : FeignClient 核心方式
        - 准备对象 ClassPathScanningCandidateComponentProvider
        - 获取标注了 EnableFeignClients 的 Map 集合
        - 构建一个 AnnotationTypeFilter 
            ?- 为什么不和上面一样使用 getAnnotationAttributes 直接获取相关类 -> PS:M5_01_01
         - getBasePackages 获取需要扫描的路径集合 Set<String>->LV001 
        FOR- 循环路径集合 : LV001
            - findCandidateComponents 获取对应的 Set<BeanDefinition>->LV002
                 ?- 注意 , 这里会通过 findCandidateComponents 查找路径下标注了 Component 的类型 ->  PS:M5_01_02
           FOR- 循环 BeanDefinition : LV002
               - 获取 FeignClient 的属性 Map<String, Object> ->LV003
               - registerClientConfiguration 注册配置信息
               - registerFeignClient 注册当前 FeignClient 
    M5_02- registerFeignClient(BeanDefinitionRegistry registry,AnnotationMetadata annotationMetadata, Map<String, Object> attributes)
        ?- 核心作用就是构建一个 BeanDefined , 其中又主要可以分为4步 -> PS:M5_02_01
         1- BeanDefinitionBuilder.genericBeanDefinition 构建一个 BeanDefinitionBuilder
         2- definition.getBeanDefinition() 构建一个 AbstractBeanDefinition
         3- new BeanDefinitionHolder 构建一个 BeanDefinitionHolder
         4- BeanDefinitionReaderUtils.registerBeanDefinition 注入 Bean  

以上是主流程 ,但是我们还是可以看一下其中的一点小细节:

// PS : 其中省略了部分代码 , 想看完整版的可以看源码
public void registerFeignClients(AnnotationMetadata metadata,
			BeanDefinitionRegistry registry) {
    // 准备 scan 对象用于扫描
    ClassPathScanningCandidateComponentProvider scanner = getScanner();
    scanner.setResourceLoader(this.resourceLoader);

    // 这一句很重要 , 这是个用于 Scan 的排除筛选器 ,他会屏蔽调无用的 Bean
    AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
				FeignClient.class);
                                
    if (clients == null || clients.length == 0) {
        // 添加排除筛选器 -> PS:0002
        scanner.addIncludeFilter(annotationTypeFilter);
        basePackages = getBasePackages(metadata);
    } else {
        // 省略 ,同样是为了进行特殊的匹配
    }

    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();
                // 获取注解属性 -> PS:0003
                Map<String, Object> attributes = annotationMetadata
                    .getAnnotationAttributes(FeignClient.class.getCanonicalName());
                // 注册 FeignClient
                registerFeignClient(registry, annotationMetadata, attributes);
            }
        }
    }
}
        
// PS:0002 排除筛选器的使用
protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
    for (TypeFilter tf : this.includeFilters) {
        //..........
    }
} 
        

Step 3 : 扫描 FeignClient


private void registerFeignClient(BeanDefinitionRegistry registry,
			AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
    String className = annotationMetadata.getClassName();
    
    // 准备 BeanDefinitionBuilder -> org.springframework.cloud.openfeign.FeignClientFactoryBean
    BeanDefinitionBuilder definition = BeanDefinitionBuilder
        .genericBeanDefinition(FeignClientFactoryBean.class);
    // 校验属性是否合法
    validate(attributes);
                
    // 省略所有的 addPropertyValue 操作 ,此操作为 definition 添加属性

    String alias = contextId + "FeignClient";
    AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
    beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);

    // 由于有一个默认 BeanDefinition  , 此处通过 primary 进行覆盖
    boolean primary = (Boolean) attributes.get("primary");
    beanDefinition.setPrimary(primary);

    BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
        new String[] { alias });
    // 向给定的bean工厂注册给定的bean定义    
    BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}

PS : 这个对象在创建Bean过程中会被调用 ,我们后面再说!!!

2.2 FeignAutoConfiguration 的配置

Feign 其实是支持 OKHttp 方式调用的 ,该方法在 FeignAutoConfiguration 中进行配置 , 该配置类中提供了2个连接框架 : HttpClientFeignConfiguration / OkHttpFeignConfiguration

// HttpClientFeignConfiguration 的配置项
@Bean
@ConditionalOnMissingBean(HttpClientConnectionManager.class)
public HttpClientConnectionManager connectionManager(ApacheHttpClientConnectionManagerFactory connectionManagerFactory,FeignHttpClientProperties httpClientProperties) {
    final HttpClientConnectionManager connectionManager = connectionManagerFactory
        .newConnectionManager(httpClientProperties.isDisableSslValidation(),
        // 最大连接数
        httpClientProperties.getMaxConnections(),
        httpClientProperties.getMaxConnectionsPerRoute(),
        // 连接存活时间和单位
        httpClientProperties.getTimeToLive(),
        httpClientProperties.getTimeToLiveUnit(),
        this.registryBuilder);
        
        this.connectionManagerTimer.schedule(new TimerTask() {
                @Override
                public void run() {
                        connectionManager.closeExpiredConnections();
                }
        }, 30000, httpClientProperties.getConnectionTimerRepeat());
        
        return connectionManager;
}

@Bean
public CloseableHttpClient httpClient(ApacheHttpClientFactory httpClientFactory,
    HttpClientConnectionManager httpClientConnectionManager,
    FeignHttpClientProperties httpClientProperties) {
    
    RequestConfig defaultRequestConfig = RequestConfig.custom()
        .setConnectTimeout(httpClientProperties.getConnectionTimeout())
        .setRedirectsEnabled(httpClientProperties.isFollowRedirects())
        .build();
        
        this.httpClient = httpClientFactory.createBuilder()
            .setConnectionManager(httpClientConnectionManager)
            .setDefaultRequestConfig(defaultRequestConfig).build();
            
        return this.httpClient;
}


// OkHttpFeignConfiguration 的配置项
@Bean
@ConditionalOnMissingBean(ConnectionPool.class)
public ConnectionPool httpClientConnectionPool(
    FeignHttpClientProperties httpClientProperties,
    OkHttpClientConnectionPoolFactory connectionPoolFactory) {
    // OKHttp 连接池属性
    Integer maxTotalConnections = httpClientProperties.getMaxConnections();
    Long timeToLive = httpClientProperties.getTimeToLive();
    TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit();
    return connectionPoolFactory.create(maxTotalConnections, timeToLive, ttlUnit);
}

@Bean
public okhttp3.OkHttpClient client(OkHttpClientFactory httpClientFactory,
    ConnectionPool connectionPool,
    FeignHttpClientProperties httpClientProperties) {
    
    Boolean followRedirects = httpClientProperties.isFollowRedirects();
    Integer connectTimeout = httpClientProperties.getConnectionTimeout();
    Boolean disableSslValidation = httpClientProperties.isDisableSslValidation();
    this.okHttpClient = httpClientFactory.createBuilder(disableSslValidation)
        .connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
        .followRedirects(followRedirects).connectionPool(connectionPool)
        .build();
        
    return this.okHttpClient;
}

那么问题来了 : 如何切换到 OKHttp 呢?

PS : 网上有一种方法 ,通过配置 okhttp3.OkHttpClient 的方法 , 但是经过个人测试 , 可能由于版本不同会出现问题

// 如果细看源码 , 会发现存在2个 OkHttpFeignConfiguration 
//一个在 FeignAutoConfiguration 内部 , 另外一个存在于 org.springframework.cloud.openfeign.clientconfig 包下

// 疑点 : 点开FeignAutoConfiguration$OkHttpFeignConfiguration 上面有多个 Conditional

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(OkHttpClient.class)
@ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
@ConditionalOnMissingBean(okhttp3.OkHttpClient.class)
@ConditionalOnProperty("feign.okhttp.enabled")
protected static class OkHttpFeignConfiguration {
    //............
}

// [Pro] : 通过 OnClassCondition 测试的时候就会发现 , 由于 ILoadBalancer 存在 , 则不会加载该类下的配置

而在 org.springframework.cloud.openfeign.clientconfig 包下的配置中 , 则有如下要求
@ConditionalOnMissingBean(okhttp3.OkHttpClient.class)

// 所以 , 已知的那种配置实际上会导致2个配置项都不会走



//!!! 那么应该怎么配置 ?



// Step 1 :添加 eign-okhttp 相关配置
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
</dependency>


// Step 2 : Application 上导入配置文件 
@Import(value = {OkHttpFeignConfiguration.class})


// Step 3 : config 中注册 Client
@Configuration
@ConditionalOnClass(Feign.class)
@AutoConfigureAfter(value = {FeignAutoConfiguration.class, OkHttpFeignConfiguration.class})
public class FeignOkHttpConfig {

    @Bean
    @ConditionalOnMissingBean(Client.class)
    public Client feignClient(okhttp3.OkHttpClient client) {
        // 这里还有相关配置 ,暂时省略 , 写流程的时候补上
        return new OkHttpLoadBalancingClient(....);
    }
}


总结

Feign 的配置篇基本上就说完了 , 后面会说明一个 FeignBean 的创建和一个 Invoke 流程

附录

PS:0003 attributes 中包含哪些属性?

image.png

C- FeignClient
    M- String value() default ""
    M- String serviceId() default ""
    M- String contextId() default "" : 如果存在,它将用作bean名而不是名称,但不会用作服务id。
    M- String name() default ""
    M- String qualifier() default ""
    M- String url() default ""
    M- boolean decode404() default false
    M- Class<?>[] configuration() : 为虚客户端定制的配置类。
    M- Class<?> fallback()
    M- Class<?> fallbackFactory()
    M- String path() default ""
    M- boolean primary() default true
        

常见的 FeignClient 配置

// 覆盖默认配置
@FeignClient(contextId = "fooClient", name = "stores", configuration = FooConfiguration.class)
link : https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/#spring-cloud-feign-overriding-defaults    
    
// 获取回退的原因
@FeignClient(name = "test", url = "http://localhost:${server.port}/", fallback = Fallback.class)
    
// 获取触发回退的原因
@FeignClient(name = "testClientWithFactory", url = "http://localhost:${server.port}/",fallbackFactory = TestFallbackFactory.class)

// 配置主 Bean
// PS : 当使用 Feign 和 Spring Cloud CircuitBreaker 回退时,在同一类型的 ApplicationContext 中有多个 bean    
@FeignClient(name = "hello", primary = false)    

Application 配置篇

feign:
    client:
        config:
            feignName:
                connectTimeout: 5000
                readTimeout: 5000
                loggerLevel: full
                errorDecoder: com.example.SimpleErrorDecoder
                retryer: com.example.SimpleRetryer
                defaultQueryParameters:
                    query: queryValue
                defaultRequestHeaders:
                    header: headerValue
                requestInterceptors:
                    - com.example.FooRequestInterceptor
                    - com.example.BarRequestInterceptor
                decode404: false
                encoder: com.example.SimpleEncoder
                decoder: com.example.SimpleDecoder
                contract: com.example.SimpleContract
                capabilities:
                    - com.example.FooCapability
                    - com.example.BarCapability
                metrics.enabled: false
                
                
// 已经配套的 Bean 类 ,Debug get/set 就可以看到使用的节点
@ConfigurationProperties(prefix = "feign.httpclient")
public class FeignHttpClientProperties {

	// 禁用SSL验证的默认值
	public static final boolean DEFAULT_DISABLE_SSL_VALIDATION = false;

        // 最大od连接数的缺省值
	public static final int DEFAULT_MAX_CONNECTIONS = 200;

	// 每条路由的最大连接数的缺省值
	public static final int DEFAULT_MAX_CONNECTIONS_PER_ROUTE = 50;

	// 存活的时间的默认值
	public static final long DEFAULT_TIME_TO_LIVE = 900L;

	//  默认存活时间单位.
	public static final TimeUnit DEFAULT_TIME_TO_LIVE_UNIT = TimeUnit.SECONDS;

	// 是否重定向
	public static final boolean DEFAULT_FOLLOW_REDIRECTS = true;

	// 连接超时的默认值
	public static final int DEFAULT_CONNECTION_TIMEOUT = 2000;

	//连接计时器重复的默认值
	public static final int DEFAULT_CONNECTION_TIMER_REPEAT = 3000;

	private boolean disableSslValidation = DEFAULT_DISABLE_SSL_VALIDATION;

	private int maxConnections = DEFAULT_MAX_CONNECTIONS;

	private int maxConnectionsPerRoute = DEFAULT_MAX_CONNECTIONS_PER_ROUTE;

	private long timeToLive = DEFAULT_TIME_TO_LIVE;

	private TimeUnit timeToLiveUnit = DEFAULT_TIME_TO_LIVE_UNIT;

	private boolean followRedirects = DEFAULT_FOLLOW_REDIRECTS;

	private int connectionTimeout = DEFAULT_CONNECTION_TIMEOUT;

	private int connectionTimerRepeat = DEFAULT_CONNECTION_TIMER_REPEAT;
        
}

@ConfigurationProperties("feign.client")
public class FeignClientProperties {

	private boolean defaultToProperties = true;
	private String defaultConfig = "default";
	private Map<String, FeignClientConfiguration> config = new HashMap<>();
        
}       
          
public static class FeignClientConfiguration {

    private Logger.Level loggerLevel;

    private Integer connectTimeout;

    private Integer readTimeout;

    private Class<Retryer> retryer;

    private Class<ErrorDecoder> errorDecoder;

    private List<Class<RequestInterceptor>> requestInterceptors;

    private Boolean decode404;

    private Class<Decoder> decoder;

    private Class<Encoder> encoder;

    private Class<Contract> contract;

    private ExceptionPropagationPolicy exceptionPropagationPolicy;         
    
    //........
    
}
          
// Step 1 : Config 配置的类
FeignClientConfiguration     
FeignHttpClientProperties
FeignClientProperties


// Step 2 : 配置的位置
C- FeignClientFactoryBean
    M- configureUsingProperties
    M- configureUsingConfiguration