目录
1.认识微服务
服务治理
分布式架构的要考虑的问题:
- 服务拆分粒度如何?
- 服务集群地址如何维护?
- 服务之间如何实现远程调用?
- 服务健康状态如何感知?
Springcloud
SpringCloud是目前国内使用最广泛的微服务框架。官网地址:Spring Cloudhttps://spring.io/projects/spring-cloud
SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验:
2.分布式服务架构案例
1. 服务拆分Demo
服务拆分(也叫项目拆分)注意事项
●不同的微服务,不要重复开发相同的业务
●要求微服务之间的数据独立,不要访问其它微服务的数据库
●微服务可以将自己的业务暴露为接口,供其它微服务调用
首先有一个已经写好的项目工程,为cloud-demo.zip,需要把这个压缩包解压并导入进idea,然后就是根据功能来拆分这个项目。这个项目的原有结构如下:
cloud-demo是父工程,里面有两个模块是order-service、user-service,这俩模块分别的作用是根据id查询订单、根据id查询用户。这俩模块就是所谓的微服务,分是订单服务和用户服务
除了把项目工程解压导入,还有两份sql文件(cloud-order.sql和cloud-user.sql)需要导入自己本地数据库(创建两个不同的database),作为数据层面的分离。订单服务只能查询cloud-order.sql表,用户服务只能查询cloud-user.sql表数据库的具体操作如下:
第一步: 在mysql创建两个数据库,为cloud_order、cloud_user# 创建两个数据库 create database if not exists cloud_order; create database if not exists cloud_user;
第二步: 分别在这两个数据库导入对应的sql文件
第三步: 下载cloud-demo.zip项目工程文件,解压到D盘的springcloud目录,并在idea打开cloud-demo项目工程
第四步: 启动cloud-demo项目工程
下面将在这个Demo进行练习,如何正确使用微服务项目
2. 服务远程调用
案例: 根据订单id查询订单功能
需求: 根据订单id查询订单的同时,把订单所属的用户信息一起返回
难点: 订单表、用户表在两个数据库,不是同一个数据库。订单业务、用户业务在两个项目,不是同一个项目
解决: 服务远程调用
目前这个Demo还没有实现服务远程调用,也就是在查订单表时,无法查到用户信息
分析:
用户项目对外暴露了一个Restful接口,如下
可以在订单项目使用Spring提供的RestTemplate工具,作用是发送http请求,也就是在订单项目向用户项目发送http请求,用户项目就会返回数据给订单项目
具体操作:
第一步: 在订单项目的OrderApplication引导类,添加如下/** * 创建RestTemplate并注入Spring容器 * @return */ //以后我们都要习惯使用上面这种注释,写法: /**+回车 @Bean public RestTemplate xxrestTemplate(){ return new RestTemplate(); }
第二步: 在订单项目的OrderService类,添加如下
@Autowired private RestTemplate kkrestTemplate; /** * 使用RestTemplate,向用户项目发起http请求,查询用户 */ String xxurl = "http://localhost:8081/user/"+order.getUserId(); User gguser = kkrestTemplate.getForObject(xxurl, User.class);//第一个参数是路径,第二个参数是你想要拿到什么类型的数据 order.setUser(gguser);//把向用户项目拿到的数据封装到这个订单项目
第三步: 测试。重新启动用户项目和订单项目,在浏览器输入如下,查看在订单项目是否能获取到用户项目的数据http://localhost:8080/order/101
总结: 跨服务的远程调用其实就是发送一次http请求,首先是在Spring容器里面注入RestTemplate对象,然后在你发送请求的类里面自动装配这个RestTemplate对象,并且在方法里面调用这个RestTemplate对象,第一个参数是路径,第二个参数是你想拿到什么类型的数据
3.eureka注册中心实用篇-Eureka注册中心
读音: yī yōu ruī kǎ
1. 提供者与消费者
服务提供者: 一次业务中,被其它微服务调用的服务。简单来说,服务提供者就是提供接口给其它微服务
服务消费者: 一次业务中,调用其它微服务的服务。简单来说,服务消费者就是调用其它微服务提供的接口
例如上面的案例中,order-service微服务是服务提供者,user-service微服务是服务消费者
思考: 如果服务A调用服务B,服务B调用服务C,那么服务B是什么角色 ?
答案: 一个服务既可以是提供者,也可以是消费者。所以服务B相对于服务A而言,服务B是提供者。服务B相对于服务C而言,服务B是消费者
2. eureka原理分析
还是以上面的案例为例,order-service微服务和user-service微服务之间,服务调用出现的问题,如下
●order-service去向user-service发送请求,使用的是硬编码,也就是 "http://localhost:8081/user/"+order.getUserId();
●硬编码每次修改需要重新打包
●如果user-service微服务(提供者)部署成了多实例,形成集群来应对并发,那么order-service微服务(消费者)硬编码到底是写哪个实例的地址
●即使你知道这些实例的地址,那么如何挑选其中一台实例来使用呢
解决: 下面要学的Eureka注册中心
Eureka的原理:
●Eureka有两个角色,其中一个是eureka-server,叫Eureka的注册中心。作用是记录和管理所有微服务
●Eureka的另一个角色就是我们的所有微服务,也就是消费者/提供者,叫Eureka的客户端EurekaClient。
●可以把注册中心理解为Key,我们的微服务项目理解为Value,Eureka理解为字典
●当我们的微服务项目(例如order-service)启动时,会主动把自己(user-service微服务)的信息注册给注册中心
●多个微服务的话,注册中心就会有多个Value,一个Value就是一个微服务的信息,这些Value会放到一个列表里面
●当其它微服务(例如user-service)要使用某个微服务(例如order-service)时,这个微服务(例如user-service就会向注册中心去拉取对应微服务(例如order-service)的信息
●注册到注册中心的微服务会每隔30秒,向注册中心发起心跳,证明自己还在健康运行
●当微服务没有正常向注册中心发起心跳,此时注册中心就会自动在列表把这个异常的微服务剔除掉
●当注册中心有多个提供者(微服务),那么消费者是通过负载均衡算法,在注册中心的服务列表中挑选一个
●负载均衡: 简单理解就是如果有很多个微服务,且这些服务都是一个相同的请求,看谁现在工作压力小就调用哪个谁
3. 搭建eureka服务
步骤有三步:
1、搭建注册中心。搭建EurekaServer注册中心,也就是创建一个项目(在cloud-demo项目内部创建eureka-server项目),把这个项目做成注册中心
2、服务注册。将user-service(前面导入的服务拆分Demo)、order-service(前面导入的服务拆分Demo)都注册到eureka
3、服务发现。在order-service中完成服务拉取,然后通过负载均衡挑选一个服务,实现远程调用
这里只学第一个步骤,也就是搭建注册中心
第一步: 创建一个新的项目,作为独立的微服务,用于搭建Eureka,也就是在cloud-demo工程里面新建eureka-server微服务项目第二步: 在eureka-server微服务的pom.xml添加如下
<!--添加注册中心的依赖坐标--> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> </dependencies>
第三步: 在eureka-server微服务的src/main/java目录新建cn.huanf.eureka.EurekaApplication类,写入如下
package cn.itcast.eureka; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; /** * @Author:豆浆 * @name :EurekeApplication * @Date:2024/3/28 11:32 */ @SpringBootApplication @EnableEurekaServer//注册中心的开关 public class EurekeApplication { public static void main(String[] args) { SpringApplication.run(EurekeApplication.class); } }
第四步: 在eureka-server微服务的src/main/resources目录新建File,名字为application.yml,写入如下
第五步: 启动eureka-server微服务。也就是运行EurekaApplication类,浏览器访问 http://localhost:8686
eureka-server微服务的Eureka在启动时,会把自己(eureka-server)也注册到Eureka。所以这就是为什么我们会在eureka-server微服务的application.yml里面写name: eurekaserver,这个其实就是自己的地址,把自己注册到Eureka,注册之后自己的名字就是EUREKASERVER
4. 服务注册
步骤有三步:
1、搭建注册中心。搭建EurekaServer注册中心
2、服务注册。将user-service(前面导入的服务拆分Demo)、order-service(前面导入的服务拆分Demo)都注册到eureka
3、服务发现。在order-service中完成服务拉取,然后通过负载均衡挑选一个服务,实现远程调用
这里只学第二个步骤,也就是服务注册。就是把user-server微服务注册到eureka-server微服务(这个微服务我们已经在上面 '3. 搭建eureka服务' ,做成了注册中心)里面。具体操作如下
第一步: 在user-service微服务的pom.xml添加如下<!--引入Eureka客户端依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
第二步: 在user-service微服务的application.yml添加如下
spring: # Eureka相关配置 application: # user的服务名称。也就是这个user-service注册到Eureka之后,这个user-service会叫什么名字 name: UserService eureka: client: service-url: # eureka的服务地址。如果有多个的话,逗号隔开。也就是把当前这个user-service微服务注册给哪个Eureka defaultZone: http://localhost:8686/eureka
第三步: 启动user-service。也就是运行UserApplication类,浏览器访问 http://localhost:8686,就可以看到user-service已经添加到Eureka里面
同理,给order-service微服务也注册到Eureka。具体操作跟上面一样
建议:
思考: 如何给user-service微服务启动多个实例呢,也就是启动多次,每次user-service微服务启动的端口都不相同,
如下
具体操作如下图-Dserver.port=8082
5. 服务发现
服务发现也叫服务拉取,我们需要在order-service完成服务拉取。服务拉取是基于服务名称获取服务列表,然后再对服务列表做负载均衡
服务发现是我们学习的重点,负载均衡不是,所以不详细介绍负载均衡,实现负载均衡只需要一个注解
首先回想一下,前面的远程调用,我们实现了在订单项目(也就是现在的order-service微服务)去查询用户项目(也就是现在的user-service微服务)的案例需求,
当时是在order-service里面使用url请求ip地址的方式,去请求user-service,从而获取user-service的用户信息。那么,学习了上面的Eureka之后,并且我们已经把order-service和user-service注册到注册中心(eureka-server)了,所以就可以在order-service里面,通过 '服务发现' 去获取user-service里面的用户信息。
具体操作也非常简单,也是使用url请求的方式,但请求的路径不是ip,而是服务名称,如下:
第一步: 在order-server微服务中的src/main/java/cn.itcast.order/service/OrderService类,修改访问的url路径,用服务名代替ip、端口。修改为如下String url = "http://UserService/user/"+order.getUserId();
第二步: 负载均衡。在order-server微服务中的OrderApplication引导类修改为如下
第三步: 重新启动在order-server微服务的OrderApplication引导类,浏览器输入http://localhost:8080/order/101,并向user-service微服务发送多次请求
4.Ribbon负载均衡原理1. 负载均衡原理
回想一下上面的 '服务发现',order-service微服务向user-service微服务发送请求,但是user-service有两个,也就是开启了两个user-service实例,且端口不同,一个是8081,另一个是8222(对应的是下图的8082),下面我们将详细学一下请求在过程中经历了什么,如下图
其中负载均衡的各种策略是在IRule接口里面,下面将会深入学习这个IRule接口
2. 负载均衡策略
Ribbon的负载均衡规则是一个叫做IRule的接口来定义的,每一个子接口都是一种规则,IRule有很多实现类,如下继承关系图每一个实现类都是一种规则,上图只是简单标注一下,下图是详细的
如何修改负载均衡策略。负载均衡策略默认是轮询,如何修改为随机呢。有两种方式如下
第一种: 代码方式(作用于全局)。在order-service中的OrderApplication类中,定义一个新的IRule。简单理解就是在项目的引导类创建一个类型为IRule的bean@Bean //bean的类型必须是IRule。bean的id是方法名,随便写 public IRule randomRule(){ //实现类不一定是RandomRule,还可以是其它,如上图那些都可。RandomRule表示把负载均衡策略修改为随机 return new RandomRule(); }
第二种: 配置文件方式(只作用于该服务名称)。在order-service的application.yml文件中,添加新的配置也可以修改规则
# UserService是你注册到Eureka时的服务名称。注意顶格写就行,不用写在spring:属性里面 UserService: ribbon: # 负载均衡策略。不一定是RandomRule,还可以是其它,如上图那些都可。RandomRule表示把负载均衡策略修改为随机 NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
如果同时使用上面这两种方式,那么配置文件的优先级比代码方式低,也就是代码方式的优先级高
注意在使用的时候,很简单,就字面意思,不需要改变你其它地方的任何代码,只需要添加如上两种方式提供的代码即可
3. 饥饿加载
Ribbon默认是采用懒加载,即第一次访问时才会去创建Ribbon的LoadBalanceClient客户端,请求时间会很长(第一次访问时间长)。而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置就可以开启饥饿加载。在order-service微服务的application.yml添加如下# 注意顶格写就行,不用写在spring:属性里面 ribbon: eager-load: # 开启饥饿加载 enabled: true # 指定对UserService这个服务开启饥饿加载。UserService是你注册到Eureka时的服务名称。如果有多个服务需要做饥饿加载,就-往下写 clients: - UserService - UserService2
5.nacos注册中心国内公司一般都推崇阿里巴巴的技术,比如注册中心,SpringCloudAlibaba也推出了一个名为Nacos的注册中心。
Nacos是SpringCloudAlibaba的组件,而SpringCloudAlibaba也遵循SpringCloud中定义的服务注册、服务发现规范。因此使用Nacos和使用Eureka对于微服务来说,并没有太大区别。
主要差异在于:
- - 依赖不同
- - 服务地址不同
## 5.1.认识和安装Nacos
[Nacos](https://nacos.io/)是阿里巴巴的产品,现在是[SpringCloud](https://spring.io/projects/spring-cloud)中的一个组件。相比[Eureka](https://github.com/Netflix/eureka)功能更加丰富,在国内受欢迎程度较高。
# Nacos安装指南
# 1.Windows安装
开发阶段采用单机安装即可。
## 1.1.下载安装包
在Nacos的GitHub页面,提供有下载链接,可以下载编译好的Nacos服务端或者源代码:
GitHub主页:https://github.com/alibaba/nacos
GitHub的Release下载页:https://github.com/alibaba/nacos/releases
如图:
本课程采用1.4.1.版本的Nacos,资料已经准备了安装包:
windows版本使用`nacos-server-1.4.1.zip`包即可。
## 1.2.解压
将这个包解压到任意非中文目录下,如图:
目录说明:
- bin:启动脚本
- conf:配置文件
## 1.3.端口配置
Nacos的默认端口是8848,如果你电脑上的其它进程占用了8848端口,请先尝试关闭该进程。
**如果无法关闭占用8848端口的进程**,也可以进入nacos的conf目录,修改配置文件中的端口:
修改其中的内容:
## 1.4.启动
启动非常简单,进入bin目录,结构如下:
cmd
然后执行命令即可:
- windows命令:
startup.cmd -m standalone
执行后的效果如图:
## 1.5.访问
在浏览器输入地址:http://127.0.0.1:8848/nacos即可:
默认的账号和密码都是nacos,进入后:
# 2.Linux安装
Linux或者Mac安装方式与Windows类似。
## 2.1.安装JDK
Nacos依赖于JDK运行,索引Linux上也需要安装JDK才行。
上传jdk安装包:
上传到某个目录,例如:`/usr/local/`
然后解压缩:
tar -xvf jdk-8u144-linux-x64.tar.gz
然后重命名为java
mv jdk-8u144-linux-x64 java
配置环境变量:
export JAVA_HOME=/usr/local/java export PATH=$PATH:$JAVA_HOME/bin
设置环境变量:
source /etc/profile
## 2.2.上传安装包
也可以直接使用课前资料中的tar.gz:
上传到Linux服务器的某个目录,例如`/usr/local/src`目录下:
## 2.3.解压
命令解压缩安装包:
tar -xvf nacos-server-1.4.1.tar.gz
然后删除安装包:
rm -rf nacos-server-1.4.1.tar.gz
## 2.4.端口配置
与windows中类似
## 2.5.启动
在nacos/bin目录中,输入命令启动Nacos:
sh startup.sh -m standalone
例如:# 3.Nacos的依赖
父工程:
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.2.5.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency>
客户端:
<!-- nacos客户端依赖包 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
spring: cloud: nacos: server-addr:localhost:8848
## 5.3.服务分级存储模型
一个**服务**可以有多个**实例**,例如我们的user-service,可以有:
- 127.0.0.1:8081
- 127.0.0.1:8082
- 127.0.0.1:8083
假如这些实例分布于全国各地的不同机房,例如:
- 127.0.0.1:8081,在上海机房
- 127.0.0.1:8082,在上海机房
- 127.0.0.1:8083,在杭州机房
Nacos就将同一机房内的实例 划分为一个**集群**。
也就是说,user-service是服务,一个服务可以包含多个集群,如杭州、上海,每个集群下可以有多个实例,形成分级模型,如图:
微服务互相访问时,应该尽可能访问同集群实例,因为本地访问速度更快。
当本集群内不可用时,才访问其它集群。例如:
杭州机房内的order-service应该优先访问同机房的user-service。
### 5.3.1.给user-service配置集群
1.修改user-service的application.yml文件,添加集群配置:
spring: cloud: nacos: server-addr: localhost:8848 discovery: cluster-name: HZ # 集群名称
重启两个user-service实例后,我们可以在nacos控制台看到下面结果:
spring: cloud: nacos: server-addr: localhost:8848 discovery: cluster-name: SZ # 集群名称
启动UserApplication3后再次查看nacos控制台:
### 5.3.2.同集群优先的负载均衡
默认的`ZoneAvoidanceRule`并不能实现根据同集群优先来实现负载均衡。
因此Nacos中提供了一个`NacosRule`的实现,可以优先从同集群中挑选实例。
(同集群间随机)
1)给order-service配置集群信息
修改order-service的application.yml文件,添加集群配置:
spring: cloud: nacos: server-addr: localhost:8848 discovery: cluster-name: HZ # 集群名称
2)修改负载均衡规则
修改order-service的application.yml文件,修改负载均衡规则:
userservice: ribbon: NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则
## 5.4.权重配置
实际部署中会出现这样的场景:
服务器设备性能有差异,部分实例所在机器性能较好,另一些较差,我们希望性能好的机器承担更多的用户请求。
但默认情况下NacosRule是同集群内随机挑选,不会考虑机器的性能问题。
因此,Nacos提供了权重配置来控制访问频率,权重越大则访问频率越高。
在nacos控制台,找到user-service的实例列表,点击编辑,即可修改权重:
在弹出的编辑窗口,修改权重:
> **注意**:如果权重修改为0,则该实例永远不会被访问
## 5.5.环境隔离
Nacos提供了namespace来实现环境隔离功能。
- - nacos中可以有多个namespace
- - namespace下可以有group、service等
- - 不同namespace之间相互隔离,例如不同namespace的服务互相不可见
例如,修改order-service的application.yml文件:
spring: cloud: nacos: server-addr: localhost:8848 discovery: cluster-name: HZ namespace: 492a7d5d-237b-46a1-a99a-fa8e98e4b0f9 # 命名空间,填ID
重启order-service后,访问控制台,可以看到下面的结果:
此时访问order-service,因为namespace不同,会导致找不到userservice,控制台会报错:
## 5.6.Nacos与Eureka的区别
Nacos的服务实例分为两种l类型:
- - 临时实例:如果实例宕机超过一定时间,会从服务列表剔除,默认的类型。
- - 非临时实例:如果实例宕机,不会从服务列表剔除,也可以叫永久实例。
配置一个服务实例为永久实例:
spring: cloud: nacos: discovery: ephemeral: false # 设置为非临时实例
Nacos和Eureka整体结构类似,服务注册、服务拉取、心跳等待,但是也存在一些差异:
- Nacos与eureka的共同点
- - 都支持服务注册和服务拉取
- - 都支持服务提供者心跳方式做健康检测
- Nacos与Eureka的区别
- - Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式
- - 临时实例心跳不正常会被剔除,非临时实例则不会被剔除
- - Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
- - Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式
# 1.实用篇-Nacos配置管理
Nacos除了可以做注册中心,同样可以做配置管理来使用。
## 1.1.统一配置管理
当微服务部署的实例越来越多,达到数十、数百时,逐个修改微服务配置就会让人抓狂,而且很容易出错。我们需要一种统一配置管理方案,可以集中管理所有实例的配置。
Nacos一方面可以将配置集中管理,另一方可以在配置变更时,及时通知微服务,实现配置的热更新。
### 1.1.1.在nacos中添加配置文件
如何在nacos中管理配置呢?
在弹出的表单中,填写配置信息:
> 注意:项目的核心配置,需要热更新的配置才有放到nacos管理的必要。基本不会变更的一些配置还是保存在微服务本地比较好。
### 1.1.2.从微服务拉取配置
微服务要拉取nacos中管理的配置,并且与本地的application.yml配置合并,才能完成项目启动。
但如果尚未读取application.yml,又如何得知nacos地址呢?
因此spring引入了一种新的配置文件:bootstrap.yaml文件,会在application.yml之前被读取,流程如下:
1)引入nacos-config依赖
首先,在user-service服务中,引入nacos-config的客户端依赖:
<!--nacos配置管理依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>
2)添加bootstrap.yaml
然后,在user-service中添加一个bootstrap.yaml文件,内容如下:
spring: application: name: userservice # 服务名称 profiles: active: dev #开发环境,这里是dev cloud: nacos: server-addr: localhost:8848 # Nacos地址 config: file-extension: yaml # 文件后缀名
这里会根据spring.cloud.nacos.server-addr获取nacos地址,再根据
`${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}`作为文件id,来读取配置。
本例中,就是去读取`userservice-dev.yaml`:
3)读取nacos配置
在user-service中的UserController中添加业务逻辑,读取pattern.dateformat配置:
代码:
@Value("${pattern.dateformat}") private String dateformat; @GetMapping("now") public String now(){ return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat)); }
在页面访问,可以看到效果:
## 1.2.配置热更新
我们最终的目的,是修改nacos中的配置后,微服务中无需重启即可让配置生效,也就是**配置热更新**。
要实现配置热更新,可以使用两种方式:
### 1.2.1.方式一
在@Value注入的变量所在类(UserController)上添加注解@RefreshScope:
### 1.2.2.方式二
使用@ConfigurationProperties注解代替@Value注解。
在user-service服务中,添加一个类,读取patterrn.dateformat属性:
在UserController中使用这个类代替@Value:
代码:
@Autowired private PatternProperties patternProperties; @GetMapping("now") public String now(){ return LocalDateTime.now().format(DateTimeFormatter.ofPattern(patternProperties.getDateformat())); }
## 1.3.配置共享
下面我们通过案例来测试配置共享
### 1)添加一个环境共享配置
我们在nacos中添加一个userservice.yaml文件:
### 2)在user-service中读取共享配置
在user-service服务中,修改PatternProperties类,读取新添加的属性:
在user-service服务中,修改UserController,添加一个方法:
### 3)运行两个UserApplication,使用不同的profile
修改UserApplication2这个启动项,改变其profile值:
这样,UserApplication(8081)使用的profile是dev,UserApplication2(8082)使用的profile是test。
启动UserApplication和UserApplication2
访问http://localhost:8081/user/prop,结果:
访问http://localhost:8082/user/prop,结果:
可以看出来,不管是dev,还是test环境,都读取到了envSharedValue这个属性的值。
### 4)配置共享的优先级
当nacos、服务本地同时出现相同属性时,优先级有高低之分:
## 1.4.搭建Nacos集群
Nacos生产环境下一定要部署为集群状态
nacos集群搭建1
上面我们一直使用的都是单点的nacos,这种方式适合测试,但是在生产环境是不使用单点nacos。原因: 生产环境强调高可用,所以nacos要做成集群
官方Nacos集群图如下。其中包含3个nacos节点,然后一个负载均衡器(例如nginx)来代理3个Nacos服务
我们将要学习的Nacos集群图如下
三个nacos节点的地址:
节点
ip
port
nacos1
127.0.0.1
8845
nacos2
127.0.0.1
8846
nacos3
127.0.0.1
8847
下面的操作是集群配置
第一步: 准备数据库数据。在你数据库执行如下语句create database if not exists nacos; use nacos;
CREATE TABLE `config_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `data_id` varchar(255) NOT NULL COMMENT 'data_id', `group_id` varchar(255) DEFAULT NULL, `content` longtext NOT NULL COMMENT 'content', `md5` varchar(32) DEFAULT NULL COMMENT 'md5', `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', `src_user` text COMMENT 'source user', `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip', `app_name` varchar(128) DEFAULT NULL, `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段', `c_desc` varchar(256) DEFAULT NULL, `c_use` varchar(64) DEFAULT NULL, `effect` varchar(64) DEFAULT NULL, `type` varchar(64) DEFAULT NULL, `c_schema` text, PRIMARY KEY (`id`), UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info'; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = config_info_aggr */ /******************************************/ CREATE TABLE `config_info_aggr` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `data_id` varchar(255) NOT NULL COMMENT 'data_id', `group_id` varchar(255) NOT NULL COMMENT 'group_id', `datum_id` varchar(255) NOT NULL COMMENT 'datum_id', `content` longtext NOT NULL COMMENT '内容', `gmt_modified` datetime NOT NULL COMMENT '修改时间', `app_name` varchar(128) DEFAULT NULL, `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段', PRIMARY KEY (`id`), UNIQUE KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='增加租户字段'; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = config_info_beta */ /******************************************/ CREATE TABLE `config_info_beta` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `data_id` varchar(255) NOT NULL COMMENT 'data_id', `group_id` varchar(128) NOT NULL COMMENT 'group_id', `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name', `content` longtext NOT NULL COMMENT 'content', `beta_ips` varchar(1024) DEFAULT NULL COMMENT 'betaIps', `md5` varchar(32) DEFAULT NULL COMMENT 'md5', `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', `src_user` text COMMENT 'source user', `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip', `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段', PRIMARY KEY (`id`), UNIQUE KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_beta'; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = config_info_tag */ /******************************************/ CREATE TABLE `config_info_tag` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `data_id` varchar(255) NOT NULL COMMENT 'data_id', `group_id` varchar(128) NOT NULL COMMENT 'group_id', `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id', `tag_id` varchar(128) NOT NULL COMMENT 'tag_id', `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name', `content` longtext NOT NULL COMMENT 'content', `md5` varchar(32) DEFAULT NULL COMMENT 'md5', `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', `src_user` text COMMENT 'source user', `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip', PRIMARY KEY (`id`), UNIQUE KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_tag'; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = config_tags_relation */ /******************************************/ CREATE TABLE `config_tags_relation` ( `id` bigint(20) NOT NULL COMMENT 'id', `tag_name` varchar(128) NOT NULL COMMENT 'tag_name', `tag_type` varchar(64) DEFAULT NULL COMMENT 'tag_type', `data_id` varchar(255) NOT NULL COMMENT 'data_id', `group_id` varchar(128) NOT NULL COMMENT 'group_id', `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id', `nid` bigint(20) NOT NULL AUTO_INCREMENT, PRIMARY KEY (`nid`), UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`), KEY `idx_tenant_id` (`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation'; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = group_capacity */ /******************************************/ CREATE TABLE `group_capacity` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', `group_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整个集群', `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值', `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量', `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值', `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数,,0表示使用默认值', `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值', `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量', `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_group_id` (`group_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表'; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = his_config_info */ /******************************************/ CREATE TABLE `his_config_info` ( `id` bigint(64) unsigned NOT NULL, `nid` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `data_id` varchar(255) NOT NULL, `group_id` varchar(128) NOT NULL, `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name', `content` longtext NOT NULL, `md5` varchar(32) DEFAULT NULL, `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `src_user` text, `src_ip` varchar(50) DEFAULT NULL, `op_type` char(10) DEFAULT NULL, `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段', PRIMARY KEY (`nid`), KEY `idx_gmt_create` (`gmt_create`), KEY `idx_gmt_modified` (`gmt_modified`), KEY `idx_did` (`data_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='多租户改造'; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = tenant_capacity */ /******************************************/ CREATE TABLE `tenant_capacity` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', `tenant_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Tenant ID', `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值', `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量', `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值', `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数', `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值', `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量', `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_tenant_id` (`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='租户容量信息表'; CREATE TABLE `tenant_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `kp` varchar(128) NOT NULL COMMENT 'kp', `tenant_id` varchar(128) default '' COMMENT 'tenant_id', `tenant_name` varchar(128) default '' COMMENT 'tenant_name', `tenant_desc` varchar(256) DEFAULT NULL COMMENT 'tenant_desc', `create_source` varchar(32) DEFAULT NULL COMMENT 'create_source', `gmt_create` bigint(20) NOT NULL COMMENT '创建时间', `gmt_modified` bigint(20) NOT NULL COMMENT '修改时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`), KEY `idx_tenant_id` (`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tenant_info'; CREATE TABLE `users` ( `username` varchar(50) NOT NULL PRIMARY KEY, `password` varchar(500) NOT NULL, `enabled` boolean NOT NULL ); CREATE TABLE `roles` ( `username` varchar(50) NOT NULL, `role` varchar(50) NOT NULL, UNIQUE INDEX `idx_user_role` (`username` ASC, `role` ASC) USING BTREE ); CREATE TABLE `permissions` ( `role` varchar(50) NOT NULL, `resource` varchar(255) NOT NULL, `action` varchar(8) NOT NULL, UNIQUE INDEX `uk_role_permission` (`role`,`resource`,`action`) USING BTREE ); INSERT INTO users (username, password, enabled) VALUES ('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', TRUE); INSERT INTO roles (username, role) VALUES ('nacos', 'ROLE_ADMIN');
第二步: 在G盘新建nacosGroup文件夹,下载链接里的nacos-server-1.4.1压缩包,并解压到nacosGroup文件夹
第三步: 把解压后的nacos文件夹,里面的cluster.conf.example文件重命名为cluster.conf
第四步: 在cluster.conf文件添加如下
第五步: 把application.properties文件修改为如下
第六步: 把做好的nacos文件夹复制三份,分别命名为nacos1、nacos2、nacos3
第七步: 把这三个nacos文件夹的conf/application.properties文件的端口改一下,分别是8845、8846、8847
nacos集群启动之后,接下来就是安装启动nginx负载均衡器
第一步: 在G盘新建nacosNginx目录,把下载好的nginx-1.18.0压缩包解压到nacosNginx目录
第二步: 把nacosNginx目录的nginx-1.18.0/conf/nginx.conf文件,修改为如下
注意三个server地址要跟你上面做的nacos一致,作用是nginx对谁进行负载均衡,也就是对我们启动的三台nacos节点进行负载均衡
listen的作用是反向代理,简单说就是你浏览器访问nacos时,不需要输入8848端口,直接就是80端口
/nacos表示代理路径,也就是你访问/nacos路径时,实际访问的是 http://nacos-clusternacos 集群路径Nginx复制代码
upstream nacos-cluster { server 127.0.0.1:8845; server 127.0.0.1:8846; server 127.0.0.1:8847; } server { listen 80; server_name localhost; location /nacos { proxy_pass http://nacos-cluster; } }
第三步: 启动nginx。在命令行输入如下。然后浏览器访问: http://localhost/nacos 。用户名和密码都是nacosPlain Text复制代码
d: cd D:\nacosNginx\nginx-1.18.0 start nginx.exe
集群搭建成功,看着好像是只访问了一台nacos,实际上已经在三台nacos之间做了一个负载均衡,也就是三台nacos都可被访问
6.Feign远程调用
http客户端Feign
6.1 Feign替代RestTemplate
先来看我们以前利用RestTemplate发起远程调用的代码:
存在下面的问题:
- •代码可读性差,编程体验不统一
- •参数复杂URL难以维护
Feign是一个声明式的http客户端,官方地址:https://github.com/OpenFeign/feign
其作用就是帮助我们优雅的实现http请求的发送,解决上面提到的问题。
Fegin的使用步骤如下:
### 1)引入依赖
我们在order-service服务的pom文件中引入feign的依赖:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
### 2)添加注解
在order-service的启动类添加注解开启Feign的功能:
@EnableFeignClients
### 3)编写Feign的客户端
在order-service中新建一个接口,内容如下:
@FeignClient("userservice") public interface UserClient { @GetMapping("/user/{id}") User findById(@PathVariable("id") Long id); }
这个客户端主要是基于SpringMVC的注解来声明远程调用的信息,比如:
- - 服务名称:userservice
- - 请求方式:GET
- - 请求路径:/user/{id}
- - 请求参数:Long id
- - 返回值类型:User
这样,Feign就可以帮助我们发送http请求,无需自己使用RestTemplate来发送了。
### 4)测试
修改order-service中的OrderService类中的queryOrderById方法,使用Feign客户端代替RestTemplate:
是不是看起来优雅多了。
6.2 自定义配置Feign可以支持很多的自定义配置,如下表所示:
一般情况下,默认值就能满足我们使用,如果要自定义时,只需要创建自定义的@Bean覆盖默认Bean即可。
下面以日志为例来演示如何自定义配置。
### 2.2.1.配置文件方式
基于配置文件修改feign的日志级别可以针对单个服务:
feign: client: config: userservice: # 针对某个微服务的配置 loggerLevel: FULL # 日志级别
也可以针对所有服务:
feign: client: config: default: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置 loggerLevel: FULL # 日志级别
而日志的级别分为四种:
- - NONE:不记录任何日志信息,这是默认值。
- - BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
- - HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
- - FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
### 2.2.2.Java代码方式
也可以基于Java代码来修改日志级别,先声明一个类,然后声明一个Logger.Level的对象:
public class DefaultFeignConfiguration { @Bean public Logger.Level feignLogLevel(){ return Logger.Level.BASIC; // 日志级别为BASIC } }
如果要**全局生效**,将其放到启动类的@EnableFeignClients这个注解中:
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration .class)
如果是**局部生效**,则把它放到对应的@FeignClient这个注解中:
@FeignClient(value = "userservice", configuration = DefaultFeignConfiguration .class)
6.3 Feign使用优化Feign底层发起http请求,依赖于其它的框架。其底层客户端实现包括:
- •URLConnection:默认实现,不支持连接池
- •Apache HttpClient :支持连接池
- •OKHttp:支持连接池
因此提高Feign的性能主要手段就是使用**连接池**代替默认的URLConnection。
这里我们用Apache的HttpClient来演示。
1)引入依赖
在order-service的pom文件中引入Apache的HttpClient依赖:
<!--httpClient的依赖 --> <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-httpclient</artifactId> </dependency>
2)配置连接池
在order-service的application.yml中添加配置:
feign: client: config: default: # default全局的配置 loggerLevel: BASIC # 日志级别,BASIC就是基本的请求和响应信息 httpclient: enabled: true # 开启feign对HttpClient的支持 max-connections: 200 # 最大的连接数 max-connections-per-route: 50 # 每个路径的最大连接数
总结,Feign的优化:
1.日志级别尽量用basic
2.使用HttpClient或OKHttp代替URLConnection
① 引入feign-httpClient依赖
② 配置文件开启httpClient功能,设置连接池参数
6.4 最佳实践所谓最近实践,就是使用过程中总结的经验,最好的一种使用方式。
自习观察可以发现,Feign的客户端与服务提供者的controller代码非常相似
没有一种办法简化这种重复的代码编写呢?
方式一: 继承。给消费者的FeignClient和提供者的controller定义统一的父接口作为标准
特点: 紧耦合,仅适用于面向契约编程的思想上来使用
操作: 让controller和FeignClient继承同一接口
方式二: 抽取。 将FeignClient抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用
特点; 低耦合,但是高冗余
操作: 将FeignClient、POJO、Feign的默认配置都定义到一个项目中,供所有消费者使用演示上面说到的方式二"抽取"的Feign实践,实现步骤:
1、首先创建一个module,命名为feign-api,然后引入feign的starter依赖
2、就可以把order-service中的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中
3、要使用怎么办,直接在order-service中引入feign-api的依赖即可
4、然后修改order-service中的所有与"UserClient、User、DefaultFeignConfiguration"有关的Import部分,改成导入feign-api中的包
具体步骤如下:
第一步: 在cloud-demo总项目中新建一个项目,项目名为feign-api
第二步: 把feign-api微服务项目的pom.xml修改为如下<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>cloud-demo</artifactId> <groupId>cn.itcast.demo</groupId> <version>1.0</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>feign-api</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> </dependencies> </project>
第三步: 在feign-api项目的src/main/java目录下新建cn.itcast.feign包,接着把order-service项目的src/main/java/cn.itcast.order/clients目录、config目录、pojo目录复制到feign-api项目的src/main/java/cn.itcast.feign目录里面。注意粘贴后的clients目录的UserClient接口会爆红,改一下import即可
第四步: 以后哪个项目需要使用Feign时,就直接在pom.xml使用刚写好的feign-api项目即可。例如把order-service项目中使用,我们可以把在order-service项目中有关Feign配置的接口和类删掉(此时order-service项目有关Feign的功能类会报错,等下会解决),然后在order-service项目的pom.xml中引入刚写好的feign-api项目依赖,接着在报错的有关Feign功能类里面重新引入一下即可解决报错<!--引入feign-api项目(自己写的)的依赖--> <dependency> <groupId>cn.itcast.demo</groupId> <artifactId>feign-api</artifactId> <version>1.0</version> </dependency>
第五步: 解决bug。当我们启动OrderApplication、UserApplication、UserApplication2服务时,会发现报错了,找不到feign-api项目的UserClient接口。解决方式有两种
第一种解决方式: 在order-service项目的启动类,指定FeignClient所在包,适合于有多个UserClient时,缺点是会把其它用不上的UserClient也加入进来@EnableFeignClients(basePackage = "cn.itcast.feign.clients")
第二种解决方式: 在order-service项目的启动类,指定FeignClient字节码,也就是直接指定扫描哪个写好的UserClient,也可以指定多个。推荐这种解决方案@EnableFeignClients(clients = {UserClient.class})
第六步。测试(先确保你的nacos已经启动),重启OrderApplication、UserApplication、UserApplication2服务
浏览器访问: http://localhost:8080/order/103
7.Gateway
前面学的Nacos是对内负载均衡,现在学的Gateway网关是对外负载均衡和校验,不冲突
1. 网关的作用
网关功能:
1、身份认证和权限校验
2、服务路由、负载均衡
3、请求限流
在SpringCloud中有两个组件可以实现网关。分别是gateway、zuul
Zuul是基于Servlet的实现,属于阻塞式编程。而SpringCloudGateway则是基于spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能
2. 网关的快速入门
网关路由可以配置的内容包括如下
路由id: 路由唯一标识
uri: 路由目的地,支持lb和http两种
predicates: 路由断言,判断请求是否符合要求,符合则转发到路由目的地
filters: 路由过滤器,处理请求或响应
搭建网关服务的步骤如下
第一步: 由于网关是一个服务,所以需要在cloud-demo总项目中新建一个项目,项目名为gateway
第二步: 在gateway微服务项目的pom.xml修改为如下
<!--nacos服务注册发现依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--网关gateway依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
第三步: 在gateway微服务项目的src/main/java目录下新建cn.itcast.gateway.GatewayApplication类,写入如下
@SpringBootApplication
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class,args);
}
第四步: 在gateway微服务项目的src/main/resources目录下新建File,File名为application.yml,写入如下
server:
port: 10010 #服务端口
spring:
application:
name: gateway #当前这个gateway微服务项目的服务名称
cloud:
nacos:
server-addr: localhost:8848 #自己本地开启的nacos的地址
gateway:
routes: #自定义网关路由规则,多个规则就用-
- id: User-Service #自定义路由标识,必须唯一
uri: lb://UserService #路由目标地址,也就是用户请求时,会请求到哪里,UserService是已有的服务名称
predicates: #路由断言,作用是判断路由是否符合规则,也就是用户请求是否符合规则,符合的话,才会被网关路由到某个服务名称,多个断言用-
- Path=/user/** #路径断言,判断用户请求的路径是否是以/user/开头,如果是则符合规则
- id: Order-Service #自定义路由标识,必须唯一
uri: lb://OrderService #让用户请求访问的是OrderService微服务
predicates:
- Path=/order/** #当用户请求路径是以开头时,才会被网关路由到OrderService微服务
第五步。测试(先确保你的nacos已经启动),先运行GatewayApplication类,然后重启OrderApplication、UserApplication、UserApplication2服务
浏览器访问: http://localhost:10010/user/3,注意要http://localhost:10010/user/开头,因为我们在第四步指定了路由断言
可以发现,我们并没有在gateway微服务项目写任何业务代码,但是却能用gateway微服务项目的路径访问到数据,原因就是网关路由,把gateway微服务项目的请求路由到我们指定的其他微服务去了,我们访问这个gateway微服务项目,实际访问的是OrderService和UserService微服务
3. 路由断言工厂
路由断言工厂Route Predicate Factory
网关路由可以配置的内容包括如下
路由id: 路由唯一标识
uri: 路由目的地,支持lb和http两种
predicates: 路由断言,判断请求是否符合要求,符合则转发到路由目的地
filters: 路由过滤器,处理请求或响应
路由id: 路由唯一标识 uri: 路由目的地,支持lb和http两种 predicates: 路由断言,判断请求是否符合要求,符合则转发到路由目的地 filters: 路由过滤器,处理请求或响应
例如 Path=/user/**是按照路径匹配,这个规则是由
org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory类来处理的
像这样的断言工厂在SpringCloudGateway还有几十个
spring提供了11种基本的Predicate工厂,如下表。
为了避免md语法冲突,我在下表写的※其实就是*
名称 | 说明 | 示例 |
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必须是指定范围 | - RemoteAddr=192.168.1.1/24 |
Weight | 权重处理 |
不会写也没事,spring官网有提供12种断言工厂的示例
Spring Cloud Gatewayhttps://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gateway-request-predicates-factories
演示如下,在刚刚快速入门的gateway微服务项目的application.yml里面添加如下
# 演示After断言工厂,也就是用户必须在亚洲上海时区2037-01-20之后访问,才算符合规则,才会让用户去请求路由到达OrderService服务
- After=2037-01-20T17:42:47.789-07:00[America/Denver] #明显我们现在的时间是不符合要求的,所以等下演示会报404
然后重启GatewayApplication服务,浏览器访问http://localhost:10010/order/102
4. 路由过滤工厂
路由的过滤器配置,路由过滤器GatewayFilter
网关路由可以配置的内容包括如下
路由id: 路由唯一标识
uri: 路由目的地,支持lb和http两种
predicates: 路由断言,判断请求是否符合要求,符合则转发到路由目的地
filters: 路由过滤器,处理请求或响应
接下来,就重点学习filters的配置
GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理。不仅可以对请求做处理,还可以对响应做处理,下面是流程图
spring提供了37种不同的路由过滤工厂,如下表
名称 | 说明 |
AddRequestHeader | 给当前请求添加一个请求头 |
RemoveRequestHeader | 移除请求中的一个请求头 |
AddResponseHeader | 给响应结果中添加一个响应头 |
RemoveResponseHeader | 从响应结果中移除有一个响应头 |
RequestRateLimiter | 限制请求的流量 |
...... |
不会写也没事,spring官网有提供37种断言工厂的示例
Spring Cloud Gateway
演示如下
第一步: 在刚刚快速入门的gateway微服务项目的application.yml里面添加如下。
表示给所有进入UserService服务的请求添加一个请求头: Hello=can you allow me request
# 演示过滤工厂
filters: #内部多个过滤器的话用-上下隔开
# AddRequestHeader表示给所有进入UserService服务的请求添加一个请求头: Hello=can you allow me request
- AddRequestHeader=Hello,can you allow me request #格式: - AddRequestHeader=key,value
然后重启GatewayApplication服务
第二步: 由于请求UserService服务时,网关给路径自动追加了请求头参数,所以我们需要去user-service微服务的src/main/java/cn.itcast.user/web目录的UserController类里面稍微修改一下请求,加一个接收参数的参数
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id, @RequestHeader(value = "Hello",required = false) String HelloGG) {
System.out.println("获取到了请求头: "+HelloGG);
return userService.queryById(id);
}
然后重启UserService、UserService2服务
第三步: 测试。浏览器访问http://localhost:10010/user/1,访问之后回到终端看一下UserService、UserService2的日志信息,看有没有打印sout那个语句
思考: 我们只是在gateway微服务项目的application.yml配置里面给访问UserService服务(user-service微服务项目)的请求添加了请求头,要是需要给所有请求微服务项目的请求都加上请求头,那岂不是在这个application.yml里面给所以相关微服务都写一遍这个代码,太麻烦了
解决: 默认过滤器,如果要对所有的路由都生效,则可以将过滤器工厂写到default下。格式如下
#演示默认过滤器,会对所有的路由请求都生效
default-filters:
- AddRequestHeader=Hello,can you allow me request #格式: - AddRequestHeader=key,value
然后重启GatewayApplication服务,浏览器访问http://localhost:10010/user/2,访问之后回到终端看一下UserService、UserService2的日志信息,看有没有打印sout那个语句
5. 全局过滤器
全局过滤器GlobalFilter的作用是拦截所有进入网关的请求和微服务响应,与GatewayFilter默认过滤器的作用一样
上面刚学的默认过滤器虽然可以作用于所有进入网关的请求和微服务响应,但是默认过滤器是通过配置的方式来定义的,配置的仅仅是参数,过滤器的业务逻辑是无法控制的,由spring写死的,功能有限。但是,如果某些业务比较复杂,例如请求进来后端,但是我想知道是谁发起的,身份是什么,有没有权限访问我,那么这些额外是功能就需要自定义自己来写,而全局过滤器GlobalFilter就能实现这个,特点是可以自定义功能
如何才能使用全局过滤器GlobalFilter,
我们只需要实现GlobalFilter接口即可,GlobalFilter接口的方法如下
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
//exchange: 请求上下文,里面可以获取Request、Response等信息
//chain: 过滤器队列,用来把请求委托给下一个过滤器,也就是放行,交给下一个过滤器
//Mono<Void>: 返回值
//filter: 方法名
案例: 定义全局过滤器,拦截请求,判断请求的参数是否满足下面条件
条件1: 参数中是否有authorization
条件2: authorization参数值是否为admin
如果同时满足则放行,否则拦截
操作过程如下
第一步: 在gateway微服务项目的src/main/java/cn.itcast.gateway目录新建AuthorizeFilter类,写入如下
package cn.itcast.gateway;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Order(-1)//过滤器执行的前后顺序,值越小越先执行,可能你的同事也定义有过滤器,所以这个可以用数字设置自己这个过滤器先执行还是后执行
@Component
public class AuthorizeFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//第一步: 通过exchange上下文对象来获取请求参数
ServerHttpRequest xxrequest = exchange.getRequest();
MultiValueMap<String, String> xxparams = xxrequest.getQueryParams();//返回值是Map集合
//第二步: 获取参数中的authorizationHelloHaha参数
String xxauth = xxparams.getFirst("authorizationHelloHaha");
//第三步: 判断authorization参数值是否为admin
if ("admin".equals(xxauth)) {
//是,放行
return chain.filter(exchange);
}
//如果要拦截了,我们就要设置状态码让用户知道请求失败了。UNAUTHORIZED表示未登录,用户就会看到401状态码
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
//否,拦截
return exchange.getResponse().setComplete();
}
}
两种一样
第二步: 重启GatewayApplication服务,分别在浏览器访问
http://localhost:10010/user/2
http://localhost:10010/user/2?authorizationHelloHaha=admin
6. 过滤器链执行顺序
到此,我们学习了三种过滤器,分别是: 当前路由过滤器、DefaultFilter、GlobalFilter。这三种过滤器在网关中的执行顺序是怎么样的呢
在上面的 '5. 全局过滤器' 的第一步里面,我们初步使用了@Order注解来指定order值,使得我们写的 'AuthorizeFilter过滤器类' 的优先级最高,不被覆盖(当然也没有谁来覆盖,因为就只定义过这个过滤器)
当请求路由之后,会将 '当前路由过滤器'、'DefaultFilter'、'GlobalFilter',合并到一个过滤器链(集合)中,然后对这些过滤器进行排序,然后依次执行每个过滤器
过滤器执行顺序:
1、每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前
2、'GlobalFilter' 通过实现Ordered接口,或者添加@Order注解(在前面 '5. 全局过滤器' 使用过一次)来指定order值,由我们自己指定order值大小
3、'路由过滤器'、'defaultFilter' 通过spring来指定order值,默认是按照声明顺序从1递增
从上图中,我们不难发现,'路由过滤器' 和 'defaultFilter过滤器' 的order值是有可能同样的,那这时候这俩order值不就一样了吗,这还怎么判断谁优先,另外由于'GlobalFilter过滤器'是可以通过@Order注解直接指定order值,那么此时三种过滤器的order值可能就一模一样,那靠order还怎么判断谁优先级。直接说结论,如下
1、同一种过滤器中,如果order值越小,那么优先级越高
2、不同过滤器中,如果order值越小,那么优先级越高
3、不同过滤器中,如果order值相同,那么优先级为 'defaultFilter过滤器' > '路由过滤器' > 'GlobalFilter过滤器'
7. 网关的cors跨域配置
跨域问题处理
在微服务项目中,所有的请求都必须先进入网关,然后由网关路由到某个具体的微服务,也就是跨域请求实际上不需要在每个微服务里面都处理一遍,仅仅在网关处理即可,所以我们需要学习如何在网关,来处理跨域请求
网关是基于Netflix来实现的。
跨域也就是域名不一致,主要包括域名不同、域名相同但端口不同。
跨域问题: 浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题。
解决方案: CORS(浏览器会不断询问服务器: 你让不让别人跨域访问你)
上图倒数第三行的*表示允许所有请求头跨域。
上图的最后一行的360000是有限期,作用是减少性能损耗(损耗来源: 浏览器不断询问服务器),当时间超过这个值,就表名跨域请求的有效期过了,浏览器将不再向服务器发起询问,而是直接放行。
上图的'[/**]'表示拦截所有请求,凡是进入网关的请求都会进行跨域处理
演示跨域并解决跨域,如下
第一步: 打开前端页面并向服务器发送请求,模拟跨域请求
第二步: 在网关微服务那里,写入处理跨域的代码
spring:
cloud:
gateway:
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许哪些网站的跨域请求
- "http://localhost:8090"
- "http://www.leyou.com"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期
第三步: 测试。再次回到前端页面,刷新网页,观察是否还会产生跨域