SpringCloud Netflix复习之OpenFeign
创始人
2024-05-08 15:29:31
0

文章目录

    • 写作背景
    • Feign核心组件介绍
      • Encoder和Decoder
      • Logger
      • Contract
      • Feign.Builder
    • 上手实战
      • 开启FeignClient调用请求日志
      • 给FeignClient注入自定义拦截器
      • Feign支持文件上传配置
      • Feign开启Gzip压缩
      • Feign配置超时时间
      • Feign整合Ribbon支持负载均衡
    • 核心源码部分
      • FeignClient注入到Spring容器的源码
        • FeignClient接口构造为bean的过程
        • FeignClientFactoryBean的getObject()构建真正的Bean实例源码
      • 构建并配置Feign.Builder的过程源码
        • Feign默认的组件注入的源码
        • 读取feign.client开头的配置信息的源码
        • 使用Feign.Builder构建一个FeignClient实例源码
      • 基于HystrixTargeter和HardCodedTarget创建Feign动态代理细节
        • Feign开启Hystrix熔断后生成动态代理的源码
        • Feign关闭Hystrix熔断生成动态代理的源码
      • Feign动态代理处理请求的核心源码
        • Contract组件解析@RequestParam等SpringMVC注解绑定到HTTP请求参数源码
        • 执行Feign拦截器的源码
      • Feign与Ribbon整合发送HTTP请求的源码
        • 真正发起HTTP请求的源码
        • 获取Ribbon相关配置源码

写作背景

前面复习了SpringCloud Netflix Eureka和Ribbon的知识,并进行了实战以及源码的验证。你会发现在没有Feign之前从fc-service-portal服务发起对fc-service-screen服务的调用,需要注入RestTemplate然后通过RestTemplate的Api来发起访问,每次都要写类似

restTemplate.getForObject(“http://fc-service-screen/getPort”, Integer.class)

这样的代码,是不是感觉有点不优雅,在微服务架构中有专门负责服务之间通信的组件,同步的组件有Feign和Rpc,异步通信的组件一般通过消息队列。本文复习的重点是SpringCloud OpenFeign,注意Feign是Netflix研发的一个轻量级RESTful的HTTP客户端,而OpenFeign是SpringCloud 官方自研的,在Feign的基础上增加了对SpringMVC注解的支持。
本文的书写思路从以下几个方面来,主要是实战和源码验证

  1. Feign的核心组件介绍
  2. 上手实战
  3. 源码验证Feign动态代理的生成和请求的发送与处理

Feign核心组件介绍

上一篇复习了Ribbon,它有核心的几个组件ILoadBalancer、IRule、IPing、ServerList,Feign一样也有几个核心组件

Encoder和Decoder

编码器和解码器
在发起Feign接口调用时,如果传递的参数是个对象,那么Feign会通过Encoder编码器组件对这个对象进行encode编码,转成Json格式,在SpringCloud中默认Encoder组件是SpringEncoder
在Feign客户端收到一个Json参数之后,就会通过Decoder解码器将Json转成本地的一个对象。在SpringCloud中默认Decoder组件是ResponseEntityDecoder

Logger

日志组件
顾名思义,日志组件是负责打印日志的,Feign是负责接口调用发送HTTP请求的,通过Logger可以打印接口调用请求的日志信息。在SpringCloud中默认Logger组件Slf4jLogger

Contract

契约组件
这个组件是用来解释SpringMVC的注解的,比如@PathVariable、@RequestMapping、@RequestParam等注解,让Feign可以跟这些SpringMVC的注解可以结合起来使用。在SpringCloud中默认的Contract组件是SpringMvcContract

Feign.Builder

Feign的构造器组件
使用构造器模式构造FeignClient实例的。一个FeignClient实例包含了上面所有的组件,比如Encoder、Decoder、Logger、Contract。在SpringCloud中Feign实例构造器是HystrixFeign.Builder, Hystrix其实也是跟Feign整合在一起使用的,而Feign的客户端实例FeignClient是LoadBalancerFeignClient底层是跟Ribbon整合来使用的。

上手实战

改造fc-service-portal服务,改RestTemplate方式为Feign方式发起接口调用
1、pom.xml引入坐标依赖

org.springframework.cloudspring-cloud-starter-openfeign

2、启动类加@EnableFeignClients注解开启 SpringCloud OpenFeign的自动装配功能

@EnableFeignClients
@EnableEurekaClient
@SpringBootApplication
public class ServicePortalApplication {
}

3、定义一个接口加@FeignClient注解标识这个接口是一个Feign客户端

/*** @author zhangyu*/
@FeignClient(value = "fc-service-screen", fallback = ScreenFeignClientHystrix.class)
public interface ScreenFeignClient {/*** 获取服务端口** @return String*/@GetMapping("/getPort")int getPort();}

value属性指定要调用的服务名
fallback属性是指定Hystrix的熔断降级的类,当fc-service-screen服务的getPort()不可用时会进入fallack降级,也就是会调用ScreenFeignClientHystrix的getPort(),关于Hystrix的知识后面等我复习到Hystrix时再来说明。

@Service
public class ScreenFeignClientHystrix implements ScreenFeignClient {@Overridepublic int getPort() {return 0;}
}

4、在fc-service-portal编写接口通过Feign来调用

@RestController
public class HelloWorldController {@ResourceScreenFeignClient screenFeignClient;@ResourceRestTemplate restTemplate;//通过RestTemplate调用@GetMapping("/getPort")public int getPort() {return restTemplate.getForObject("http://fc-service-screen/getPort", Integer.class);}//看这个通过Feign调用@GetMapping("/getPortByFeign")public int getPortByFeign() {return screenFeignClient.getPort();}
}

我们先启动fc-service-portal服务,然后再启动fc-service-screen服务,然后发起

http://localhost:8002/getPortByFeign
在这里插入图片描述

开启FeignClient调用请求日志

OpenFeign有四种日志级别,默认是NONE,就是不打印任何日志
NONE:默认级别,不显示日志
BASIC:仅记录请求方法、URL、响应状态及执行时间
HEADERS:除了BASIC中定义的信息之外,还有请求和响应头信息
FULL:除了HEADERS中定义的信息之外,还有请求和响应Body等元数据信息

1、首先在配置类里注入一个Logger.Lever的Bean

@Configuration
public class ScreenFeignConfiguration {/*** 开启feign请求日志要注入下面这个Bean*/@BeanLogger.Level feignLoggerLevel() {return Logger.Level.FULL;}
}

2、然后在ScreenFeignClient里指定这个配置类

@FeignClient(value = "fc-service-screen", configuration = ScreenFeignConfiguration.class, fallback = ScreenFeignClientHystrix.class)
public interface ScreenFeignClient {/*** 获取服务端口** @return String*/@GetMapping("/getPort")int getPort();}

@FeignClient注解里的configuration属性可以指定用哪个配置类

3、最后要在配置文件里指定哪个FeignClient要打印日志

#指定某个feign客户端的日志级别
logging:level:com:zhangyu:serviceportal:feign:ScreenFeignClient: DEBUG

loggin.lever下面是ScreenFeignClient的一个全限定类名,DEBUG表示日志级别是DEBUG

配置完成后,我们重启fc-service-portal服务,然后再发起一个请求

http://localhost:8002/getPortByFeign

可以看到控制台已经打印了日志
在这里插入图片描述

给FeignClient注入自定义拦截器

1、自定义拦截器实现RequestInterceptor接口

/*** 自定义HeaderRequestInterceptor实现了RequestInterceptor接口* 主要往请求头加点东西,这里演示加个appId** @author zhangyu* @since 2023/1/6 12:43*/
public class HeaderRequestInterceptor implements RequestInterceptor {@Overridepublic void apply(RequestTemplate template) {template.header("appId", "12345");}
}

往请求头里加一个appId的属性,value值为12345

2、将自定义的拦截器注入到FeignClient的配置类里

@Configuration
public class ScreenFeignConfiguration {/*** 开启feign请求日志要注入下面这个Bean*/@BeanLogger.Level feignLoggerLevel() {return Logger.Level.FULL;}/*** 注入一个自定义的拦截器* @return RequestInterceptor*/@Beanpublic RequestInterceptor requestInterceptor () {return new HeaderRequestInterceptor();}
}

3、去下游服务fc-service-screen里打印日志看拦截器是否成功

@RestController
public class HelloWordController {private static Logger log = LoggerFactory.getLogger(HelloWordController.class);@Value("${server.port}")int port;@GetMapping("/getPort")public int getPort(HttpServletRequest request) {log.info("header里的appId:{}", request.getHeader("appId"));return port;}}

发起如下请求,然后去看fc-service-screen的控制台日志打印请求头里是否有appId的值

http://localhost:8002/getPortByFeign

我们现在拦截器里加一个断点可以看看,发现请求已经进来了
在这里插入图片描述
在这里插入图片描述
可以看到日志里打印的appId的值是拦截器里设置的。

Feign支持文件上传配置

上面组件介绍里有说过,Feign需要对参数进行编解码的,文件上传的编解码需要注入一个专门的编码器SpringFormEncoder

@Configuration
public class ScreenFeignConfiguration {@Autowiredprivate ObjectFactory messageConverters;@Bean@Scope("prototype")public Encoder multipartFormEncoder() {return new SpringFormEncoder(new SpringEncoder(messageConverters));}

然后要引入github社区提供的依赖

io.github.openfeign.formfeign-form3.4.1

io.github.openfeign.formfeign-form-spring3.4.1

最后就是在FeignClient里具体的文件上传接口,要注意的是入参要有@RequestPart注解,我demo里就不演示上传了。

/*** 上传文件** @param file 文件* @return String*/@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)String upLoad(@RequestPart("file") MultipartFile file);

Feign开启Gzip压缩

Feign可以对请求和响应进行压缩,默认都是未开启的,开Gzip压缩有两个好处,一个是减少存储空间,另一个是减少网络传输时间。只需要在配置文件里增加如下配置即可,压缩阈值和类型都是默认的,其实只是修改了request和response的默认关闭false为true。

feign:#开启请求和响应压缩compression:request:enabled: true#压缩阈值min-request-size: 2048#压缩类型mime-types: text/xml,application/xml,application/jsonresponse:enabled: true

在这里插入图片描述
开启压缩后可以看到请求和响应头里

Feign配置超时时间

Feign默认的请求处理超时时间为1s,有时候有些业务请求会超过1s的限制,就需要修改Feign的超时时间配置,比如下面的配置,设置请求连接超时5s,请求处理超时5s。
打个断点验证一下,在没有配置Feign和Ribbon超时时间的情况,Feign默认的连接超时和请求处理超时时间
在这里插入图片描述

feign:client:config:fc-service-screen:connectTimeout: 5000readTimeout: 5000

故意让fc-service-screen里线程睡个5s然后模拟超时看下

@GetMapping("/getPort")public int getPort(HttpServletRequest request) throws InterruptedException {Thread.sleep(5000);log.info("header里的appId:{}", request.getHeader("appId"));return port;}

访问如下请求,从日志里可以看到超时了

http://localhost:8002/getPortByFeign
在这里插入图片描述

Feign整合Ribbon支持负载均衡

上面说到Feign有自己的超时配置,Ribbon也有超时配置,那如果既设置了Feign的超时又设置了Ribbon超时,那以谁的为准呢?经过测试以Feign的的超时配置为准,比如我设置对fc-service-screen服务的Ribbon的连接超时和请求处理超时都是5s,然后设置Feign的连接和处理超时为1s,处理请求的线程睡个3s然后观察以哪个为准,如果请求超时说明是以Feign为准,请求成功说明以Ribbon为准

#Ribbon超时配置
fc-service-screen:ribbon:#请求连接超时时间ConnectTimeout: 5000#请求处理超时时间ReadTimeout: 5000#对所有操作都进⾏重试OkToRetryOnAllOperations: true#对当前选中实例重试次数,不包括第⼀次调⽤MaxAutoRetries: 0#切换实例的重试次数MaxAutoRetriesNextServer: 0

Feign的超时设置

feign:client:config:default:connectTimeout: 1000readTimeout: 1000
 @GetMapping("/getPort")public int getPort(HttpServletRequest request) throws InterruptedException {//睡3sThread.sleep(3000);log.info("header里的appId:{}", request.getHeader("appId"));return port;}

访问入下请求

http://localhost:8002/getPortByFeign
在这里插入图片描述
结果是超时,说明当Feign和Ribbon两个都设置了超时时间,以Feign的超时时间为准

核心源码部分

Feign的核心机制是将打了@FeignClient注解的接口生成Feign的动态代理,然后注入到Spring容器中,以及解析并处理接口上打的那些SpringMVC的注解,比如@RequestMapping、@RequstParam、@PathVarialbe等,基于这些SpringMVC的注解来生成接口对应的HTTP请求。因此源码部分主要从以下几个方面来分析

  1. Feign是如何与Spring整合将FeignClient注入到Spring容器的
  2. Feign动态代理是如何创建的
  3. Feign是如何接受和处理请求的

FeignClient注入到Spring容器的源码

首先看@EnableFeignClients注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
//导入了一个关键类
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {

导入一个极为重要的类:@Import(FeignClientsRegistrar.class) 这个类实现了ImportBeanDefinitionRegistrar接口,它是Spring的一个扩展点,实现registerBeanDefinitions()方法我们可以自己封装一个BeanDefinition注册到Spring容器。
我们看下FeignClientsRegistrar的registerBeanDefinitions()方法的逻辑

@Overridepublic void registerBeanDefinitions(AnnotationMetadata metadata,BeanDefinitionRegistry registry) {//这个看名字是注册默认的配置,先不看registerDefaultConfiguration(metadata, registry);//重点是这个,看名字就是注册FeignClient的registerFeignClients(metadata, registry);}

我们跟进去,可以看到有扫描打了@FeignClient注解的代码,还有

public void registerFeignClients(AnnotationMetadata metadata,BeanDefinitionRegistry registry) {//获取一个扫描器ClassPathScanningCandidateComponentProvider scanner = getScanner();scanner.setResourceLoader(this.resourceLoader);Set basePackages;Map attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());//添加一个FeignClient.class的注解过滤器AnnotationTypeFilterAnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(FeignClient.class);final Class[] clients = attrs == null ? null: (Class[]) attrs.get("clients");if (clients == null || clients.length == 0) {scanner.addIncludeFilter(annotationTypeFilter);basePackages = getBasePackages(metadata);}else {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() {@Overrideprotected boolean match(ClassMetadata metadata) {String cleaned = metadata.getClassName().replaceAll("\\$", ".");return clientClasses.contains(cleaned);}};scanner.addIncludeFilter(new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));}for (String basePackage : basePackages) {Set candidateComponents = scanner.findCandidateComponents(basePackage);for (BeanDefinition candidateComponent : candidateComponents) {if (candidateComponent instanceof AnnotatedBeanDefinition) {// verify annotated class is an interfaceAnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();Assert.isTrue(annotationMetadata.isInterface(),"@FeignClient can only be specified on an interface");//拿到注解的元数据Map attributes = annotationMetadata.getAnnotationAttributes(FeignClient.class.getCanonicalName());String name = getClientName(attributes);registerClientConfiguration(registry, name,attributes.get("configuration"));//注册FeignClient接口registerFeignClient(registry, annotationMetadata, attributes);}}}}

FeignClient接口构造为bean的过程

private void registerFeignClient(BeanDefinitionRegistry registry,AnnotationMetadata annotationMetadata, Map attributes) {String className = annotationMetadata.getClassName();//构建一个BeanDefinitionBuilderBeanDefinitionBuilder definition = BeanDefinitionBuilder//注意这里有个FeignClientFactoryBean,是生成动态代理的关键.genericBeanDefinition(FeignClientFactoryBean.class);validate(attributes);//构建的BeanDefiniton包含FeignClient注解和ScreenFeignClient接口的所有信息definition.addPropertyValue("url", getUrl(attributes));definition.addPropertyValue("path", getPath(attributes));String name = getName(attributes);definition.addPropertyValue("name", name);String contextId = getContextId(attributes);definition.addPropertyValue("contextId", contextId);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 = contextId + "FeignClient";AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();boolean primary = (Boolean) attributes.get("primary"); // has a default, won't be// nullbeanDefinition.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);}

我们打个断点看下这个BeanDefinitionHolder包含哪些内容
在这里插入图片描述

FeignClientFactoryBean的getObject()构建真正的Bean实例源码

我们重点看下FeignClientFactoryBean,因为它是一个FactoryBean在Spring容器获取这个Bean的时候实际上是调用FactoryBean的getObject()方法,我们看下这个方法

@Overridepublic Object getObject() throws Exception {return getTarget();} T getTarget() {FeignContext context = this.applicationContext.getBean(FeignContext.class);//构建一个Feign.Builder,这是一个核心组件Feign.Builder builder = feign(context);//如果@FeignClient注解里的url属性为空,if (!StringUtils.hasText(this.url)) {if (!this.name.startsWith("http")) {this.url = "http://" + this.name;}else {this.url = this.name;}this.url += cleanPath();//那么FeignClient客户端就是一个带负载均衡功能的LoadBalancer就是feign+ribbon整合return (T) loadBalance(builder, context,new HardCodedTarget<>(this.type, this.name, this.url));}。。。}

构建并配置Feign.Builder的过程源码

protected Feign.Builder feign(FeignContext context) {FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);Logger logger = loggerFactory.create(this.type);// @formatter:off//从FeignContext里获取一个Feign.Builder实例,然后从FeignContext里获取其他几个组件并赋值给builderFeign.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));// @formatter:on//后去feign.client开头的配置信息并配置到Feign.Builder的实例中去configureFeign(context, builder);return builder;}

这些组件我们没有配置,默认是在哪里注入的呢?

Feign默认的组件注入的源码

我们看FeignClientsConfiguration类里面

@Bean@ConditionalOnMissingBeanpublic Decoder feignDecoder() {return new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)));}@Bean@ConditionalOnMissingBean@ConditionalOnMissingClass("org.springframework.data.domain.Pageable")public Encoder feignEncoder() {return new SpringEncoder(this.messageConverters);}@Bean@ConditionalOnMissingBeanpublic Contract feignContract(ConversionService feignConversionService) {return new SpringMvcContract(this.parameterProcessors, feignConversionService);}@Bean@ConditionalOnMissingBeanpublic Retryer feignRetryer() {return Retryer.NEVER_RETRY;}			

读取feign.client开头的配置信息的源码

我们看下configuraFeign()方法

protected void configureFeign(FeignContext context, Feign.Builder builder) {//FeignClientProperties就是对应装配feign.client开头的配置的FeignClientProperties properties = this.applicationContext.getBean(FeignClientProperties.class);//有配置过feign相关配置的走这里			if (properties != null) {if (properties.isDefaultToProperties()) {configureUsingConfiguration(context, builder);configureUsingProperties(properties.getConfig().get(properties.getDefaultConfig()),builder);configureUsingProperties(properties.getConfig().get(this.contextId),builder);}else {configureUsingProperties(properties.getConfig().get(properties.getDefaultConfig()),builder);configureUsingProperties(properties.getConfig().get(this.contextId),builder);configureUsingConfiguration(context, builder);}}else {configureUsingConfiguration(context, builder);}}

我们直接打个断点来看吧
在这里插入图片描述
这些配置数据对应我们application.yml的feign.client开头的配置

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

再看configureUsingConfiguration()看名字是用配置来配置builder实例的,我们看下源码

protected void configureUsingConfiguration(FeignContext context,Feign.Builder builder) {Logger.Level level = getOptional(context, Logger.Level.class);if (level != null) {//logger组件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);}//Feign的拦截器Map requestInterceptors = context.getInstances(this.contextId, RequestInterceptor.class);if (requestInterceptors != null) {builder.requestInterceptors(requestInterceptors.values());}QueryMapEncoder queryMapEncoder = getOptional(context, QueryMapEncoder.class);if (queryMapEncoder != null) {builder.queryMapEncoder(queryMapEncoder);}if (this.decode404) {builder.decode404();}}

我们打个断点看看此时Feign.Builder里有哪些数据
在这里插入图片描述
到这里其实Feign.Builder就全部构造完了,我这里想试下如果不配置Feign的超时时间,默认的超时时间是多少
在这里插入图片描述
可以看到Feign默认的连接超时是10s,请求处理超时是60s

使用Feign.Builder构建一个FeignClient实例源码

FeignClientFactoryBean#getTarget

 T getTarget() {FeignContext context = this.applicationContext.getBean(FeignContext.class);Feign.Builder builder = feign(context);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));

先是new了一个HardCodedTarget,里面包含了接口类型(com.zhangyu.serviceportal.feign.ScreenFeignClient)、服务名称(fc-service-screen)、跟Feign.Builder、FeignContext,一起,传入了loadBalance()方法里去。

跟进去看下这个loadBalance()方法

protected  T loadBalance(Feign.Builder builder, FeignContext context,HardCodedTarget target) {//从FeignContext里获取Feign.Client	Client client = getOptional(context, Client.class);if (client != null) {builder.client(client);//OpenFeign的实现类是HystrixTargeter,targeter是一个接口Targeter targeter = get(context, Targeter.class);//这个targetr的target()方法会得到一个实例return targeter.target(this, builder, context, target);}}

再打个断点看看这里获取的client是个啥
在这里插入图片描述
哦,原来是LoadBalancerFeignClient,那这个LoadBalancerFeignClient是那里注入的呢?看名字跟负载均衡有关,应该是和Ribbon整合的代码中,看这个类FeignRibbonClientAutoConfiguration

@ConditionalOnClass({ ILoadBalancer.class, Feign.class })
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.ribbon.enabled",matchIfMissing = true)
@Configuration(proxyBeanMethods = false)
@AutoConfigureBefore(FeignAutoConfiguration.class)
@EnableConfigurationProperties({ FeignHttpClientProperties.class })
//导入了三个类
@Import({ HttpClientFeignLoadBalancedConfiguration.class,OkHttpFeignLoadBalancedConfiguration.class,DefaultFeignLoadBalancedConfiguration.class })
public class FeignRibbonClientAutoConfiguration {

导入了三个类,我们每个点进去看下,首先HttpClientFeignLoadBalancedConfiguration是需要有feign.httpclient.enabled为true才生效;然后OkHttpFeignLoadBalancedConfiguration需要有feign.okhttp.enabled为true才生效

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ApacheHttpClient.class)
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
@Import(HttpClientFeignConfiguration.class)
class HttpClientFeignLoadBalancedConfiguration {@Bean@ConditionalOnMissingBean(Client.class)public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,SpringClientFactory clientFactory, HttpClient httpClient) {ApacheHttpClient delegate = new ApacheHttpClient(httpClient);return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);}}@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(OkHttpClient.class)
@ConditionalOnProperty("feign.okhttp.enabled")
@Import(OkHttpFeignConfiguration.class)
class OkHttpFeignLoadBalancedConfiguration {@Bean@ConditionalOnMissingBean(Client.class)public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,SpringClientFactory clientFactory, okhttp3.OkHttpClient okHttpClient) {OkHttpClient delegate = new OkHttpClient(okHttpClient);return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);}}

那就只剩最后一个了DefaultFeignLoadBalancedConfiguration,默认返回的是LoadBalancerFeignClient,它是Feign的客户端实例,里面包含execute()是发起一个请求的核心逻辑。

@Configuration(proxyBeanMethods = false)
class DefaultFeignLoadBalancedConfiguration {@Bean@ConditionalOnMissingBeanpublic Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,SpringClientFactory clientFactory) {return new LoadBalancerFeignClient(new Client.Default(null, null), cachingFactory,clientFactory);}}

基于HystrixTargeter和HardCodedTarget创建Feign动态代理细节

最后看这行代码return targeter.target(this, builder, context, target);其实就是HystrixTargeter#target

public  T target(FeignClientFactoryBean factory, Feign.Builder feign,FeignContext context, Target.HardCodedTarget target) {//区分feign是否开启了hystrix支持(配置文件feign.hystrix.enabled=true,默认是不开启的)if (!(feign instanceof feign.hystrix.HystrixFeign.Builder)) {//没有开启进这里return feign.target(target);}//开启了进这里,又从FeignContext里获取了HystrixFeignfeign.hystrix.HystrixFeign.Builder builder = (feign.hystrix.HystrixFeign.Builder) feign;String name = StringUtils.isEmpty(factory.getContextId()) ? factory.getName(): factory.getContextId();SetterFactory setterFactory = getOptional(name, context, SetterFactory.class);if (setterFactory != null) {builder.setterFactory(setterFactory);}Class fallback = factory.getFallback();if (fallback != void.class) {//如果fallback属性不为空,返回fallback配置的类实例return targetWithFallback(name, context, target, builder, fallback);}Class fallbackFactory = factory.getFallbackFactory();if (fallbackFactory != void.class) {return targetWithFallbackFactory(name, context, target, builder,fallbackFactory);}return feign.target(target);}

Feign开启Hystrix熔断后生成动态代理的源码

开启Feign的熔断支持后,Feign.Builder就是HystrixFeign.builder()

@Configuration(proxyBeanMethods = false)@ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class })protected static class HystrixFeignConfiguration {@Bean@Scope("prototype")@ConditionalOnMissingBean@ConditionalOnProperty(name = "feign.hystrix.enabled")public Feign.Builder feignHystrixBuilder() {return HystrixFeign.builder();}}

我们配置文件是开启Feign的熔断的,接着往下看

targeter是一个接口,它的target()方法可以用来生成一个实例,它有两个实现类分别是DefaultTargeter和HystrixTargeter,OpenFiegn使用的是HystrixTarger的实现,可以打断点看下
在这里插入图片描述
因为我给ScreenFeignClient配置了一个Hystrix熔断

@FeignClient(value = "fc-service-screen", configuration = ScreenFeignConfiguration.class, fallback = ScreenFeignClientHystrix.class)
public interface ScreenFeignClient {

打断点可以看到这里获取到fallback里的ScreenFeignClientHystrix
在这里插入图片描述
我们跟进去看看这个源码

private  T targetWithFallback(String feignClientName, FeignContext context,Target.HardCodedTarget target, HystrixFeign.Builder builder,Class fallback) {//又是从FeignContext里获取@FeignClient注解里fallback属性对应的BeanT fallbackInstance = getFromContext("fallback", feignClientName, context,fallback, target.type());//这里应该是关键return builder.target(target, fallbackInstance);}public  T target(Target target, T fallback) {//看build()方法返回的是ReflectiveFeign的实例,然后ReflectiveFeign再new一个实例出来return build(fallback != null ? new FallbackFactory.Default(fallback) : null).newInstance(target);}

feign.hystrix.HystrixFeign.Builder#build(feign.hystrix.FallbackFactory)

Feign build(final FallbackFactory nullableFallbackFactory) {super.invocationHandlerFactory(new InvocationHandlerFactory() {@Overridepublic InvocationHandler create(Target target,Map dispatch) {//再创建原来是你                             return new HystrixInvocationHandler(target, dispatch, setterFactory,nullableFallbackFactory);}});super.contract(new HystrixDelegatingContract(contract));//调用父类也就是Feign的build()方法return super.build();}

我们看下父类Feign.build()干了些啥,feign.Feign.Builder#build

public Feign build() {//生成处理FeignClient接口方法对应的HandlerSynchronousMethodHandler.Factory synchronousMethodHandlerFactory =new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,logLevel, decode404, closeAfterDecode, propagationPolicy);ParseHandlersByName handlersByName =new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,errorDecoder, synchronousMethodHandlerFactory);return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);}

看下ReflectiveFeign的newInstance()方法,它是构建一个FeignClient实例的关键,看下做了什么

@Overridepublic  T newInstance(Target target) {Map nameToHandler = targetToHandlersByName.apply(target);Map methodToHandler = new LinkedHashMap();List defaultMethodHandlers = new LinkedList();//遍历ScreenFeignClient的所有方法for (Method method : target.type().getMethods()) {if (method.getDeclaringClass() == Object.class) {continue;} else if (Util.isDefault(method)) {DefaultMethodHandler handler = new DefaultMethodHandler(method);defaultMethodHandlers.add(handler);methodToHandler.put(method, handler);} else {//然后给每个方法生成对应的Hander,其实就是Feign.build()里面生成的SynchronousMethodHandler,然后//添加到methodToHandlermethodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));}}//通过InvocationHandlerFactory创建JDK的动态代理,如果是Hystrix的就会创建HystrixInvocationHandlerInvocationHandler handler = factory.create(target, methodToHandler);T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),new Class[] {target.type()}, handler);for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {defaultMethodHandler.bindTo(proxy);}return proxy;}

ReflectiveFeign的newInstance()方法主要做了两件事

  1. 扫描FeignClient接口里所有方法,然后为每个方法生成对应的SynchronousMethodHandler
  2. 使用Proxy创建FeignClient实例的动态代理对象

看了这么多静态源码了,打了断点看下动态数据
在这里插入图片描述

在这里插入图片描述

Feign关闭Hystrix熔断生成动态代理的源码

我们先在配置 文件里关闭Feign的Hystrix的支持

feign:hystrix:enabled: false

我们回个头看下HystrixTargeter#target

@Overridepublic  T target(FeignClientFactoryBean factory, Feign.Builder feign,FeignContext context, Target.HardCodedTarget target) {//非Hystrix的进入这个if (!(feign instanceof feign.hystrix.HystrixFeign.Builder)) {//看这个target方法return feign.target(target);}//后面的是开启了Hystrix的逻辑就不看了...}public  T target(Target target) {//先buildreturn build().newInstance(target);}	
public Feign build() {SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,logLevel, decode404, closeAfterDecode, propagationPolicy);ParseHandlersByName handlersByName =new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,errorDecoder, synchronousMethodHandlerFactory);return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);}

最终创建的JDK动态代理对象如下
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
可以看到如果Feign开启了Hystrix支持,创建的动态代理对象的InvocationHandler为HystrixInvocationHandler
没有开启Hystrix支持,创建的动态代理对象的InvocationHandler为FeignInvocationHandler

Feign动态代理处理请求的核心源码

我先关闭掉Feign的Hystrix支持,配置文件设置feign.hystrix.enabled=flase,然后再打断点跟下源码,上面分析也知道对FeignClient的函数调用会进入动态代理对象的FeignInvocationHandler的invoke()方法,我们看下它的源码
FeignInvocationHandler#invoke

@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {if ("equals".equals(method.getName())) {try {Object otherHandler =args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;return equals(otherHandler);} catch (IllegalArgumentException e) {return false;}} else if ("hashCode".equals(method.getName())) {return hashCode();} else if ("toString".equals(method.getName())) {return toString();}//重点是这里,其实就是根据调用方法名找到对应的SynchronousMethodHandler的invoke()方法return dispatch.get(method).invoke(args);}

那么我们继续看SynchronousMethodHandler的invoke()方法源码

@Overridepublic Object invoke(Object[] argv) throws Throwable {//根据方法参数创建RequestTemplate 请求模板的实例对象,这里面包含解析RequestTemplate template = buildTemplateFromArgs.create(argv);Options options = findOptions(argv);Retryer retryer = this.retryer.clone();while (true) {try {//重点在这里return executeAndDecode(template, options);} catch (RetryableException e) {try {retryer.continueOrPropagate(e);} catch (RetryableException th) {。。。continue;}}}

Contract组件解析@RequestParam等SpringMVC注解绑定到HTTP请求参数源码

RequestTemplate template = buildTemplateFromArgs.create(argv);

看这行代码,它里面有Contract组件解析SpringMVC的注解比如@RequestParam,将请求的入参绑定到HTTP请求参数里去

@Overridepublic RequestTemplate create(Object[] argv) {//获取请求中的参数,然后添加到varBuilder中RequestTemplate mutable = RequestTemplate.from(metadata.template());if (metadata.urlIndex() != null) {int urlIndex = metadata.urlIndex();checkArgument(argv[urlIndex] != null, "URI parameter %s was null", urlIndex);mutable.target(String.valueOf(argv[urlIndex]));}Map varBuilder = new LinkedHashMap();for (Entry> entry : metadata.indexToName().entrySet()) {int i = entry.getKey();Object value = argv[entry.getKey()];if (value != null) { // Null values are skipped.if (indexToExpander.containsKey(i)) {value = expandElements(indexToExpander.get(i), value);}for (String name : entry.getValue()) {varBuilder.put(name, value);}}}//这个resolvr方法就是解析@RequestParam,@PathVariable等注解的RequestTemplate template = resolve(argv, mutable, varBuilder);if (metadata.queryMapIndex() != null) {// add query map parameters after initial resolve so that they take// precedence over any predefined valuesObject value = argv[metadata.queryMapIndex()];Map queryMap = toQueryMap(value);template = addQueryMapQueryParameters(queryMap, template);}if (metadata.headerMapIndex() != null) {template =addHeaderMapHeaders((Map) argv[metadata.headerMapIndex()], template);}

为了演示效果,我在fc-service-portal加一个接口如下

@GetMapping("/getUser/{id}")public User getUser(@PathVariable("id")Integer id,@RequestParam(name = "age", required = false) Integer age) {return screenFeignClient.getUser(id, age);}@FeignClient(value = "fc-service-screen", configuration = ScreenFeignConfiguration.class, fallback = ScreenFeignClientHystrix.class)
public interface ScreenFeignClient {@GetMapping("/getUser/{id}")User getUser(@PathVariable("id")Integer id,@RequestParam(name = "age", required = false) Integer age);    

然后fc-service-screen的服务里也加个接口

@GetMapping("/getUser/{id}")public User getUser(@PathVariable("id")Integer id,@RequestParam(name = "age", required = false) Integer age) {return User.builder().id(id).age(age).name("愉乐人生").build();}

重启fc-service-portal和fc-service-screen服务,访问如下请求

http://localhost:8002/getUser/1?age=22

我直接打断点,看下解析后的请求
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
GET /getUser/{id}?age=22 HTTP/1.1
基于SpringMvcContract也会去解析@RequestParam注解,将方法的入参,绑定到http请求参数里去
GET /user/sayHello/1?age=张三 HTTP/1.1

在这里插入图片描述
看这个核心方法,看名字就是执行并解码响应
SynchronousMethodHandler#executeAndDecode

Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {//执行feign的拦截器Request request = targetRequest(template);if (logLevel != Logger.Level.NONE) {logger.logRequest(metadata.configKey(), logLevel, request);}Response response;long start = System.nanoTime();try {//通过LoadBalancerFeignClient执行请求response = client.execute(request, options);} catch (IOException e) {。。。省略}  //如果返回响应正确if (response.status() >= 200 && response.status() < 300) {if (void.class == metadata.returnType()) {return null;} else {//通过Decoder解析响应Object result = decode(response);shouldClose = closeAfterDecode;return result;}} else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) {Object result = decode(response);shouldClose = closeAfterDecode;return result;} else {throw errorDecoder.decode(metadata.configKey(), response);}} 。。。}

执行Feign拦截器的源码

SynchronousMethodHandler#targetRequest
多个拦截器遍历执行apply()方法

Request targetRequest(RequestTemplate template) {for (RequestInterceptor interceptor : requestInterceptors) {interceptor.apply(template);}return target.apply(template);}

Feign与Ribbon整合发送HTTP请求的源码

LoadBalancerFeignClient#execute

@Overridepublic Response execute(Request request, Request.Options options) throws IOException {try {//到这里asUrl是http://fc-service-screen/getUser/1?age=22URI asUri = URI.create(request.url());//取出要访问的服务名 clientName是fc-service-screenString clientName = asUri.getHost();//将请求url中的服务名称给干掉了URI uriWithoutHost = cleanUrl(request.url(), clientName);//基于去掉服务名的url创建一个符合Ribbon的请求RibbonRequestFeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(this.delegate, request, uriWithoutHost);//获取配置文件Ribbon相关的配置IClientConfig requestConfig = getClientConfig(options, clientName);//根据服务名创建先创建一个FeignLoadBalancer,然后执行return lbClient(clientName).executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();}catch (ClientException e) {IOException io = findIOException(e);if (io != null) {throw io;}throw new RuntimeException(e);}}

根据服务名创建一个FeignLoadBalancer的源码看下

private FeignLoadBalancer lbClient(String clientName) {return this.lbClientFactory.create(clientName);
}public FeignLoadBalancer create(String clientName) {//先从缓存一个Map里获取 Map cache = new ConcurrentReferenceHashMap<>();FeignLoadBalancer client = this.cache.get(clientName);if (client != null) {return client;}IClientConfig config = this.factory.getClientConfig(clientName);//这不是Ribbon的核心组件ILoadBalancer吗ILoadBalancer lb = this.factory.getLoadBalancer(clientName);ServerIntrospector serverIntrospector = this.factory.getInstance(clientName,ServerIntrospector.class);client = this.loadBalancedRetryFactory != null? new RetryableFeignLoadBalancer(lb, config, serverIntrospector,this.loadBalancedRetryFactory): new FeignLoadBalancer(lb, config, serverIntrospector);this.cache.put(clientName, client);return client;}

我们打个断点看看
在这里插入图片描述
激动,这不是Ribbon默认的ZoneAwareLoadBalancer吗,Ribbon会和Eureka整合获取Eureka的服务注册表

真正发起HTTP请求的源码

FeignLoadBalancer通过Ribbon负载均衡获取要发送请求的server,然后就要发送HTTP请求了

public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {LoadBalancerCommand command = buildLoadBalancerCommand(request, requestConfig);try {return command.submit(new ServerOperation() {@Override//执行HTTP请求public Observable call(Server server) {URI finalUri = reconstructURIWithServer(server, request.getUri());S requestForServer = (S) request.replaceUri(finalUri);try {return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));} catch (Exception e) {return Observable.error(e);}}}).toBlocking().single();} catch (Exception e) {Throwable t = e.getCause();if (t instanceof ClientException) {throw (ClientException) t;} else {throw new ClientException(e);}}}

这段逻辑由LoadBalancerCommand来执行这段逻辑,LoadBalancerCommand肯定是在某个地方先使用ribbon的ZoneAwareLoadBalancer负载均衡选择出来了一个server,然后将这个server,交给SeerverOpretion中的call()方法去处理
这个call()方法里面,很明显就是发送物理请求最终的一块代码,直接构造出来了具体的http请求的地址,然后基于底层的http通信组件,发送出去了这个请求。应该是ServerOperation对负载均衡选择出来的这个server封装了一下,然后直接基于这个server替换掉请求URL中的fc-service-screen,然后直接拼接出来最终的请求URL地址,然后基于底层的http组件发送请求

获取Ribbon相关配置源码

IClientConfig requestConfig = getClientConfig(options, clientName);
在这里插入图片描述

打断点看下Decoder解码器解码响应后的是什么
在这里插入图片描述

相关内容

热门资讯

四面八方说进博 | 挪威商学院...   第八届进博会期间,挪威第一大商学院BI挪威商学院战略学教授、国际商务学会院士卡尔•费 在接受总台...
共创共赢!进博会成全球创新“入...   站在进博会这个“入海口”,世界看到的还有创新活力。今年特斯拉赛博无人驾驶电动车在进博会实现亚太首...
【青春华章・向西而歌】路生梅:...   路生梅,跨越千里从首都来到陕北佳县。在岁月里褪去青涩、鬓染霜花,把心留在这片土地,留给了这里的父...
一道弧线,治水千年?中国的“生...   它没有钢筋水泥,却让洪水低头;它无需电力驱动,却灌溉万亩良田。1500多年来,丽水通济堰用一条弧...
华东地区首架AS700载人飞艇...   被誉为“空中白鲸”的“祥云”AS700载人飞艇近日飞抵浙江省绍兴市越城区鉴水科技城,标志着华东地...
冬季皮肤干痒每天敷面膜行么?保...   随着冬季到来,不少人的皮肤就开始“闹别扭”:洗脸后脸颊紧绷得像糊了层纸,小腿一挠就掉白屑,还有人...
冬季“中风”高发 有些致病风险...   节气已过立冬,冬天是脑卒中的高发季。脑卒中俗称“中风”,是我国成年人致死、致残的首要原因,成年人...
H3N2甲流凶猛!自救药怎么选...   当前  H3N2甲型流感病毒  进入高发传播期  常导致高烧  (39—40℃)  剧烈头痛  ...
伊朗将启动货币改革 删除面值末...   新华社德黑兰11月9日电(记者陈霄 沙达提)伊朗伊斯兰共和国通讯社8日报道,伊朗宪法监护委员会已...
视频丨全运会历史上首次!电光水...   历届运动会开幕式的火炬点燃都充满悬念。昨晚(9日),在十五运会的开幕式上,手擎“绽放”火炬的火炬...