SpringCloud(二)番外篇(一):FeignClient 配置及实现分析

阅读更多

 

(编写不易,转载请注明: https://shihlei.iteye.com/blog/2435851)

一 概述

 

我们的项目中使用 Feign 进行服务调用,底层使用OkHttpClinet的实现。生产环境,特别关注 “超时”,“重试”等的设置。

 

关于SpringCloud Feign的使用可以参考之前的文章:《SpringCloud(二):声明式RestClient—Feign》 

 

顺便看了下查看了Feign的源码,这里做个记录。

 

依赖版本:

 

    
        org.springframework.cloud
        spring-cloud-dependencies
        Finchley.SR1
        pom
        import
    

    
        org.springframework.cloud
        spring-cloud-starter-openfeign
    

    
        org.springframework.cloud
        spring-cloud-starter-netflix-ribbon
    

    
        io.github.openfeign
        feign-okhttp
        9.5.1
    

 

 

二 配置feign

1)配置使用优先级

(1)优先使用:yml 文件中的 feign.client.config.feignName 配置

 

feign:
  client:
    config:
      feignName:
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: full
        errorDecoder: com.example.SimpleErrorDecoder
        retryer: com.example.SimpleRetryer
        requestInterceptors:
          - com.example.FooRequestInterceptor
          - com.example.BarRequestInterceptor
        decode404: false
        encoder: com.example.SimpleEncoder
        decoder: com.example.SimpleDecoder
        contract: com.example.SimpleContract

 

 
(2)其次: yml 文件中的 feign.client.config.default 配置

  

feign:
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: basic

 

 

(3)最后:

 

@FeignClient(name = "echo", url="${feign.client.config.echo.url}", configuration = {EchoApi.EchoApiConfiguration.class})
public interface EchoApi {

    @PostMapping(value = "/v1/show", consumes = MediaType.APPLICATION_JSON_VALUE)
    Protocol receive(@RequestBody EchoRequest request);

    class EchoApiConfiguration {

    	// 重试
        @Bean
        Retryer feignRetryer() {
            return Retryer.NEVER_RETRY;
        }

        // 超时时间
        @Bean
        Request.Options options() {
            return new Request.Options(111, 111);
        }
    }
}

 

 

2)Feign 配置源码:FeignClientFactoryBean

 

protected void configureFeign(FeignContext context, Feign.Builder builder) {
	FeignClientProperties properties = applicationContext.getBean(FeignClientProperties.class);
	if (properties != null) {
		if (properties.isDefaultToProperties()) {
			// Configuration 中的配置			
			configureUsingConfiguration(context, builder);
			// yml 文件中的 feign.client.config.default 配置
			configureUsingProperties(properties.getConfig().get(properties.getDefaultConfig()), builder);
			// yml 文件中的 feign.client.config.feignName 配置
			configureUsingProperties(properties.getConfig().get(this.name), builder);
		} else {
			configureUsingProperties(properties.getConfig().get(properties.getDefaultConfig()), builder);
			configureUsingProperties(properties.getConfig().get(this.name), builder);
			configureUsingConfiguration(context, builder);
		}
	} else {
		configureUsingConfiguration(context, builder);
	}
}

 

 

3)Feign重试条件

(1)请求时发生IOException
(2)返回码非 200 ~ 300 ,404 且 HttpResponse Header 存在重试标识 “Retry-After”

 

注:详细参见下面源码分析

 

4)其他:禁用Hystrix,使用okhttp作为 client

(1)添加依赖

 

    
        io.github.openfeign
        feign-okhttp
        9.5.1
    

 

 
(2)配置 

 

feign:
  okhttp:
    enabled: true
  hystrix:
    enabled: false

 

 

二 Feign 设计分析

1) 概述:

Feign 的设计思路,将Http请求过程各个步骤抽象成如下组件:Client,Logger,Target,Options,Retryer,Decoder,ErrorDecoder。其中Target 是业务接口,通过FeignBuilder 对其生成代理 SynchronousMethodHandler ,完成请求响应。

 

2)结构概述 

SpringCloud(二)番外篇(一):FeignClient 配置及实现分析_第1张图片
 

(1)Clinet:http请求接口,默认实现

(a)Default:基于 JDK HttpURLConnection 实现的 Http Client
(b)OkHttpClient:基于 Okhttp3 实现的 Http Client,OkHttp开启了 SPDY 提供了更好的网络控制
(c)LoadBalancerFeignClient:负载均衡代理,依赖具体的Client实现进行请求,在请求前拦截,进行Server选择,调用Client。

(2)Target:FeignClient 接口Class,用于提供请求的元数据信息:请求类型,参数,返回值。
(3)Options:超时配置,只有connectTimeoutMillis,readTimeoutMillis。Client接口执行时接收该参数,设置具体的 timeout。
(4)Retryer:重试策略,如重试次数,重试前是否等待一段时间等。
(5)Decoder:解析结果,可以自定义 Response 到 返回值的转换过程。
(6)ErrorDecoder:解析错误

 

三 Feign 源码分析

核心流程:SynchronousMethodHandler 代理执行过程

 

1)入口invoke() 方法:主要实现重试框架

 

public Object invoke(Object[] argv) throws Throwable {
	RequestTemplate template = buildTemplateFromArgs.create(argv);
	Retryer retryer = this.retryer.clone();
	while (true) {
	  try {
	    return executeAndDecode(template);
	  } catch (RetryableException e) {
	  	// 特别注意:RetryableException 才会进行重试流程
	  	// 重试实现:retryer的实现判定是否重试,抛异常则重试终止
	    retryer.continueOrPropagate(e);
	    if (logLevel != Logger.Level.NONE) {
	      logger.logRetry(metadata.configKey(), logLevel);
	    }
	    continue;
	  }
}

 

注:

【1】Retryer 由于封装了重试次数,每次请求都需要重新计数,所以一次请求clone一个Retryer。

【2】收到 RetryableException 是触发 Retryer 流程的唯一条件

【3】具体流程为 executeAndDecode() 方法

 

2)核心代码流程:

 

Object executeAndDecode(RequestTemplate template) throws Throwable {
    Request request = targetRequest(template);

    // 请求部分 。。。。。。

    Response response;
    try {
      response = client.execute(request, options);
      response.toBuilder().request(request).build();
    } catch (IOException e) {
      // 发生IOExcepton 则 转换成 RetryableException 走重试流程
      throw errorExecuting(request, e);
    }

    // 处理响应部分 。。。。。。
    boolean shouldClose = true;
    try {

   	  // 。。。。。。

      if (response.status() >= 200 && response.status() < 300) {

        // 成功:提取内容 。。。。。。

      } else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) {

      	// 404 处理提取内容 。。。。。。
        return decode(response);

      } else {
      	// http 状态码非 200 ~ 300 ,404 则抛异常,
      	// 但是,如果 HttpResponse Header 存在重试标识 “Retry-After”,会抛出 RetryableException 走重试流程
        throw errorDecoder.decode(metadata.configKey(), response);
      }
    } catch (IOException e) {
     	//关闭部分 。。。。。。
    }
  }

注:

【1】http请求,捕获IOException 会 转换成 RetryableException 向上抛出,走重试流程

【2】获取到响应后,解析响应

<1> 如果响应成功,则通过Decoder解析响应

<2> 如果响应状态码非 200 ~ 300 ,404 则抛异常,特别注意:如果Response 的Http Header 存在 “Retry-After” 设置,会抛出 RetryableException

 

3)超时和重试部分说明:

 

(1)Clinet:实际请求的实现

  默认是基于 “JDK HttpURLConnection” 做的实现,没有太多东西;Options 被传入,用于设置timeout

 

  HttpURLConnection convertAndSend(Request request, Options options) throws IOException {

      // 。。。。。。

      connection.setConnectTimeout(options.connectTimeoutMillis());
      connection.setReadTimeout(options.readTimeoutMillis());
     
      // 。。。。。。
      return connection;
  }

 

(2)Retryer:重试策略实现

<1> 接口:
public interface Retryer extends Cloneable {

  /**
   * 允许重试则直接返回(可能会sleep一段时间);不允许重试则直接抛异常
   */
  void continueOrPropagate(RetryableException e);

  /**
   * 为保证每次请求一个Retryer,提供clone方法,用于创建一个新的Retryer
   */
  Retryer clone();
}
 
<2> 默认实现:

 

public static class Default implements Retryer {
	// 最大重试次数
    private final int maxAttempts;

    // 重试周期,每次重试会在这个基础上放大 Math.pow(1.5, attempt - 1) 倍用于等待,但不超过 maxPeriod;
    private final long period;

    // 重试时等待server 恢复的最大时间周期,超过则使用该值作为 sleep 时间长度
    private final long maxPeriod;

    int attempt;
    long sleptForMillis;

    public Default() {
      this(100, SECONDS.toMillis(1), 5);
    }

    public Default(long period, long maxPeriod, int maxAttempts) {
      this.period = period;
      this.maxPeriod = maxPeriod;
      this.maxAttempts = maxAttempts;
      this.attempt = 1;
    }

    // visible for testing;
    protected long currentTimeMillis() {
      return System.currentTimeMillis();
    }

    public void continueOrPropagate(RetryableException e) {
      // 重试超过最大次数,直接抛异常终止重试
      if (attempt++ >= maxAttempts) {
        throw e;
      }


      long interval;
      
      if (e.retryAfter() != null) {
      	// Http Response Header 指定了 “Retry-After” 什么时间后重试,则计算需要sleep多久,然后重试
        interval = e.retryAfter().getTime() - currentTimeMillis();
        if (interval > maxPeriod) {
          interval = maxPeriod;
        }
        if (interval < 0) {
          return;
        }
      } else {

        interval = nextMaxInterval();
      }
      try {
      	// sleep 一段时间,等待 Server 恢复
        Thread.sleep(interval);
      } catch (InterruptedException ignored) {
        Thread.currentThread().interrupt();
      }
      sleptForMillis += interval;
    }

    long nextMaxInterval() {
      long interval = (long) (period * Math.pow(1.5, attempt - 1));
      return interval > maxPeriod ? maxPeriod : interval;
    }

    @Override
    public Retryer clone() {
      return new Default(period, maxPeriod, maxAttempts);
    }
  }

 

注:

业务逻辑比较简单:超过最大次数直接抛异常,否则sleep指定的时间周期,再重试

等待时间周期算法:(long) (period * Math.pow(1.5, attempt - 1)) ,将用于指定的period 按照重试次数逐渐延长。

 

四 SpringCloud FeignClinet 构建过程

1)Spring 框架相关知识

 

(1)@Import:用于导入其他Configuration配置信息,特殊之处,如果 value 的类型为 ImportSelector 或 ImportBeanDefinitionRegistrar 调用这两个接口的回调方法,可用于框架扩展。@EnableFeignClients,就是借助这种方式实现容器识别 @FeignClient

 

(2)BeanDefinition:Sping描述Bean的封装类,包括如xml方式或annocation解析出Bean的依赖或者单例等定义信息,用于初始化bean或初始化FactoryBean。

 

(3)FactoryBean:Bean实例创建工厂,提供 getObject() 方法用于创建实例,在调用容器 context.getBean() 时会调用,通过这种方式可以方便创建代理等。

 

2)FeignClient 构建分析 
(1)@EnableFeignClient:扩展入口
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
	
	// 。。。。。。

	/**
	 * 所有FeignClient的默认配置
	 */
	Class[] defaultConfiguration() default {};

	// 。。。。。。

}

 

注:重要的就两个:

【1】@Import(FeignClientsRegistrar.class):这里是借助 @Import 扩展容器对 对@FeignClient的支持

Spring容器扫描到@Import注解,发现里面class类型是ImportBeanDefinitionRegistrar.class,则调用 该类的 registerBeanDefinitions()方法。

FeignClientsRegistrar主要完成@FeignClient注解的扫描,生成接口代理,在代理中整合Feign。

 

【2】defaultConfiguration() 从注解上看,默认 FeignClientsConfiguration,提供了Feign各个组件的默认配置,即上文中  “》 二 Feign 设计分析 》 2)结构概述 中所需组件的默认设置” 。

 

2)FeignClientsRegistrar:扫描 @FeignClient 注解 完成 BeanDefinition及制定 FactoryBean 的 实现类 FeignClientFactoryBean
(1)registerBeanDefinitions:扩展入口

 

@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
		BeanDefinitionRegistry registry) {
	
	// 注册默认的Configuration
	registerDefaultConfiguration(metadata, registry);

	// 注册 @FeignClient 注解接口的 BeanDefinition,并解析 @FeignClient属性,用于初始化 接口
	registerFeignClients(metadata, registry);
}

  

注:

【1】registerDefaultConfiguration():取 @EnableFeignClients 的 defaultConfiguration 属性指定的 Configuration 注册

【2】registerFeignClients():注册 FeignClinet

 

(2)registerFeignClients():注册 BeanDefinition 的主逻辑

 

public void registerFeignClients(AnnotationMetadata metadata,
		BeanDefinitionRegistry registry) {

	// 省略获取 待 扫描包的过程  。。。。。。

	for (String basePackage : basePackages) {
		Set candidateComponents = scanner
				.findCandidateComponents(basePackage);
		for (BeanDefinition candidateComponent : candidateComponents) {
			if (candidateComponent instanceof AnnotatedBeanDefinition) {
				
				// 。。。。。。

				Map attributes = annotationMetadata
						.getAnnotationAttributes(
								FeignClient.class.getCanonicalName());

				String name = getClientName(attributes);

				// 注册 @FeignClient 
				registerClientConfiguration(registry, name,
						attributes.get("configuration"));

				// 注册 @FeignClient 
				registerFeignClient(registry, annotationMetadata, attributes);
			}
		}
	}
}

 

(3)registerFeignClient():核心,重点说明

 

private void registerFeignClient(BeanDefinitionRegistry registry,
		AnnotationMetadata annotationMetadata, Map attributes) {
	String className = annotationMetadata.getClassName();

	// 指定FeignClient 注解接口生成代理的 FactoryBean,FactoryBean 的 getObject() 方法用于用于运行时获取代理实例

	BeanDefinitionBuilder definition = BeanDefinitionBuilder
			.genericBeanDefinition(FeignClientFactoryBean.class);

	validate(attributes);

	// BeanDefinitionBuilder 指定 FeignClientFactoryBean 初始化需要的 成员变量的值,多数是从@FeignClient中解析出来的

	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";

	// 生成FeignClientFactoryBean 的 beanDefinition
	AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();

	boolean primary = (Boolean)attributes.get("primary"); 

	beanDefinition.setPrimary(primary);

	String qualifier = getQualifier(attributes);
	if (StringUtils.hasText(qualifier)) {
		alias = qualifier;
	}

	BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
			new String[] { alias });
	// 注册 FeignClientFactoryBean 的 beanDefinition 到容器中
	BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}

  

注:重点:

【1】注册BeanDefinition的 FactoryBean 为 FeignClientFactoryBean.class,用于代理创建使用;

【2】注册BeanDefinition的 属性(url,path, decode404 , fallback 等)的初始化值,用于初始化 FeignClientFactoryBean,在需要实例的时候用于构建Feign使用;

 

3)FeignClientFactoryBean:创建 @FeignClient 注释接口的代理,真正集成Feign的地方 

(1)成员变量:大部分有上面指定了初始化值,用于传递给Feign使用
        private Class type;

	private String name;

	private String url;

	private String path;

	private boolean decode404;

	private ApplicationContext applicationContext;

	private Class fallback = void.class;

	private Class fallbackFactory = void.class;

 

(2)getObject() 方法

 

@Override
public Object getObject() throws Exception {
	FeignContext context = applicationContext.getBean(FeignContext.class);

	// 初始化 Feign.Builder 用于生成代理
	Feign.Builder builder = feign(context);

	// @FeignClient 没有指定 url 属性
	if (!StringUtils.hasText(this.url)) {
		String url;

		// 拼接 http 前缀 和 seriveId 部分
		if (!this.name.startsWith("http")) {
			url = "http://" + this.name;
		}
		else {
			url = this.name;
		}
		url += cleanPath();

		// 创建 负载均衡的 客户端
		return loadBalance(builder, context, new HardCodedTarget<>(this.type,
				this.name, url));
	}

	// @FeignClient 指定url 属性,没有http 则 补充默认 http 协议
	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) {

			// @FeignClient 指定url 属性,则会禁用Ribbon 的负载均衡能力,所以解包
			client = ((LoadBalancerFeignClient)client).getDelegate();
		}
		builder.client(client);
	}

	//生成代理
	Targeter targeter = get(context, Targeter.class);
	return targeter.target(this, builder, context, new HardCodedTarget<>(
			this.type, this.name, url));
}

 

注:这里是生成代理的核心步骤

【1】初始化 Feign.Builder 用于生成代理。

【2】生成请求的url,如果 @FeignClient 指定url 则使用该url 补充默认协议;如果未指定 Url,则使用 http://name

【3】根据 @FeignClient 是否指定url属性确定是否使用Ribbon进行负载均衡

<1>未指定url:使用 loadBalance() 方法获取 基于Ribbon 的 Feign Client 接口实现

<2>指定url:则使用用户指定的Feign Client接口实现,但是Client的实现如果是 LoadBalancerFeignClient 类型,则解包,提取非负载均衡实现,用于禁用负载均衡能力。

【4】最后生成接口代理

 

(3)feign(context): 初始化,指定 logger,Encoder,Decoder 等等快速过

 

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

	Feign.Builder builder = get(context, Feign.Builder.class)
			// required values
			.logger(logger)
			.encoder(get(context, Encoder.class))
			.decoder(get(context, Decoder.class))
			.contract(get(context, Contract.class));

	configureFeign(context, builder);

	return builder;
} 

  

(4)configureFeign() feign 配置 重点
protected void configureFeign(FeignContext context, Feign.Builder builder) {
	FeignClientProperties properties = applicationContext.getBean(FeignClientProperties.class);
	if (properties != null) {
		if (properties.isDefaultToProperties()) {
			// 从上下文中设置各个组件的默认值				
			configureUsingConfiguration(context, builder);
			// yml 文件中的读取 feign.client 的配置覆盖组件设置
			configureUsingProperties(properties.getConfig().get(properties.getDefaultConfig()), builder);
			// 使用用户自定义配置设置
			configureUsingProperties(properties.getConfig().get(this.name), builder);
		} else {
			configureUsingProperties(properties.getConfig().get(properties.getDefaultConfig()), builder);
			configureUsingProperties(properties.getConfig().get(this.name), builder);
			configureUsingConfiguration(context, builder);
		}
	} else {
		configureUsingConfiguration(context, builder);
	}
}

 

注:

【1】configureUsingConfiguration() 从contex 中读取读取读取各个组件和超时时间配置

【2】configureUsingProperties() 从yml中读取各个组件及超时时间配置

【3】这里默认和非默认只是个先后顺序问题,因此完成了

 

(5)设置属性:几大属性都被初始化完成 “二 配置feign 》1)配置使用优先级”

 

protected void configureUsingConfiguration(FeignContext context, Feign.Builder builder) {
	Logger.Level level = getOptional(context, Logger.Level.class);
	if (level != null) {
		builder.logLevel(level);
	}
	Retryer retryer = getOptional(context, Retryer.class);
	if (retryer != null) {
		builder.retryer(retryer);
	}
	ErrorDecoder errorDecoder = getOptional(context, ErrorDecoder.class);
	if (errorDecoder != null) {
		builder.errorDecoder(errorDecoder);
	}
	Request.Options options = getOptional(context, Request.Options.class);
	if (options != null) {
		builder.options(options);
	}
	Map requestInterceptors = context.getInstances(
			this.name, RequestInterceptor.class);
	if (requestInterceptors != null) {
		builder.requestInterceptors(requestInterceptors.values());
	}

	if (decode404) {
		builder.decode404();
	}
}

protected void configureUsingProperties(FeignClientProperties.FeignClientConfiguration config, Feign.Builder builder) {
	if (config == null) {
		return;
	}

	if (config.getLoggerLevel() != null) {
		builder.logLevel(config.getLoggerLevel());
	}

	if (config.getConnectTimeout() != null && config.getReadTimeout() != null) {
		builder.options(new Request.Options(config.getConnectTimeout(), config.getReadTimeout()));
	}

	if (config.getRetryer() != null) {
		Retryer retryer = getOrInstantiate(config.getRetryer());
		builder.retryer(retryer);
	}

	if (config.getErrorDecoder() != null) {
		ErrorDecoder errorDecoder = getOrInstantiate(config.getErrorDecoder());
		builder.errorDecoder(errorDecoder);
	}

	if (config.getRequestInterceptors() != null && !config.getRequestInterceptors().isEmpty()) {
		// this will add request interceptor to builder, not replace existing
		for (Class bean : config.getRequestInterceptors()) {
			RequestInterceptor interceptor = getOrInstantiate(bean);
			builder.requestInterceptor(interceptor);
		}
	}

	if (config.getDecode404() != null) {
		if (config.getDecode404()) {
			builder.decode404();
		}
	}

	if (Objects.nonNull(config.getEncoder())) {
		builder.encoder(getOrInstantiate(config.getEncoder()));
	}

	if (Objects.nonNull(config.getDecoder())) {
		builder.decoder(getOrInstantiate(config.getDecoder()));
	}

	if (Objects.nonNull(config.getContract())) {
		builder.contract(getOrInstantiate(config.getContract()));
	}
}

 

 

  • SpringCloud(二)番外篇(一):FeignClient 配置及实现分析_第2张图片
  • 大小: 572.4 KB
  • 查看图片附件

你可能感兴趣的:(SpringCloud,Feign,Feign配置,@FeignClient分析,Feign超时重试配置,FeignClient)