SpringCloud系列之Feign-5.@EnableFeignClients底层机制深度解析
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringCloud系列之Feign-5.@EnableFeignClients底层机制深度解析相关的知识,希望对你有一定的参考价值。
参考技术A@EnableFeignClients 源码比较值得一读,读完之后我们就学会了如何自己写一个注解并成功运用起来了
1.首先我们进入到 @EnableFeignClients注解里面可以看到这个注解里面声明了几个属性,通过名称大概可以看到比如basePackages应该是包路径,value的话应该是个basePackages别名,我们暂且不管,看下这个注解上面有个@Import的注解,点进去这个类来看一下
2.如图可以看到FeignClientsRegistrar这个类实现了三个Spring的类,根据名称大概猜一下,第一个应该是关于类定义注册的类,第二个第三个相信大家可能用过或者了解过就是Spring的Aware的一些类,大致就是加载资源或者环境变量的类
那我们看一下ImportBeanDefinitionRegistrar类里面是什么:
可以看到里面定义了一个方法,那么我们重新返回到FeignClientsRegistrar看下他是怎么实现这个方法的:
这个方法主要干了两件事儿,第一个方法是注册了默认的配置信息,第二个就是注册FeignClients,我们挨个一个个详细的看下:
到了较为详细的源码时候,如果看不懂,我们最好是打断点,当我们启动了Eureka-server,Eureka-client,然后再启动Feign-consumer的时候,断点就可以进来,我们可以看到一些传参的信息:
可以看到这个metedata里面的数据刚好就是在启动类上面的三个注解,并且还带有三个注解的属性信息,下面再给大家看下主类对照下就懂了:
然后我们继续往下面学习:
这一步就比较好理解了,我们拿到EnableFeignClients这个注解的属性信息。
然后就是拼接了一个名称,这个名称就是启动类的前端加了个default.而已,然后registry没变还是传参过来,defaultAttrs.get("defaultConfiguration")这个属性从刚刚断点来看也是空的。调用了一个registerClientConfiguration方法:
这个方法就是使用了Spring的BeanDefinitionBuilder把FeignClientSpecification这个bean给注册到Spring容器中了。
然后我们继续放回到主方法中看下一个方法:
这个方法根据名称registerFeignClients来说应该就是注册FeignClients类了,进入方法中,第一个scanner我们看下:
有点看不懂,没关系,猜一下,可能是扫描类的工具把。
我们继续王下面走,scanner加载了一个resourceLoader这个类,这个类我们可以查一下,他是Spring框架中与资源相关的类,然后再往下看
下面还是获取主类中的注解EnableFeignClients的属性信息
再往下,我们可以从图中看到在属性中获取关于clients的信息,但是没有,然后scanner就加了一个类似过滤器的东东,然后调用了getBasePackages的方法
下面我们看下getBasePackages方法:
看过之前的方法,这个方法就好理解了,首先就还是获取EnableFeignClients的所有属性信息,然后把值都给取出来,取得属性分别是value还是basePackages等关于包路径的属性值,如果都没有获取到,就获取一个默认的包路径
这个包路径断点可以看到就是主类的包路径,所以整体上看,这些逻辑就是首先看注解中有没有关于FeignClient的包路径信息,如果没有配置,那程序就准备从主程序所在的包路径下找所有的FeignClient了。
我们再详细看下:
然后继续看这个registerClientConfiguration方法:
这个方法我们之前看过就是把某个类加载到Spring中所以继续下一步,看registerFeignClient方法:
获取到这个FeignClient的所有属性之后,我们就进行数据处理,把属性信息都赋值给definition
这个方法表示我们把这个类以 按照类型注入 作为属性,然后
这块逻辑就是为了防止两个有同一个父类的FeignClient出现问题所做的,我们应该都遇到过一个问题就是 使用 @Autowire注入类的时候发现报错,说是有两个类不知道注入哪一个,而如果其中一个有@primary注解的话,spring是会优先注入这个类的。
下面就没什么了,直到最后执行了
整个@EnableFeignClients的实现到此执行完毕,这个注解的源码相对来说看起来算是比较清晰明了了,而且对于我们如果有做一个新注解的需求的话,完全可以参照着做,非常具有模板意义。
重学SpringCloud系列三之服务注册与发现---下
重学SpringCloud系列三之服务注册与发现---下
- 白话服务注册与发现
- DiscoveryClient服务发现
- Eureka集群环境构建(linux)
- Eureka集群多网卡环境ip设置
- Eureka集群服务注册与安全认证
- Eureka自我保护与健康检查
- 主流服务注册中心对比(含nacos)
- zookeeper概念及功能简介
- zookeeper-linux集群安装
- zookeeper服务注册与发现
- consul概念及功能介绍
- consul-linux集群安装
- consul服务注册与发现
- 通用-auatator导致401问题
白话服务注册与发现
公益图书馆例子
笔者不想直接用专业的术语来说明“微服务注册与发现”,所以我们来看生活中的一个案例:“公益图书馆”。
随着人们生活水平的不断提高,追求精神食粮的朋友也越来越多。笔者曾经在一些城市看见过公益图书馆,其运行逻辑是:一些公益组织和个人提供一块场所,然后由组织内的人向图书馆内捐书。捐出的书越多,一段时间内能够借阅的书也就越多。这种做法有助于大家分享图书、节约资金、交流读书心得。那我们来看一下几个关键环节:
- 捐书:组织内的人向公益图书馆捐书,是不是直接将书放到书架上就完事了呢?当然不是,是先向图书管理系统记录一下捐书的人、书名、捐书的时间等信息,再将书放到书架上。
- 借书:借书的人通常是通过图书管理系统的一个小程序查询图书,然后取书,全靠自觉。图书可能存在多个副本(多人捐的同一种书),借书的人会根据书籍状态择优选择。
- 这其中非常重要的一个角色就是图书管理系统,为大家捐书、借书提供了数据支持和集中管理功能。
- 兼职图书管理员定期维护图书,将破损图书从图书管理系统中下架维护。
其实上面的这个“公益图书馆的例子”就是典型的服务注册与发现:
- 每一本图书就是一个服务,捐书的过程就是“服务注册”的过程。
- 借书的查询图书的过程就是“服务发现”的过程。
- 其中最重要的角色:图书管理系统及管理员,就是服务注册中心或者服务注册平台。
- 捐书者可能同时是借书者。进行服务注册的微服务节点,同时可能也使用服务发现机制发现其他微服务。
- 捐书是主动行为,不是被动行为。这和微服务的注册是一样的,微服务必须在启动的时候向服务注册组件进行主动注册。这样做的目的就是降低数据维护成本,不需要专人维护注册数据。
- 图书下架是被动的,不是主动的,不是捐书的人将其下架。微服务也是一样,当服务出现故障发生问题,服务发现注册组件应具备将服务下线的能力。
- 图书管理员可以检查图书并下架,这过程在服务注册与发现中被称为:健康检查
- 对于同一种图书可能存在多个同样的副本,由使用者择优选择借哪一本书。对于服务发现获得的结果:同一种服务的多个副本的情况,由服务调用者择优决定使用哪一个服务副本。这种服务方式比较专业的说法是:客户端负载均衡。
与客户端负载均衡相对的方法就是服务端负载均衡,如果上面的例子中借书过程一本书有多个副本,由图书管理员或系统决定借书者借其中的哪一个副本,这个就是服务端负载均衡。如:nginx、haproxy等就是服务端负载均衡。
服务注册与发现
- 服务注册 -服务在中央注册表中注册其服务位置的过程。通常注册其主机和端口,有时还注册认证凭证,协议,版本号和或环境信息。
- 服务发现 -客户端应用程序查询中央注册表以了解服务位置的过程。
- 维护中央注册表的角色被称为服务注册平台或者服务注册中心
服务注册
当一个微服务启动的时候,必须主动向服务注册中心注册其服务地址,以供其他微服务查询调用。图中橘黄色为服务注册中心,绿色为微服务节点。
客户端负载均衡
- 当一个微服务有多个实例的时候,由调用者从服务注册中心获取注册服务列表
- 调用者拿到"注册服务列表"之后,决定访问哪一个服务实例。
Spring Cloud常用的服务注册中心
- Eureka:Spring Cloud的大儿子,出生的时候条件一般,长大后素质有限
- Nacos:后起之秀,曾经Spring Cloud眼中“别人家的孩子”,已经纳入收养范围(孵化项目)。
- Apache Zookeeper:关系户,与hadoop关系比较好
- Consul:关系户,曾经与docker关系比较好
- etcd:关系户,与kubernetes关系比较好
如果你的应用已经使用到了hadoop、kubernetes、docker,在Spring Cloud实施过程中可以考虑使用其关系户组件,避免搭建两套注册中心,节省资源。但是二者兼容使用说说容易,真正用起来还需要功夫。目前看,笔者觉得最佳选择应该是Nacos。
这里可以先简单的了解一下常见的这些服务注册中心,后面的章节我们会逐步的详细介绍。
DiscoveryClient服务发现
在前面的章节,我们已经为大家介绍了
- 服务注册的方式,在服务启动的时候主动向服务注册中心注册服务信息
- 服务消费者可以以负载均衡的方式,远程调用服务提供者提供的服务。
服务消费者调用微服务之前,需要向服务注册中心,获取注册服务列表及服务信息。这个过程就是“服务发现”,那么服务发现是通过什么类实现的?服务列表及服务信息又包含哪些内容?本节就带着大家来解开这样的疑惑!
DiscoveryClient测试用例
DiscoveryClient 代表的就是:服务发现操作对象。
public interface DiscoveryClient extends Ordered
int DEFAULT_ORDER = 0;
String description();
List<ServiceInstance> getInstances(String serviceId);
List<String> getServices();
default int getOrder()
return 0;
它有两个核心方法:
- getServices获取在服务注册中心,注册的所有服务的id。比如:ASERVICE-RBAC、ASERVICE-SMS。
- getInstances根据服务id,获取该服务的所有启动实例的注册信息。即:一个微服务的多个副本的注册信息。
下面是一个基于Spring、Junit的测试用例,使用上面两个方法来实现服务发现。
@ExtendWith(SpringExtension.class)
@SpringBootTest
public class DiscoveryClientTest
@Resource
private DiscoveryClient discoveryClient; // 进行eureka的发现服务
@Test
void discoveryClientTest()
//获取服务Id
List<String> services = discoveryClient.getServices();
services.forEach(System.out::println);
//获取每个服务的多个启动实例的注册信息。
for (String service:services)
discoveryClient.getInstances(service)
.forEach(s ->
System.out.println("InstanceId=" + s.getHost() + ":" + s.getPort());
System.out.println("Host:Port="+ s.getHost() + ":" + s.getPort());
System.out.println("Uri=" + s.getUri());
System.out.println("InstanceId=" + s.getInstanceId());
System.out.println("Schema=" + s.getScheme());
System.out.println("ServiceId=" + s.getServiceId());
System.out.println("Metadata="+ s.getMetadata());
);
结合测试结果的打印,可以更清楚的知道服务注册及发现相关的信息。理解DiscoveryClient 及其方法的作用。控制台打印结果如下:
两种服务发现注解的区别
在Spring Cloud中实现服务发现可以使用两种注解:@EnableDiscoveryClient和@EnableEurekaClient
,两者的用法基本上是一样的。但存在区别,简单地说:
- 如果服务注册中心是eureka,就需要在服务启动类加上
@EnableEurekaClient
注解,实现服务发现。 - 如果是其他的注册中心,那么更推荐使用
@EnableDiscoveryClient
,该注解更加的通用。
在Hello-microservice章节,实现微服务向服务注册中心注册的时候,我们使用了@EnableEurekaClient
,是因为我们当时搭建的服务注册中心是基于eureka搭建的。Spring Cloud中还有很多的其他服务注册中心的选项,比如:consul、zookeeper、nacos,这时就不能使用@EnableEurekaClient
注解了,需要使用@EnableDiscoveryClient
注解。
Eureka集群环境构建(linux)
服务注册中心在整个微服务体系中,至关重要!如果服务注册中心挂了,整个系统都将崩溃。所以服务注册中心通常不会被部署为单点应用,而是采用集群的部署方式,其中个别节点挂掉不影响整个系统的运行。
下面,我们就来为大家介绍,如何基于CentOS7服务器构建eureka服务注册中心。
CentOS7(linux)环境准备
主机名称 | 主机ip |
---|---|
peer1 | 192.168.161.3 |
peer2 | 192.168.161.4 |
peer3 | 192.168.161.5 |
并且在安装eureka服务注册中心之间,需要将服务器时间同步,不能相差太多,否则eureka服务有可能启动失败。可以使用ntp
进行时间同步。如:
ntpdate ntp.api.bz
profile配置文件修改
application-peer1.yml
- hostname为peer1
- defaultZone为peer2和peer3
- 下面配置我们之所以重写了health-check-url(健康检查路径),是因为设置了context-path。默认的健康检查路径是以“/”为项目的context-path。所以我们需要修改为server.servlet.context-path配置的值。
- 因为我们设置了context-path,所以defaultZone访问端点有两个eureka,第一个eureka是我们配置的context-path。如果不设置context-path,defaultZone:http://peer2:8761/eureka/,http://peer3:8761/eureka/
- 在eureka集群搭建过程中,fetch-registry和register-with-eureka一定设置为true。
这两个值之所以设置为true,目的是让eureka集群之间实现互相注册,互相心跳健康状态,从而达到集群的高可用。
#是否从其他实例获取服务注册信息,因为这是一个单节点的EurekaServer,不需要同步其他的EurekaServer节点的数据,所以设置为false;
fetch-registry: false
#表示是否向eureka注册服务,即在自己的eureka中注册自己,默认为true,此处应该设置为false;
register-with-eureka: false
把它们设置为false,是能解决你可能遇到的一些集群环境问题。这就好比你腿疼,你把腿砍了是不疼了,但你还能走路么。我们要的是让腿不疼,而不是把腿砍掉。“把腿砍了”这就不是“高可用”集群了,相当于你搭建了多个eureka server单点,这是“掩耳盗铃”的做法。
server:
port: 8761
servlet:
context-path: /eureka
spring:
application:
name: eureka-server
eureka:
instance:
hostname: peer1
health-check-url: http://$eureka.instance.hostname:$server.port/$server.servlet.context-path/actuator/health
client:
#从其他两个实例同步服务注册信息
fetch-registry: true
#向其他的两个eureka注册当前eureka实例
register-with-eureka: true
service-url:
defaultZone: http://peer2:8761/eureka/eureka/,http://peer3:8761/eureka/eureka/
application-peer2.yml
- hostname为peer2
- defaultZone为peer1和peer3
server:
port: 8761
servlet:
context-path: /eureka
spring:
application:
name: eureka-server
eureka:
instance:
hostname: peer2
health-check-url: http://$eureka.instance.hostname:$server.port/$server.servlet.context-path/actuator/health
client:
#从其他两个实例同步服务注册信息
fetch-registry: true
#向其他的两个eureka注册当前eureka实例
register-with-eureka: true
service-url:
defaultZone: http://peer1:8761/eureka/eureka/,http://peer3:8761/eureka/eureka/
application-peer3.yml
- hostname为peer3
- defaultZone为peer1和peer2
server:
port: 8761
servlet:
context-path: /eureka
spring:
application:
name: eureka-server
eureka:
instance:
hostname: peer3
health-check-url: http://$eureka.instance.hostname:$server.port/$server.servlet.context-path/actuator/health
client:
#从其他两个实例同步服务注册信息
fetch-registry: true
#向其他的两个eureka注册当前eureka实例
register-with-eureka: true
service-url:
defaultZone: http://peer1:8761/eureka/eureka/,http://peer2:8761/eureka/eureka/
为eureka server加入actuator
spring-boot-starter-actuator是为Spring Boot服务提供相关监控信息的包。因为我们的eureka server要互相注册,并检查彼此的健康状态,所以这个包必须带上。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
CentOS7环境下部署eureka集群
如果你的网络环境内没有DNS,需要配置/etc/hosts文件,将主机名称与ip地址关联。这一步必须要做,否则linux主机之间通过hostname访问eureka服务将无效,每台eureka server主机上都要执行。
192.168.161.3 peer1
192.168.161.4 peer2
192.168.161.5 peer3
开放防火墙端口(CentOS7),每台eureka server主机上都要执行。
firewall-cmd --zone=public --add-port=8761/tcp --permanent
firewall-cmd --reload
- 第一条命令式在防火墙开放8761端口
- 第二条命令是是开放端口重新加载,使生效
将dhy-server-eureka通过maven打包,然后上传CentOS主机,启动Eureka服务注册中心集群
# 在peer1主机执行
nohup java -jar -Dspring.profiles.active=peer1 dhy-server-eureka-1.0.jar &
# 在peer2主机执行
nohup java -jar -Dspring.profiles.active=peer2 dhy-server-eureka-1.0.jar &
# 在peer3主机执行
nohup java -jar -Dspring.profiles.active=peer3 dhy-server-eureka-1.0.jar &
访问测试
访问http://192.168.161.3:8761/eureka/,即:访问peer1的eureka服务。可以见到DS Replicas中已经注册了peer3、peer2。
同理:
- 访问http://192.168.161.4:8761/eureka/,即:访问peer2的eureka服务。可以见到DS Replicas中已经注册了peer1、peer3。
- 访问http://192.168.161.5:8761/eureka/,即:访问peer3的eureka服务。可以见到DS Replicas中已经注册了peer1、peer2。
出现上面的这种eureka server之间互相注册的效果,表示我们的eureka服务注册中心集群模式搭建成功了!那么恭喜你,你是一个幸运儿。在实际的生产环境中,网络及主机环境往往更复杂,搭建过程的参数调整也更加复杂。
其他需要注意的点:
出现 unavailable-replicas 问题,首先要去检查一下你的health-check-url
是否能正常响应。如果没有设置context-path
,默认是:http://ip:端口/actuator/health
。UP状态表示处于可用状态。
如果健康检查没有问题:
1.是否开启了register-with-eureka=true
和fetch-registry=true
2.eureka.client.serviceUrl.defaultZone
配置项的地址,不能使用localhost
,要使用ip
或域名。或者可以通过hosts
或者DNS
解析的主机名称hostname
。
3.spring.application.name
要一致,不配置也可以,配置了要一致
4.默认情况下,Eureka
使用 hostname(如:peer1、peer2、peer3)
进行服务注册,以及服务信息的显示(eureka web
页面),那如果我们希望使用 IP
地址的方式,该如何配置呢?答案就是eureka.instance.prefer-ip-address=true
。当设置prefer-ip-address: true
时 ,修改配置defaultZone:http://你的IP:9001/eureka/
。如果此时你仍然使用http://peer1:8761/eureka
会导致健康检查失败。
Eureka集群多网卡环境ip设置
在上一小节,我们为大家讲解了如何在linux环境下搭建集群式Eureka服务注册中心。有的朋友可能会遇到下面的问题(导致服务注册失败、健康检查失败):
上图中蓝色部分:大家可以明确的看到eureka server的服务绑定的ip是10.0.2.15?这是为什么?我们上一节中,也没有使用过这个ip啊,我们使用的是192.168.161.3。这是因为我的CentOS服务器上有多个网卡,还有一些docker相关的虚拟网卡。“多网卡”在生产环境上是非常常见的情况。怎么让eureka server服务绑定实例我们期望它绑定的网卡
?
配置实现
首先来看一下,我的服务器(虚拟机)上面的网卡设备,一共五个(虚拟的)。
- docker0虚拟网卡是因为我在这台机器上安装了docker
- enp0s3网卡是一个NAT网络的网卡,虚拟机上常用。大家注意它的ip是10.0.2.15。
- enp0s8才是我们真正需要使用的本地网络。
那我们现在要做的就是通知spring cloud
,我们部署的微服务希望ip
是192.168
的本地网段。不要使用docker0和enp0s3
的网段。
-
spring.cloud.inetutils.preferredNetworks
表示我们期望使用的网段,可以使用正则表达式 -
spring.cloud.inetutils.ignoredInterfaces
表示我们希望忽略掉的网卡设备。 - 另外我们重新配置了
eureka.instance.instance-id
。这个问题比较特殊,spring cloud
在组成instance-id
规则的时候,并没有遵守我们的preferredNetworks和ignoredInterfaces
约定(有可能是版本问题,没准下一个版本就好了)。所以我们不要在instance-id
使用ip
(因为enp0s3
虚拟机桥接网卡的ip
在所有的虚拟机上都是10.0.2.15
),这导致所有eureka server的instance-id
全一样,所以只能注册成功其中一个。
server:
port: 8761
servlet:
context-path: /eureka
spring:
application:
name: eureka-server
cloud:
inetutils:
preferredNetworks:
- 192.168
ignoredInterfaces:
- enp0s3
- docker0
eureka:
instance:
hostname: peer1
instance-id: $spring.application.name-$eureka.instance.hostname:$server.port
health-check-url: http://$eureka.instance.hostname:$server.port/$server.servlet.context-path/actuator/health
client:
#从其他两个实例同步服务注册信息
fetch-registry: true
#向其他的两个eureka注册当前eureka实例
register-with-eureka: true
service-url:
defaultZone: http://peer2:8761/eureka/eureka/,http://peer3:8761/eureka/eureka/
多网卡ip选择配置方法总结归纳
除去上面的配置方法,还有其他能实现多网卡ip
选择的方式,可以根据自己的网络环境情况选择使用。归纳如下:
方法一:直接配置eureka.instance.ip-address
eureka.instance.ip-address=192.168.1.7
直接配置一个完整的ip,一般适用于环境单一场景,对于复杂场景缺少有利支持。比如:你的eureka环境是结合docker容器部署的,就会有问题。因为docker容器的ip是动态的不固定的,所以你很难为docker容器中的服务指定ip。所以这种方式通常不建议使用。
方法二:增加inetutils相关配置
配置对应org.springframework.cloud.commons.util.InetUtilsProperties
,其中包含:
配置 | 说明 |
---|---|
spring.cloud.inetutils.default-hostname | 默认主机名,只有解析出错才会用到 |
spring.cloud.inetutils.default-ip-address | 默认ip地址,只有解析出错才会用到 |
spring.cloud.inetutils.ignored-interfaces | 配置忽略的网卡地址 |
spring.cloud.inetutils.preferred-networks | 期望优先匹配的网卡,正则匹配的ip地址或者ip前缀 |
spring.cloud.inetutils.timeout-seconds | 计算主机ip信息的超时时间,默认1秒钟 |
spring.cloud.inetutils.use-only-site-local-interfaces | 只使用内网ip |
上面已经为大家介绍了ignored-interfaces和preferred-networks用法,其他的配置举例说明如下:
使用/etc/hosts中主机名称映射的ip,这一种在docker swarm环境中比较好用。
# 随便配置一个不可能存在的ip,会走到InetAddress.getLocalHost()逻辑。
spring.cloud.inetutils.preferred-networks=none
当所有的网卡遍历逻辑都没有找到合适的网卡ip,会走JDK的InetAddress.getLocalHost()。该方法会返回当前主机的hostname, 然后会根据hostname解析出对应的ip。
# 只使用内网地址,遵循 RFC 1918
# 10/8 前缀
# 172.16/12 前缀
# 192.168/16 前缀
spring.cloud.inetutils.use-only-site-local-interfaces=true
通过启动命令行传递配置
java -jar xxx.jar --spring.cloud.inetutils.preferred-networks= #需要设置的IP地址
或者
java -jar xxx.jar --spring.cloud.inetutils.ignored-interfaces= #需要过滤掉的网卡
源码解析
为了说明这个问题的解决方案,我们需要翻看一下Eureka Client
的源码。com.netflix.appinfo
包下的InstanceInfo
类封装了本机信息,其中就包括了IP
地址。在 Spring Cloud
环境下,Eureka Client
并没有自己实现探测本机IP
的逻辑,而是交给Spring
的InetUtils
工具类的findFirstNonLoopbackAddress()
方法完成的:
public InetAddress findFirstNonLoopbackAddress()
InetAddress result = null;
try
// 记录网卡最小索引
int lowest = Integer.MAX_VALUE;
// 获取主机上的所有网卡
for (Enumeration<NetworkInterface> nics = NetworkInterface
.getNetworkInterfaces(); nics.hasMoreElements();)
NetworkInterface ifc = nics.nextElement();
if (ifc.isUp())
log.trace("Testing interface: " + ifc.getDisplayName());
if (ifc.getIndex() < lowest || result == null)
lowest = ifc.getIndex(); // 记录索引
else if (result != null)
continueSpringCloud系列之自定义GatewayFilterFactory