(编写不易,转载请注明: 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) Protocolreceive(@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)结构概述
(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) { SetcandidateComponents = 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, Mapattributes) { 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); } MaprequestInterceptors = 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())); } }