SpringCloud – 微服务基础
简介
单体架构
在以前的网络环境中,我们使用单体架构去开发一个后端系统,把各个模块都集成在一个项目中,并且服务与服务之间存在互相调用的情况,这种项目我们就叫单体架构。
单体架构的优点:
- 架构简单
- 部署成本低
缺点:
- 耦合度高(维护困难、升级困难),因为服务之间互相调用,当项目慢慢变大后,使得代码错综复杂,维护困难,也不敢随便修改原来的代码。
分布式架构
根据业务功能对系统做拆分,每个业务功能模块作为独立项目开发,称为一个服务。
分布式架构优点:
- 降低服务耦合
- 有利于服务升级和拓展
缺点:
- 服务调用关系错综复杂
分布式架构虽然降低了服务耦合,但是服务拆分时也有很多问题需要思考:
- 服务拆分的粒度如何界定?
- 服务之间如何调用?
- 服务的调用关系如何管理?
分布式架构存在一个管理上的问题,即拆分多个模块后,如何管理这些模块,如何知道这些模块的运行状态,我们需要有一个工具去管理这些模块的问题。
在早期,阿里巴巴也有开发过自己的分布式架构Dubbo,但是因为技术支持的原因,实际上比较原始,使用了Dubbo独有的服务通讯协议增加学习成本,也缺少很多分布式架构所需要的管理软件。
随着SpringCloud的发布,对各种管理软件的高度集成后改变了这一现状,阿里随后也基于SpringCloud开发了支持Dubbo的微服务版本 SpringCloud Alibaba.目前是国内使用比较广泛的微服务架构。
微服务
- 单一职责:微服务是分面式架构的再一次升级,它不单局限于一个项目中的服务拆分,而是把服务中的每一个主要功能进行拆分,而这样做使得粒度更小,每一个服务都对应唯一的业务能力,做到单一职责
- 自治:团队独立、技术独立、数据库独立,独立部署和交付
- 面向服务:服务提供统一标准的接口,与语言和技术无关
- 隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题
微服务的上述特性其实是在给分布式架构制定一个标准,进一步降低服务之间的耦合度,提供服务的独立性和灵活性。做到高内聚,低耦合。
因此,可以认为微服务是一种经过良好架构设计的分布式架构方案 。
但方案该怎么落地?选用什么样的技术栈?全球的互联网公司都在积极尝试自己的微服务落地方案。
其中在Java领域最引人注目的就是SpringCloud提供的方案了。
SpringCloud 底层是依赖SpringBoot的,所以SpringCloud的版本要与SpringBoot的版本严格匹配,其版本配对如下:
Cloud版本 | 对应Boot版本 |
---|---|
2022.0.x aka Kilburn | 3.0.x, 3.1.x (Starting with 2022.0.3) |
2021.0.x aka Jubilee | 2.6.x, 2.7.x (Starting with 2021.0.3) |
2020.0.x aka Ilford | 2.4.x, 2.5.x (Starting with 2020.0.3) |
Hoxton | 2.2.x, 2.3.x (Starting with SR5) |
Greenwich | 2.1.x |
Finchley | 2.0.x |
Edgware | 1.5.x |
Dalston | 1.5.x |
消费者与提供者
微服务中是把一个项目的多个模块抽离并形成一个个不同且独立的项目,这些项目我们也叫服务。
那么不可避免的,每个服务都有可能需要使用其它服务的数据,而这些数据是通过服务暴露接口提供的
1.对于调用其它服务的项目,我们称它为消费者服务。
2.对于被其它服务调用的服务项目,我们称它为提供者服务。
3.每一个服务它都可能既是提供者,又是消费者,如A服务调用B服务的数据,B调用C的数据。那么对于B服务而言,它就是既是提供者服务,也是消费者服务,这取决于站在哪个角度去看。
SpringBoot 中使用http请求
微服务中,我们的各个服务都以独立的方式运行,而若消费者服务需要调用提供者服务中的数据时,是通过使用http请求提供者服务获取的,在这里 SpringBoot 提供了一个用于http请求的对象 RestTemplate.
@Configuration
public class SpringConfig {
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
注意:RestTemplate 需要引入 spring-boot-web 依赖
然后就可以使用 RestTemplate 来请求数据了:
@RestController
@RequestMapping("/")
public class OrderController {
@Resource
private RestTemplate restTemplate;
@GetMapping
public String rest(String userId){
// 请求其它服务的接口,并把获取到的数据封装成 User 对象
String url = "http://127.0.0.1:8081/user/"+userId;
User user = restTemplate.getForObject(url, User.class);
return null;
}
}
Eureka 注册中心
Eureka 是一个用于集成多个微服务的中间管理程序,因为我们的微服务程序是互相独立的,服务与服务之间必然会存在互相调用对方的数据的情况,但是存在几种前置问题:
1.如果提供者服务使用分布式,该如何挑选合适的?
2.消费者服务在获取到的提供者服务的地址后,如何知道这个地址是可用的?
Eureka 就是提供这些服务的登记中心,所有服务都向Eureka注册在案(包括消费者服务),当消费者服务需要调用某个服务的接口时,会向Eureka询问提供者服务地址,获取并调用。
搭建 Eureka 注册中心
Eureka 本身也依赖于一个服务,因此我们需要创建一个SpringBoot项目,并引入 Eureka 依赖,在单个服务项目中启动。
1.引入 Eureka 依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>2.2.9.RELEASE</version>
</dependency>
2.在启动类中,开启 Eureka服务器
@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class,args);
}
}
3.application.yaml 文件中配置 Eureka 服务,并把当前的项目服务注册到Eureka中(以后Eureka可能会集群使用)
spring:
application:
name: EurekaServer # 定义服务名称
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka #注册本服务到Eureka服务器中
server:
port: 10086
4.启动服务即可
5.其它服务,如果需要注册到Eureka服务中时,只需要引入Eureka的客户端依赖,和注册服务地址:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>2.2.9.RELEASE</version>
</dependency>
6.对服务进行注册
spring:
application:
name: userservice # 服务的名称要标记好
server:
port: 8081
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka # 注册到Eureka服务器地址
Eureka 服务发现
上章我们已经通过搭建 Eureka 服务器,并把服务注册到Eureka中了,那我们的消费者服务如何通过在Eureka中获取提供者服务呢?
1.我们只需要在 RestTemplate 的Bean 声明中加入注册 @LoadBalanced
@Configuration
public class SpringConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
2.然后我们在请求服务接口时,就不再需要硬写死请求的地址了,而是写成服务的 application.name
spring:
application:
name: userservice
@GetMapping
public String rest(String userId){
String url = "http://userservice/user/"+userId;
User user = restTemplate.getForObject(url, User.class);
return null;
}
系统会通过查找 Eureka 服务器中有效的提供者服务并自动使用。
Ribbon 负载均衡
Ribbon 是一个配合Eureka做提供者服务器选择的负载均衡器
我们从上一章可以看到,请求路径中,不再需要写死请求ip地址,而是把请求的服务名称写上就可以了。
@GetMapping
public String rest(String userId){
String url = "http://userservice/user/"+userId;
User user = restTemplate.getForObject(url, User.class);
return null;
}
那么,Eureka 是如何通过这个请求url来获取到对应的提供者服务呢?我们可以看下图:
说明:
1.消费者服务发出请求时,会被loadBalanceInterceptor负载均衡拦截器拦截http请求(加了@LoadBalanced 注解的bean发出的请求会被拦截)
2.由 RibbonLoadBanlancerClient 取出 请求 url 中的 host 地址,并让 DynameicServierListLoadBalancer 处理提供者服务的选择问题.
3.提供者服务的选择问题由接口 IRule 的负责策略,它的实现类进行实现服务选择(有随机分配,也有轮询分配的实现方法)
4.把选择到的提供者服务地址,替换成原来的url的 host 名称,并请求。实现动态负载。
负载均衡策略
内置负载均衡规则类 | 规则描述 |
RoundRobinRule | 简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则。 |
AvailabilityFilteringRule | 对以下两种服务器进行忽略:
(1)在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加。 (2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AvailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上限,可以由客户端的<clientName>.<clientConfigNameSpace>.ActiveConnectionsLimit属性进行配置。 |
WeightedResponseTimeRule | 为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。 |
ZoneAvoidanceRule(默认) | 以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。 |
BestAvailableRule | 忽略那些短路的服务器,并选择并发数较低的服务器。 |
RandomRule | 随机选择一个可用的服务器。 |
RetryRule | 重试机制的选择逻辑 |
如果希望使用特定的负载机制,可以手动创建对应的策略实现类:
1.使用全局Bean方式配置,会对消费所有服务都使用同样的策略
@Bean
public IRule randomRule(){
return new RandomRule();
}
2.使用配置文件方式:可以对特定的提供者使用独特的策略
在配置文件中配置对应的提供者服务的访问策略
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则
这样只会对请求 userservice 服务时才会使用 RandomRule 负载策略
饥饿加载
Ribbon 默认采用懒加载方式,在启动后,不会马上创建对应的负载均衡对象,而是在首次发生请求时,才会创建负载均衡对象并拉取服务列表,这使得首次请求时的请求时间大大增加。
饥饿加载是指在启动完毕后,马上创建负载均衡对象。
在配置项中配置以下即可开启饥饿加载:
ribbon:
eager-load:
enabled: true # 开启饥饿加载
clients: # 指定对单个或多个服务做饥饿加载
- userservice
- xxxservice
Nacos 注册中心
Nacos是阿里巴巴的产品,现在是SpringCloud中的一个组件。相比Eureka功能更加丰富,在国内受欢迎程度较高。
Windows 安装
Nacos 是由Spring开发的程序,下载地址:
https://github.com/alibaba/nacos
下载后解压,并使用命令运行:
startup.cmd -m standalone // 使用单一服务形式启动
startup.cmd -p embedded // 使用内置数据源启动
startup.cmd // 使用集群并使用外置数据源启动
-m standalone 是指使用单机运行模式。Nacos 支持集群运行模式。
注册服务
1.引入阿里巴巴的SpringCloud依赖项
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba</artifactId>
<version>2021.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
2.引入Nacos依赖项
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2021.1</version>
</dependency>
3.在配置文件中加入nacos的配置项
spring:
application:
name: userservice
cloud:
nacos:
server-addr: localhost:8848
发现服务
按 Eureka的服务发现就可以了。
多级集群模型
通常情况下,我们不会把所有服务存放到一个机房中,而是在各个地域都设有相同的集群服务。
但是我们当然希望服务在访问期间使用同一个机房的其它服务,而不是跳到其它地域机房的服务。
这时我们可以设置二级集群,把同机房的特定服务设置为同一个集群,这样这些服务会优先访问同机房中的服务。
spring:
application:
name: userservice
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: GD # 设置为广东集群,同一个集群的服务会优先访问
负载均衡
与 Eureka 的负载均衡一样,依然是使用 Ribbo 进行调配分发到策略的,但是我们要使用 nacos 的策略:
spring:
application:
name: orderservice
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: GD # 设置为广东集群,同一个集群的服务会优先访问
userservice: # 消费者名称
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # nacos负载均衡规则
1.当 order 和 user 服务都设置集群为 GD 时,再使用 ribbon 设置 Nacos 策略后,就会由Nacos 分配处理本地机房的服务
2.当本地机房的服务都宕机后,才会使用异地机房的集群,并报出使用异地集群警告。
3.Nacos 在基于本地机房方式后,使用的是随机策略。
根据权重负载均衡
若在同一个机房中的服务器的性能可能出现不同的情况,对于一些性能比较差的服务器,总不能和性能好的服务器做相同的数据量吧,所以我们可以通过设置权重,来使得不同的服务器被受到的数据处理量不一样。
注意:
1.权重值可以设置范围 0~1 之间
2.同集群水的多个实例,权重越高被访问到的频率越高。
3.权重设置为0则完全不会被访问。
- 权重设置为0,可以实现任务处理的平滑过渡,在完全不停机下让部分机器进行升级操作,整个升级过程用户端无感知。
环境隔离 namespaced
在一个项目中,我们可能会有多个服务,而服务与服务之间可能会互相的调用,但并不是所有的服务都会互相调用,一些业务不相关的服务,它们就不会有调用的可能,这种情况下,可以做一个环境隔离。
就好比同一个学校里的两个学生,虽然在同一个学校,但互相之间没有任何交集,那么他们俩可以分配到两个班级,互不能访问。是提高服务之间滥用调用的措施。
环境隔离不是必须的配置的,如果一个项目中的多个服务都需要互相调用的话,那么就不需要设置环境隔离。
设置环境隔离方法如下:
1.在Nacos控制台可以创建namespace,用来隔离不同环境
2.创建一个新的命名空间,ID可以不用填,则使用 UUID自动生成
3.创建完命名空间之后,会产生一个ID
4.在配置项中,设置该服务的所属命名空间:
spring:
application:
name: orderservice
cloud:
nacos:
server-addr: localhost:8848
discovery:
namespace: 492a7d5d-237b-46a1-a99a-fa8e98e4b0f9 # 设置命名空间的ID
5.这样设置后,该服务只能访问同命名空间下的其它服务,不在其命名空间下的服务是不可见的。
设置非临时实例
Nacos 对加入的服务默认为临时服务,临时服务会采用心跳检测方式,服务每30秒向Nacos发送心跳检测,若临时服务宕机后,Nacos会直接剔除掉。
而非临时服务会采用主动询问方式,Nacos会每30秒向服务发送询问是否正常的请求,若服务宕机后,Nacos不会剔除非临时服务。
设置服务为非临时服务:
spring:
application:
name: userservice
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: GD
ephemeral: false # 是否设置为临时实例
Nacos 配置管理
Nacos 中除了可以给服务进行注册外,还提供了配置统一化管理,假如我们的单个服务存在多个实例,若我们需要对这个服务进行增加或修改配置时,那么我们是不是需要对这些实例都要一个个的进行修改配置呢?
答案是不需要的,Nacos 提供配置统一管理,我们可以在Nacos 中设置需要统一修改的配置项进行发布,这样规定的所有实例中的配置都会随之而更改。
实现原理:
1.正常的SpringBoot项目启动过程如下图,启动后会读取application.yaml配置文件,再进行IoC容器创建,并加载Bean
2.我们可以在SpringBoot项目在读取application.yaml之前,我们可以读取Nacos的配置,再进行application.yaml配置文件读取。
3.然而问题来了,如果要在项目读取application.yaml文件之前读取nacos的配置,但是我们的nacos服务器配置是放在application.yaml文件中的,我们要如何让项目在读取application.yaml配置之前读取nocas配置呢?
答案是:bootstrap.yaml 配置文件!
需要引入依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
4.SpringBoot 在启动初期,不是先读取 application.yaml 配置文件,application.yaml是SpringBoot的核心配置文件,是在启动开始后对组件的配置。
而SpringBoot在启动之前,还有一个配置文件,即就是 bootstrap.yaml 文件。我们可以把Nacos服务器配置设置在bootstrap.yaml文件中,让它加载application.yaml之前就加载Nacos了。
实现步骤:
1.在Nacos后台创建一个配置
2.添加配置项
注意:配置项中的 Data ID 为这个配置文件的文件名,Boot项目会对这个文件进行匹配以便是否加载,配置内容和yaml配置文件内容格式一致即可。
3.在项目的bootstrap.yaml 文件中做如下配置:
spring:
cloud:
nacos:
server-addr: localhost:8808 # 设置 Nacos 服务器
config:
file-extension: yaml # 设置Nacos配置服务器中的配置文件后缀
application:
name: userservice # 设置本项目的名称(也影响到Nacos配置文件名匹配)
profiles:
active: dev # 应用子级配置文件 如 application-dev.yaml
1.Nacos 会对项目中的 application.name 中的名字、profiles.active、config.file-extension中的值,来拼凑成一个配置文件名,如上面的配置值,会得出【userservice-dev.yaml】的配置文件
如果 Nacos 配置服务器上有Data ID 为【userservice-dev.yaml】的配置,将会匹配并下发其配置并加载。
配置自动加载
因为Nacos 的配置是在加载 application.yaml 之前加载的,加载后启动的项目,不会再对配置进行监听,即使Nacos服务器中的配置发生改变了,SpringBoot也不会实时刷新。
要让SpringBoot实时刷新Nacos平台上的配置,有以下两种方法:
1.如果使用了 @Value 注解对成员变量进行注入数据的,可以在其类上加上 @RefreshScope 注解即可:
@Component
@RefreshScope
public class User {
@Value("{yaml.prop}")
private String data;
}
2.如果使用的是自动匹配配置文件的@ConfigurationProperties注解,那么可以无需做任何操作,对于Nacos的配置变更,@ConfigurationProperties 都会自动刷新,非常贴心。
@Component
@Data
@ConfigurationProperties("yaml")
public class User {
private String data;
}
多环境配置共享
当我们在Nacos中配置了多个环境配置时,如【dev,prod,test】等环境中,假如都有一个共同的配置,万一这个配置需要修改时,那我们既不是要把【dev,prod,test】这些环境配置都得改一遍?其实不需要!
Nacos 会根据我们的微服务的 Spring.name、profiles、file-extension 自动判断我们当前的运行环境,来自动加载对应的环境yaml配置
例如:
Spring.name:cart-service
profiles:local
file-extension:yaml
Nacos 会自动加载至少以下两个配置文件
cart-service.yaml
cart-service-local.yaml
在Nacos中,会自动对各自的微服务进行默认读取多个线上配置文件,如:
- [spring.application.name]-[spring.profiles.active].yaml,例如:userservice-dev.yaml -> 仅对于 dev 环境生效
- [spring.application.name].yaml,例如:userservice.yaml -> 对于任何环境都生效
对于 [spring.application.name].yaml 这种无环境标记的默认配置,无论在任何时候,都会进行尝试加载操作,因此我们可以利用无环境标记的默认配置来做公共的配置。
多环境下的配置优先级
如果在【无环境标记配置】、【有环境标记配置】、【本地配置】中都配置了一样的值时,它们的值优先级分别为:
【有环境标记配置】> 【无环境标记配置】> 【本地配置】
举例:
【有环境标记配置】如 userservice-dev.yaml
【无环境标记配置】如 userservice.yaml
【本地配置】如 bootstrap.yaml
那么它们的优先级为:userservice-dev.yaml 覆盖> userservice.yaml 覆盖> bootstrap.yaml
Nacos 集群搭建
Nacos 站在服务的负载均衡方面,在应用上必须是高可用的,如果只是单节点的Nacos那么其可用性就变得很低了,万一这个Nacos服务宕机了,那么整个平台都会宕机,所以在企业使用中,Nacos通常都以集群的型式进行的。
我们可以看一下Nacos集群的主要原理:
Nginx对多个Nacos集群进行负载均衡访问,而集群的Nacos需要数据库的支持,我们使用mysql为例:
1.导入Nacos数据库表:
数据库表下载:
https://www.tzming.com/wp-content/uploads/filedown/mysql-schema.sql
2.对Nacos配置集群:
在 conf/cluster.conf.example 文件改为 cluster.conf 文件后,修改里面的文件:
192.168.16.101:8848
192.168.16.102:8848
192.168.16.103:8848
有多少台服务器做集群,就加上多少台服务器的ip和端口,每一台服务器上的Nacos都应该配置相同的集群配置。
3.对Nacos配置服务器:
在 conf/application.properties 文件中修改以下地方:
# 服务器的端口,如果是多台服务器,它们的端口都可以为8848
server.port=8848
# 配置Nacos服务使用Mysql数据库做数据源
spring.datasource.platform=mysql
# 设置多少台Mysql数据库
db.num=1
# 设置第0台数据库服务器的数据库连接信息
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=root
注意:
- 1.Nacos 通常在企业中会使用Mysql主从数据库实现数据库高可用,因此可以在配置项中设置使用了多少台数据库
- 2.如果使用了多台数据库,那及就需要在数据库信息设置中设置多个数据库连接信息。
- db.url.0、db.url.1、db.url.2 这样填写
4.启动所有Nacos服务器,即可集群启动。访问任意一台服务器,都可以访问到集群Nacos后台。
5.使用Nginx做负载均衡:
upstream nacos-cluster {
server 192.168.16.101:8848;
server 192.168.16.102:8848;
server 192.168.16.103:8848;
}
server {
listen 80;
server_name localhost;
location /nacos {
proxy_pass http://nacos-cluster;
}
}
配置说明:
- 1.使用Nginx设置一个负载均衡反向代理,名为 nacos-cluster
- 2.Nginx开放访问端口80
- 3.当访问地址为 /nacos 时,反向代理到 nacos-cluster,实现这三台Nacos均衡访问。
6.因为使用了Nginx做了负载均衡,且端口使用了80,所以我们的微服务配置Nacos中要改为:
spring:
cloud:
nacos:
server-addr: localhost:80 # 设置 Nacos 服务器
config:
file-extension: yaml # 设置Nacos配置服务器中的配置文件后缀
application:
name: userservice # 设置本项目的名称(也影响到Nacos配置文件名匹配)
profiles:
active: dev # 应用子级配置文件 如 application-dev.yaml
HTTP客户端 Feign
Feign是一个声明式的http客户端,官方地址:https://github.com/OpenFeign/feign
其作用就是帮助我们优雅的实现http请求的发送,解决上面提到的问题。
如果使用以往的 RestTemplate 来进行服务请求的话,会使代码显得可读性非常差,但是使用 Feign 我们可以实现以声明式的方式进行配置HTTP请求。
使用步骤:
1.引入Feign 的SpringCloud依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>1.4.7.RELEASE</version>
</dependency>
注意:spring-cloud-starter-openfeign 为 SpringBoot 支持SpringMVC的@RequestMapping注解的实现,spring-cloud-starter-feign 为原始的 feign 组件,因此我们应该引入 openfeign
2.开启Feign的声明式注解支持
@SpringBootApplication
@EnableFeignClients // 开启feign的注解支持
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
3.创建一个接口,在接口中创建接口方法,以声明我们需要发送的请求的结构,并使用注解 @FeignClient 声明该接口的请求都会向那个微服务接口进行请求:
@FeignClient("userservice")
public interface UserClient {
@GetMapping("/user/{id}")
User getUserById(@PathVariable("id") Long id);
}
- 1.@FeignClient 指定Feign会使用哪个微服务进行请求数据
- 2.@GetMapping 指定 Feign 会使用何种请求方式请求微服务,并且请求的uri地址是什么
- 3.如果请求微服务中需要带有动态参数,可以使用 @PathVariable 来声明传入的形参为该动态参数。
- 4.上面的代码可以代表为 "GET http://userservice/user/id"
4.接下来只要在需要请求微服务方法时,就通过调用方法的方式即可:
@RestController
@RequestMapping("/order")
public class OrderController {
@Resource
private OrderMapper orderMapper;
@Resource
private UserClient userClient;
@GetMapping("/{id}")
public String getOrder(@PathVariable("id") Long orderId){
Order order = orderMapper.selectById(orderId);
User user = userClient.getUserById(order.getUserId());
return null;
}
}
这就能做到请求一个http就像调方法一样方便。
Feign 自定义配置
Feign 在SpringBoot中帮我们做好了所有配置,但是我们可以通过自定义配置覆盖原来的配置。
feign.Logger.Level 日志级别
- none : 默认值,不输出任何日志信息
- BASIC : 输出请求基本信息
- HEADERS : 除了输出请求基本信息,还会把请求头信息也会输出
- FULL : 把请求、头信息和响应信息全部都会输出
feign.codec.Decoder 响应结果解析器
http远程调用的结果做解析,例如解析json字符串为java对象
feign.codec.Encoder 请求参数编码
将请求参数编码,把Java对象转化为json给request通过http请求发送出去
feign.Contract 注解格式
默认使用了 SpringMVC提供的请求注解作为feign的请求标识
feign.Retryer 重试机制
请求失败的重试机制,默认是没有的,不过会使用Ribbon的重试
基于配置文件的方式修改:
修改日志级别
feign:
client:
config:
default: # 使用 default 则表示全局feign请求都使用该级别
logger-level: full
userservice: # 使用微服务名称指定当请求某些微服务时才会使用该级别
logger-level: none
基于Java代码方式配置:
1.创建一个Feign配置文件,新增以下Bean
public class FeignConfig {
@Bean
public Logger.Level FeignLoggerLevel(){
return Logger.Level.FULL;
}
}
2.如果使用的是全局配置的话,则把这个配置在 @EnableFeignClients 注解中
@EnableFeignClients(defaultConfiguration = FeignConfig.class)
public class OrderApplication { }
3.如果使用的是局部配置的话,则把这个配置在 @FeignClient 上:
@FeignClient(value = "userservice",configuration = FeignConfig.class)
public interface UserClient { }
Feign 性能优化
Feign 虽然是一个Http客户端,但实际上它依然需要依赖Http请求支持,Feign 的Http请求支持有三种:
- URLConnection : JDK 提供的HTTP请求方法,Feign 默认使用,但性能较低。
- Apache HttpClient : Apache 提供的第三方HTTP请求方法,Feign 默认开启,但如果不引入依赖Feign不会启用。
- OKHttp : 轻量级HTTP请求方法
使用 Apache HttpClient 替代 默认的 URLConnection HTTP请求客户端:
1.引入 Apache HttpClient 依赖
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
httpclient 已被 SpringCloud 收纳管理,所以不需要维护版本。
2.配置开启httpclient ,其实Feign 默认已经开启了但依然可以手动声明开启:
feign:
client:
config:
default:
logger-level: full
userservice:
logger-level: none
httpclient:
enabled: true # 开启 HttpClient
max-connections: 200 # 设置最大请求连接数
max-connections-per-route: 50 # 设置单个服务最大连接数
1.引入 okhttp 依赖
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
2.配置项中开启 okhttp
feign:
okhttp:
enabled: true # 开启 okhttp 作为 feign 的请求依赖
Feign 配置请求参数
在某些情况下,我们的微服务使用Feign请求其它微服务获得信息时,可能需要提供(或转发)必要的认证信息(如token).
举例子,支付模块服务需要使用Feign请求购物车服务获得当前用户在购物车中的商品信息,但购物车服务中需要获得当前请求的用户信息,如果支付模块直接向购物车模块发送请求,购物车模块是无法通过token获得是哪个用户的购物车信息(如果购物车需要token而支付模块请求时并没有提供token情况下),这时购物车就无法获得相应信息。
这时我们就需要在支付模块的Fiegn中增加必要的认证信息再请求购物车模块。
我们可以实现一个拦截器接口方法,使Feign在请求中加入必要的请求参数,还可以修改Feign请求时的其它参数:
/**
* 针对Feign发送请求时对Feign请求参数进行处理
*/
@Component
public class OrderServiceInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
requestTemplate.header("token","xxxx");
}
}
Feign 最佳实践
在业务开发过程中,我们可能会开发多个微服务,而这些微服务又会对应调用其它多个微服务,这样我们在开发微服务过种中,都要重新配置一次Feign,重新写与Feign有关的类,使得每个微服务之间的代码变得亢余.
最佳的方式是可以把与Feign请求有关的类、pojo等都抽取到一个微服务中,也就是说这个微服务只作为编写Feign相关的业务,其它需要使用Feign来调用请求的微服务,可以通过引入依赖实现。
实现步骤:
1.创建一个模块,然后这个模块只写与Feign有关的业务和接口等:
2.在消费者微服务中引入依赖:
<dependency>
<groupId>cn.unsoft</groupId>
<artifactId>feignservice</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
3.把原先迁出到Feign微服务的相关对象引用改回依赖Feignservice中的引用
4.当把一些对象迁出到单独的微服务后,原先的消费者微服务有可能会因为扫描不到包而引起的IoC加载错误,这是因为在消费者微服务处启动时没有找到处于feign微服务中的对象,从而无法把对象创建并存入IoC容器中,当需要使用自动装配的地方时,就会报出无法做自动装配的错误。
如果多个微服务的包结构一致的情况下,有可能不会出现此类报错,只有在包结构不一致的情况下,消费者服务扫描包时没有指定扫描到Feign服务的包时才会报此类错误。
方法一:我们可以设置Feign的具体client和具体配置项的包地址:
@EnableFeignClients(clients = {UserClient.class},defaultConfiguration = FeignConfig.class)
在启动Feign注解处声明客户端位置的类
方法二:使用包扫描方式,直接使包中所有的类都引入IoC:
@EnableFeignClients(basePackages = {"cn.unsoft.clients"},defaultConfiguration = FeignConfig.class)
统一网关 Gateway
我们在微服务架构中,存在多个不同模块的服务,当用户访问时,我们需要有一个“门卫”挡在这些服务之前,拦截用户的访问行为,对用户进行身份认证,向用户访问进行路由指向,和防止服务外露被人滥用。网关就是充当这样一个角色。
网关技术可分为以下两种:
Spring Cloud Gateway : 一种比较新的网关技术,是Spring在最近版本推出的声明式网关,性能较好
zuul : 一种基于Servlet的阻塞式编程网关,性能比较差。
Spring Cloud Gateway 基本搭建
配置一个网关,并使用这个网关进行访问,通过访问不同的地址,网关负责帮我们跳转到特定的微服务。
1.网关也是一个微服务,它应该也被注册到nacos中,因此我们需要创建一个新的服务用于承载网关系统:
引入网关所需依赖
<!-- nacos 用于把网关注册到网络发现中 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- nacos 配置中心 如果网关没有统一配置需求可以不引入 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- gateway 网关系统依赖,必须引入 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
2.在配置项中配置网关服务名及微服务的路由指向地址:
server:
port: 10010
spring:
application:
name: gateway
cloud:
nacos:
server-addr: localhost:8848
gateway:
routes:
- id: userservice # 定义该路由的id,名字随意,但必须唯一
uri: # 定义该路由应该指向那些服务,可以是微服务,也可以是固定ip
- lb://userservice # 这里使用了微服务,lb是 loadBalanced,负载均衡
- http://127.0.0.1:8082 # 这里使用了固定ip
predicates: # 路由断言,即当访问到什么路径时,会指向这个路由
- Path=/user/** # 这里定义了其中一种规则Path,当访问到的是 /user/xx 时
- id: orderservice # 定义第二个路由id
uri:
- lb://orderservice
predicates:
- Path=/order/**
说明:
1.gateway 是安插在一个springboot项目中的,所以它也要被注册到nacos,因此它也要设置唯一的服务名称。
2.gateway.id 是作为这个网关路由的一个唯一性id,可以随便命名,但名称需要唯一
3.uri 支持负载均衡写法和固定ip写法,负载均衡写法使用 lb:// 开头,即loadBalanced 的缩写,它会把请求向nacos中查找服务并把请求转发到该服务中。而如果使用固定ip,则只能做单个服务处理请求。
4.predicates 断言是指一系列匹配规则,当这个请求符合某个规则要求时,网关就会知道这个请求应该转发到那个服务中处理。其中Path是请求路径规则。
5.概念图如下:
路由断言工厂 Route Predicates Factory
上面章节我们知道,网关的路转是根据路由规则匹配进行的,这一章节我们来说说,都有哪些路由规则。
After
- 说明:指需要在某个时间点后请求才会匹配
- 示例:- After=2037-01-20T17:42:47.789-07:00[America/Denver]
Before
- 说明:指需要在某个时间点之前请求才会匹配
- 示例:- Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai]
Between
- 说明:指需要在某两个时间点之间请求才会匹配
- 示例:- Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver]
Cookie
- 说明:请求中必须包含某些cookie才会匹配
- 示例:- Cookie=chocolate, ch.p
Header
- 说明:请求中必须包含某些header
- 示例:- Header=X-Request-Id, \d+
Host
- 说明:请求中必须是访问某个host域名
- 示例:- Host=**.somehost.org,**.anotherhost.org
Method
- 说明:请求中请求方式必须是指定的方式
- 示例:- Method=GET,POST
Path
- 说明:请求路径必须符合指定规则
- 示例:- Path=/red/{segment},/blue/**
Query
- 说明:请求参数中必须包含指定参数
- 示例:- Query=name, Jack或者- Query=name
RemoteAddr
- 说明:请求者的ip必须是指定ip范围
- 示例:- RemoteAddr=192.168.1.1/24
Weight
- 说明:权重处理
网关过滤器 GatewayFilter
GatewayFilter 是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理:
spring:
application:
name: gateway
cloud:
nacos:
server-addr: localhost:8848
gateway:
routes:
- id: userservice # 定义该路由的id,名字随意,但必须唯一
uri: # 定义该路由应该指向那些服务,可以是微服务,也可以是固定ip
- lb://userservice # 这里使用了微服务,lb是 loadBalanced,负载均衡
- http://127.0.0.1:8082 # 这里使用了固定ip
filters: # 为路由添加过滤器规则
- AddRequestHeader=X-Request-red, blue
predicates: # 路由断言,即当访问到什么路径时,会指向这个路由
- Path=/user/** # 这里定义了其中一种规则Path,当访问到的是 /user/xx 时
- id: orderservice # 定义第二个路由id
uri:
- lb://orderservice
predicates:
- Path=/order/**
Spring Cloud Gateway 提供了34种过滤器
主要过滤器说明如下:
AddRequestHeader
- 说明:在请求中添加请求头数据
- 示例:- AddRequestHeader=X-Request-red, blue
- 解析:请求会在通过过滤器后,请求头中会增加一个key为X-Request-red,值为blue的请求头信息
- 代码:
-
spring: gateway: routes: - id: userservice # 定义该路由的id,名字随意,但必须唯一 uri: # 定义该路由应该指向那些服务,可以是微服务,也可以是固定ip - lb://userservice # 这里使用了微服务,lb是 loadBalanced,负载均衡 - http://127.0.0.1:8082 # 这里使用了固定ip filters: # 为路由添加过滤器规则 - AddRequestHeader=X-Request-red, blue predicates: # 路由断言,即当访问到什么路径时,会指向这个路由 - Path=/user/** # 这里定义了其中一种规则Path,当访问到的是 /user/xx 时
AddRequestParameter
- 说明:在请求中添加请求参数
- 示例:- AddRequestParameter=red, blue
- 解析:请求会在通过过滤器后,请求中会增加一个key为red,值为blue的请求参数信息
- 代码:
-
filters: # 为路由添加过滤器规则 - AddRequestParameter=red, blue
其它更多过滤器包括:
- AddResponseHeader:增加响应头信息
- DedupeResponseHeader:删除重复响应头信息
- CircuitBreaker:断路器
- RemoveResponseHeader:删除响应头信息
- ...........
其它路由过滤器可参考官方文档:https://docs.spring.io/spring-cloud-gateway/docs/3.1.8/reference/html/#gatewayfilter-factories
全局过滤器:
针对希望某些过滤器是向全部微服务都生效的情况,SpringCloud提供了一个全局过滤器,它的过滤规则会使请求所有的微服务都会生效:
spring:
application:
name: gateway
cloud:
nacos:
server-addr: localhost:8848
gateway:
routes:
... 路由规则
default-filters: # 全局过滤器,与routes同级
- AddRequestParameter=red, blue
设置全局过滤器,则对所有 routes 中的路由指向都会生效!
自定义全局过滤器 GlobalFilter
上面章节中我们讲到了全局过滤器 default-filters,但是这个全局过滤器是SpringCloudGateway提供给我们的默认过滤器,它无法做到自定义过滤功能,所以SpringCloud提供了一个允许我们自定义过滤逻辑的过滤器接口GlobalFilter。
GlobalFilter 是一个接口,它只有一个方法Filter,当请求到达网关后,会对请求先通过过滤器处理,自定义判断是否通过过滤
@Component
@Order(999)
public class MyGlobalFilter implements GlobalFilter, Ordered {
// 创建一个用于匹配uri路径的对象
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
/**
* 自定义全局过滤器方法
* exchange 是网关传进来的路由对象,它可以获取请求的请求、响应、头等信息
* chain 是过滤器链,允许执行是否通过过滤器
* Mono<Void> 是一个过滤器业务返回值,返回它让网关处理通过与否
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取request对象
ServerHttpRequest request = exchange.getRequest();
// 获取response对象
ServerHttpResponse response = exchange.getResponse();
// 获取请求的URL路径
String path = request.getURI().getPath();
// 可以使用 AntPathMatcher 来判断URI路径是否满足规则
antPathMatcher.match("/api/**/auth/**", path); // 针对 /api/xx/xx/auth/xx/xx 之类的请求都会成功匹配
// 获取session对象
Mono<WebSession> session = exchange.getSession();
// 获取请求参数对象
MultiValueMap<String, String> queryParams = request.getQueryParams();
String auth = queryParams.getFirst("auth");
// 自定义过滤条件
if (auth.equals("admin")) {
// 若过滤条件通过,则使用返回 chain 链
return chain.filter(exchange);
}
// 若不通过过滤条件,则使用 exchange 中止路由下传
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
// 路由设置中止下传,并返回 Mono 对象
return exchange.getResponse().setComplete();
}
/**
* 设置Order
* @return
*/
@Override
public int getOrder() {
return 999;
}
}
说明:
1.过滤器有多个,但是如何让网关知道我们的过滤器应该在那一层执行,需要在类上面加上 @Order 定义Bean的执行优先级,值越小,优先级越高。
同时,也可以通过 实现 Ordered 接口的 getOrder() 方法来指定这个过滤器的优先级,与 @Order 二选一。
Nacos 中动态网关路由
借着Nacos配置中心的动态配置功能,我们可以在不重启微服务的情况下,对微服务中的非Spring官方配置进行热更新(指的是自定义的配置数据,使用@ConfigurationProprties获取)。
但如果我们考虑到,配置热更新,能否对网关中的路由规则也进行热更新?
网关作为Spring,它会在启动时,把yaml文件中的路由规则加载到内存中,之后就不会再读取yaml文件配置了,所以理论上讲,网关路由是不能自动进行热更新的。
- 但是网关Gateway提供了一个接口,允许我们实时地对路由规则进行增删的接口 。interface RouteDefinitionWriter
- 同时,Nacos 也为我们提供了一个监听器接口,用于实现当Nacos配置发生改变时,向我们的微服务进行提醒。 NacosConfigManager 类
使用以上两个方式,我们就可以手动为网关动态更新路由规则了。
说明:
1.NacosConfigManager 来自Nacos,在自动配置时,会向IoC容器中注入,所以我们可以直接自动注入NacosConfigManager
2.RouteDefinitionWriter 接口来自 Gateway,需要我们实现两个方法【save】和【delete】,分别用于新增路由规则和删除路由规则。
3.我们可以在 NacosConfigManager 中新增一个 Listener 监听器,当Nacos配置发生改变时,我们再通过实现 RouteDefinitionWriter 进行增删路由规则
实现代码如下:
@Component
public class RoutesConfigLoader {
/**
* 在IoC容器中获取 Nacos 的 NacosConfigManager
*/
@Resource
private NacosConfigManager nacosConfigManager;
/**
* 定义一个方法,这个函数要求在创建类完成后被调用一次
* 这个方法是用于为Nacos创建一个监听器,并且在监听器触发时调用Gateway更新路由
* NacosConfigManager 中可以获取当前配置,和增加监听器
* getConfig
* addListener
* 还有一个同时执行的复合方法 getConfigAndSignListener
* getConfigAndSignListener 是获取配置数据后,顺便添加个监听器
* String dataId:监听Nacos上的哪一个配置文件,只支持json格式
* String group:该配置文件的归属组
* long timeoutMs:监听超时时间
* Listener listener:该配置文件发生改变时,会触发的方法
*/
@PostConstruct
public void initRouteDefinition() throws NacosException {
/**
* 返回做 routesConfig 是直接读取当前的配置数据
*/
String routesConfig = nacosConfigManager
.getConfigService()
.getConfigAndSignListener(
"gateway-routes.json",
"DEFAULT_GROUP",
1000,
new Listener() {
/**
* 创建一个线程池去处理
* @return
*/
@Override
public Executor getExecutor() {
return Executors.newSingleThreadExecutor();
}
/**
* 当Nacos发生改变时,才会解发修改更新网关配置
* @param configInfo config info
*/
@Override
public void receiveConfigInfo(String configInfo) {
updateRouteConfigInfo(configInfo);
}
}
);
// 这里是指让Nacos上来就读当前的路由配置,并写入到Gateway中
updateRouteConfigInfo(routesConfig);
}
/**
* RouteDefinition 是用于存储 网关规则的对象类,其Json格式如下:
* [
* {
* "id":"" // 路由规则的id名称
* "predicates":[ // 路由规则
* "name":"Path" // 使用 Path 规则
* "args": { // 使用 Path 规则需要传入的参数
* "_genkey_0": "",
* "_genkey_1": "",
* ...
* }
* ],
* "filters":[] // 网关局部过滤器
* "uri": "lb://xxx" // 转发请求的微服务Nacos名称
* }
* ]
*
* @param configInfo
*/
// 来自Gateway提供的路由规则读写接口方法
@Resource
private RouteDefinitionWriter routeDefinitionWriter;
// 保存旧的路由信息,以方便后面清除重新增加
private Set<RouteDefinition> oldRoutes = new HashSet<>();
private void updateRouteConfigInfo(String configInfo) {
List<RouteDefinition> routeList = JSONUtil.toList(configInfo, RouteDefinition.class);
// 在写入网关之前,把旧的网关删除
for (RouteDefinition oldRoute : oldRoutes) {
routeDefinitionWriter.delete(Mono.just(oldRoute.getId())).subscribe();
}
oldRoutes.clear();
if (routeList.isEmpty()) {
return;
}
// 对路由规则进行遍历并写入网关中
for (RouteDefinition routeDefinition : routeList) {
routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();
oldRoutes.add(routeDefinition);
}
}
}
过滤器的执行顺序
通过上面的章节,我们知道网关具有三个过滤器,分别是【路由过滤器filters】、【默认全局过滤器default-filter】、【自定义全局过滤器GlobalFilter】,那么它们三个的执行顺序是如何进行的呢?
1.当 路由过滤器filters 有多个过滤器时,按照先声明先执行的规则进行
2.当 默认全局过滤器default-filter 有多个过滤器时,按照先声明先执行的规则进行
3.当 自定义全局过滤器GlobalFilter 有多个过滤器时,按照 GlobalFilter 中的 @Order 排序值先后执行
4.三者过滤器的总体执行顺序是 【默认全局过滤器default-filter】->【路由过滤器filters】->【自定义全局过滤器GlobalFilter】
网关中跨域问题处理
当我们把所有的请求都发向网关中时,就会出现一个问题,浏览器中依然会存在跨域访问的问题,但是Gateway 已经考虑到这个问题了,只需要在网关中设置CORS即可。
具体配置如何:
spring:
application:
name: gateway
cloud:
nacos:
server-addr: localhost:8848
gateway:
globalcors: # 配置全局CORS
cors-configurations: # CORS 配置规则
'[/**]': # 针对所有的请求的规则
allowed-origins: # 允许CORS的请求域名
- "http://localhost:8090"
- "http://www.unsoft.cn"
allowed-methods: # 允许CORS的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowed-headers: # 允许请求携带的 header 请求头
- "*"
allow-credentials: true # 允许请求携带 Cookie 信息
max-age: 360000 # 设置浏览器向服务器请求 CORS 的有效期
add-to-simple-url-handler-mapping: true # 是否允许浏览器向服务器询问CORS
1.max-age 是指,CORS 实际上是浏览器向服务器询问是否允许跨域访问,服务器得到允许后浏览器会对跨域请求放行,但每一次请求都要询问服务器,这是很耗性能的,所以设置 max-age 则是浏览器每一次询问后,允许多长时间内是否有效
2.add-to-simple-url-handler-mapping 是浏览器向服务器询问是否允许跨域,但浏览器询问时会使用 OPTIONS 请求方法,这种方法通常都会被浏览器所禁止,所以服务端这边需要允许浏览器使用 OPTIONS 请求方法向服务端询问CORS允许,如果不开启,有可能使浏览器询问跨域时就被跨域问题所禁止!
共有 0 条评论