Feign是什么?

简单来说,feign是用在微服务中,各个微服务间的调用。它是通过声明式的方式来定义接口,而不用实现接口。接口的实现由它通过spring bean的动态注册来实现的。

feign让服务间的调用变得简单,不用各个服务去处理http client相关的逻辑。并且它里面集成了ribbon用来做负载均衡,通过集成了hystrix用来做服务熔断和降级。

在feign的使用中,我们主要用到它的两个注解,下面一一来说明。

注解

1、@EnableFeignClients

  • 用于表示该客户端开启Feign方式调用

  • 创建一个关于FeignClient的工厂Bean,这个工厂Bean会通过@FeignClient收集调用信息(服务、接口等),创建动态代理对象

2、@FeignClient

负责标识一个用于业务调用的Client,给FactoryBean提供创建代理对象,提供基础数据(类名、方法、服务名、URI等),作用是提供这些静态配置

实现原理

一般对于一个spring boot服务来说,如果要使用feign,都会这样定义:

@Configuration
@EnableScheduling
@EnableDiscoveryClient
@EnableFeignClients(value = {"com.ts"})
@MapperScan(value = {"com.ts.xx.xx"}, nameGenerator = FullBeanNameGenerator.class)
public class xxxxAutoConfig {
}

这里就使用到了上面说的EnableFeignClients这个注解,这个注解有什么用呢?我们进入该注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {}

可以看到该注解导入了FeignClientsRegistrar类,我们进入其中:

class FeignClientsRegistrarimplements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {}@Overridepublic void registerBeanDefinitions(AnnotationMetadata metadata,BeanDefinitionRegistry registry) {registerDefaultConfiguration(metadata, registry);registerFeignClients(metadata, registry);}

整个过程大概就是,通过配置类,或者package路径做扫描,收集FeignClient的静态信息,每个Client会把他的基本信息,类名、方法、服务名等绑定到FactoryBean上,这样就就具备了生成一个动态代理类的基本条件。

这里穿插2个知识点

1、spring bean的动态注册

在spring中有两类bean:

  • 普通的bean:通过xml配置或者注解配置

  • 工厂bean:也是一个Bean,这个Bean我们业务中不会直接用到,它主要是用于生成其他的一些Bean,内部进行了高度封装,非常容易实现配置化的管理,屏蔽了实现细节。动态注册是解决什么问题,根据客户端的配置动态的,也就是可以按需做bean的注册。

1.1 使用方式

实现ImportBeanDefinitionRegistrar接口,重点实现registerBeanDefinitions方法,该接口需要配合@Configuration@Import注解,

  • @Configuration定义Java格式的Spring配置文件,

  • @Import注解导入实现了ImportBeanDefinitionRegistrar接口的类。这也就是上面我们看到的FeignClientsRegistrar

那feign为什么要这样做呢?因为需要生成不同的代理类的实现bean。

1.2 实现原理

所有实现了该接口的类的都会被ConfigurationClassPostProcessor处理,ConfigurationClassPostProcessor实现了BeanFactoryPostProcessor接口,所以ImportBeanDefinitionRegistrar中动态注册的bean是优先于依赖它的bean初始化的,也能被aop、validator等机制处理。

2、FactoryBean

FactoryBean的特殊之处在于它可以向容器中注册两个Bean,一个是它本身,一个是FactoryBean.getObject()方法返回值所代表的Bean。

我们知道:在Spring容器启动阶段,会调用到refresh()方法,在refresh()中有调用了finishBeanFactoryInitialization()方法,最终会调用到beanFactory.preInstantiateSingletons()方法。

getObjectForBeanInstance()方法中会先判断bean是不是FactoryBean,如果不是,就直接返回Bean。如果是FactoryBean,且name是以&符号开头,那么表示的是获取FactoryBean的原生对象,也会直接返回。如果name不是以&符号开头,那么表示要获取FactoryBean中getObject()方法返回的对象。会先尝试从FactoryBeanRegistrySupport类的factoryBeanObjectCache这个缓存map中获取,如果缓存中存在,则返回,如果不存在,则去调用getObjectFromFactoryBean()方法。

getObjectFromFactoryBean()方法中,主要是通过调用doGetObjectFromFactoryBean()方法得到bean,然后对bean进行处理,最后放入缓存。而且还会针对单例bean和非单例bean做区分处理,对于单例bean,会在创建完后,将其放入到缓存中,非单例bean则不会放入缓存,而是每次都会重新创建。

注册FeignClient

我们回到FeignClientsRegistrar类中,下面接着看下注册FeignClient的实现,代码如下:

public void registerFeignClients(AnnotationMetadata metadata,BeanDefinitionRegistry registry) {//创建一个类扫描器ClassPathScanningCandidateComponentProvider scanner = getScanner();scanner.setResourceLoader(this.resourceLoader);Set<String> basePackages;//获取EnableFeignClients注解包含的属性Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());//这是一个FeignClient注解过滤器AnnotationTypeFilter 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<String> 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) {//先扫描出含有@FeignClient注解的类Set<BeanDefinition> 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<String, Object> attributes = annotationMetadata.getAnnotationAttributes(FeignClient.class.getCanonicalName());//获取服务名称String name = getClientName(attributes);registerClientConfiguration(registry, name,attributes.get("configuration"));//循环对使用@FeignClient注解的不同的类,做工厂bean注册registerFeignClient(registry, annotationMetadata, attributes);}}}}private void registerFeignClient(BeanDefinitionRegistry registry,AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {//含有@FeignClient该类的名称String className = annotationMetadata.getClassName();//创建一个BeanDefinitionBuilder,内含AbstractBeanDefinition,指定待创建的Bean的名字是FeignClientFactoryBeanBeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);validate(attributes);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";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);}//BeanDefinitionReaderUtils.registerBeanDefinitionpublic static void registerBeanDefinition(BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)throws BeanDefinitionStoreException {// Register bean definition under primary name.String beanName = definitionHolder.getBeanName();//BeanDefinitionRegistry.registerBeanDefinition实现具体的注册,会告知他需要注册的类名、以及AbstractBeanDefinitionregistry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());// Register aliases for bean name, if any.String[] aliases = definitionHolder.getAliases();if (aliases != null) {for (String alias : aliases) {registry.registerAlias(beanName, alias);}}}

创建代理类

在上面提到的FactoryBean,可以看到FeignClientFactoryBean继承了它,通过调用实现类的getObject完成代理类的创建:

@Overridepublic Object getObject() throws Exception {return getTarget();}<T> T getTarget() {FeignContext context = applicationContext.getBean(FeignContext.class);Feign.Builder builder = feign(context);//先校验基础属性,基础属性是在FeignClientsRegistrar中给动态Bean,添加属性addPropertyValue时候赋值的//URL为空,则使用http://serviceName的方式拼接if (!StringUtils.hasText(this.url)) {String url;if (!this.name.startsWith("http")) {url = "http://" + this.name;}else {url = this.name;}url += cleanPath();//创建动态的代理对象,采用JDK动态代理return (T) loadBalance(builder, context, new HardCodedTarget<>(this.type,this.name, url));}//含有url,也就是FeignClient的注解接口里是一个绝对地址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) {// not load balancing because we have a url,// but ribbon is on the classpath, so unwrapclient = ((LoadBalancerFeignClient)client).getDelegate();}builder.client(client);}Targeter targeter = get(context, Targeter.class);return (T) targeter.target(this, builder, context, new HardCodedTarget<>(this.type, this.name, url));}

从上面代码可以知道,如果用户在FeignClient的注解中直接使用了URL,这种方式一般用于调试环境,直接指定一个服务的绝对地址,这种情况下不会走负载均衡,走默认的Client,代码如下:

@Overridepublic Response execute(Request request, Options options) throws IOException {HttpURLConnection connection = convertAndSend(request, options);return convertResponse(connection).toBuilder().request(request).build();}

如果用户在FeignClient中使用了seriveName,那么请求地址将会是http://serviceName,这种情况下是需要走负载均衡的,通过如下代码发现Feign的负载均衡也是基于Ribbon实现:

public Response execute(Request request, Request.Options options) throws IOException {try {URI asUri = URI.create(request.url());String clientName = asUri.getHost();URI uriWithoutHost = cleanUrl(request.url(), clientName);FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(this.delegate, request, uriWithoutHost);IClientConfig requestConfig = getClientConfig(options, clientName);return lbClient(clientName).executeWithLoadBalancer(ribbonRequest,requestConfig).toResponse();}catch (ClientException e) {IOException io = findIOException(e);if (io != null) {throw io;}throw new RuntimeException(e);}}

相关配置

1、服务配置

大多数情况下,我们对于服务调用的超时时间可能会根据实际服务的特性做 一 些调整,所以仅仅依靠默认的全局配置是不行的。

在使用SpringCloud Feign的时候,针对各个服务客户端进行个性化配置的方式与使用SpringCloud Ribbon时的配置方式是 一 样的, 都采用. ribbon.key=value 的格式进行 设置。

在定义Feign客户端的时候, 我们使用了@FeignClient注解。在初始化过程中,SpringCloud Feign会根据该注解的name属性或value属性指定的服务名, 自动创建一 个同名的Ribbon客户端。

也就是说,在之前的示例中,使用@FeignClient(value= "cloud-provider")来创 建 Feign 客 户 端 的 时 候 , 同时也创建了一个 名为cloud-provider的Ribbon客户端。既然如此, 我们就可以使用@FeignClient注解中的name或value属性值来设置对应的Ribbon参数, 比如:

cloud-provider.ribbon.ConnectTimeout = 500 //请求连接的超时时间。
cloud-provider.ribbon.ReadTimeout = 2000  //请求处理的超时时间。
cloud-provider.ribbon.OkToRetryOnAllOperations = true //对所有操作请求都进行重试。
cloud-provider.ribbon.MaxAutoRetriesNextServer = 2 //切换实例的重试次数。
cloud-provider.ribbon.MaxAutoRetries = 1 //对当前实例的重试次数。

2、日志配置

Spring Cloud Feign 在构建被 @FeignClient 注解修饰的服务客户端时,会为每 一 个客户端都创建 一 个 feign.Logger 实例,我们可以利用该日志对象的 DEBUG 模式来帮助分析 Feign 的请求细节。

可以在 application.properties 文件中使用 logging.level. 的参数配置格式来开启指定 Feign 客户端的 DEBUG 日志, 其中 为 Feign 客户端定义接口的完整路径, 比如针对本文中我们实现的 HelloService 可以按如下配置开启:

logging.level.com.wuzz.demo.HelloService = DEBUG

但是, 只是添加了如上配置, 还无法实现对 DEBUG 日志的输出。这时由于 Feign 客户端默认的 Logger.Level 对象定义为 NONE 级别, 该级别不会记录任何 Feign 调用过程中的信息, 所以我们需要调整它的级别, 针对全局的日志级别, 可以在应用主类中直接加入 Logger.Level 的 Bean 创建, 具体如下:

@Bean
Logger.Level feignLoggerLevel() {return Logger.Level.FULL;
}

或者添加个配置类:

@Configuration
public class FullLogConfiguration {
@Bean
Logger.Level feignLoggerLevel() {return Logger.Level.FULL;
}
@FeignClient(name = "cloud-provider", configuration = FullLogConfiguration.class)
public interface TestService {}

3、服务降级

根据目标接口,创建一个实现了FallbackFactory的类

@Component
public class HystrixClientService implements FallbackFactory<ClientService> {@Overridepublic ClientService create(Throwable throwable) {return new ClientService() {@Overridepublic String test() {return "test sdfsdfsf";}};}
}

在目标接口上的@FeignClient中添加fallbackFactory属性值

@FeignClient(value ="cloud-provider", fallbackFactory = HystrixClientService.class)
public interface ClientService {@RequestMapping(value ="/test",method= RequestMethod.GET)String test() ;
}

修改 application.yml ,添加一下

feign:hystrix:enabled: true

SpringCloudFeign原理剖析相关推荐

  1. socket之send和recv原理剖析

    socket之send和recv原理剖析 1. 认识TCP socket的发送和接收缓冲区 当创建一个TCP socket对象的时候会有一个发送缓冲区和一个接收缓冲区,这个发送和接收缓冲区指的就是内存 ...

  2. fastText的原理剖析

    fastText的原理剖析 1. fastText的模型架构 fastText的架构非常简单,有三层:输入层.隐含层.输出层(Hierarchical Softmax) 输入层:是对文档embeddi ...

  3. lua游戏脚本实例源码_Lua与其他宿主语言交互原理剖析

    Lua与其他宿主语言交互原理剖析 题外话:今天周末,刚好在家有时间就把我这次项目组内部分享的文章贴出来,分享给大家,同时也方便以后自己翻阅. 一. Lua简介 目标:Lua语言本身是用C语言来编写开发 ...

  4. Go语言底层原理剖析

    作者:郑建勋 出版社:电子工业出版社 品牌:博文视点 出版时间:2021-08-01 Go语言底层原理剖析

  5. 彻底搞透视觉三维重建:原理剖析、代码讲解、及优化改进

    视觉三维重建 = 定位定姿 + 稠密重建 + surface reconstruction +纹理贴图.三维重建技术是计算机视觉的重要技术之一,基于视觉的三维重建技术通过深度数据获取.预处理.点云配准 ...

  6. Elasticsearch分布式一致性原理剖析(一)-节点篇

    2019独角兽企业重金招聘Python工程师标准>>> 摘要: ES目前是最流行的开源分布式搜索引擎系统,其使用Lucene作为单机存储引擎并提供强大的搜索查询能力.学习其搜索原理, ...

  7. java 反序列化 ysoserial exploit/JRMPListener 原理剖析

    目录 0 前言 1 payloads/JRMPClient 1.1 Externalizable 1.2 生成payload 1.3 gadget链分析 2 exploit/JRMPListener ...

  8. 统计学习方法|支持向量机(SVM)原理剖析及实现

    欢迎直接到我的博客查看最近文章:www.pkudodo.com.更新会比较快,评论回复我也能比较快看见,排版也会更好一点. 原始blog链接: http://www.pkudodo.com/2018/ ...

  9. 统计学习方法|逻辑斯蒂原理剖析及实现

    欢迎直接到我的博客查看最近文章:www.pkudodo.com.更新会比较快,评论回复我也能比较快看见,排版也会更好一点. 原始blog链接: http://www.pkudodo.com/2018/ ...

最新文章

  1. 在symfony2项目中100%提升doctrine的性能
  2. Check failed: status == CUDNN_STATUS_SUCCESS (4 vs. 0) CUDNN_STATUS_INTERNAL_ERROR
  3. shell脚本——字符串 数组
  4. led显示屏背景墙设计_全彩LED显示屏比例设计因素都有哪些?
  5. 124. Leetcode 583. 两个字符串的删除操作 (动态规划- 字符串系列)
  6. [CISCN2018]crackme-java
  7. Codeforces Round #494 (Div. 3)【未完结】
  8. AtCoder Regular Contest 100 E - Or Plus Max Sos dp
  9. Mybatis源码研究5:数据源的实现
  10. Swing basic
  11. 暴力测试也疯狂——论Python代码优化
  12. javaweb——总结
  13. 利用ziparchive压缩文件、zip_file解压文件
  14. 英文版 《UNIX 网络编程.卷一 (UNIX Network Programming .volume1. 3rd edition)》(pdf)高清精校版下载
  15. 【低代码平台浅析】钉钉宜搭
  16. PhotoShop CS6破解方法及文件下载
  17. 张亮:Sharding-Sphere成长记
  18. echarts 饼图移动端_使用Echarts实现饼图
  19. 行为识别阅读笔记(paper + parted code):Beyond Frame-level CNN Saliency-Aware 3-D CNN with LSTM for Video Acti
  20. word2016开机后首次打开非常慢_终于找到了电脑开机时间长的原因了,一看就会,一招到位...

热门文章

  1. android adb push、adb pull 使用
  2. pytorch学习8-实现卷积神经网络
  3. C语言读取文件内容,保存到数组里面
  4. linux 运行class文杰,Linux详细教程
  5. FFMpeg安装步骤
  6. better-scroll的学习和使用
  7. Html2Image在Linux中文方块乱码问题(亲测可用)
  8. 网络直播,对现代人的一些影响,和网络直播的一些内幕
  9. matlab 结构体
  10. 程序员这个职业赚钱吗?能赚多少钱?