1、简介
SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验。
其中常见的组件包括:
2、服务拆分和远程调用
2.1 服务拆分
这里总结了微服务拆分时的几个原则:
- 不同微服务,不要重复开发相同业务
- 微服务数据独立,不要访问其它微服务的数据库
- 微服务可以将自己的业务暴露为接口,供其它微服务调用
eg:
cloud-demo:父工程,管理依赖
- order-service:订单微服务,负责订单相关业务
- user-service:用户微服务,负责用户相关业务
要求:
- 订单微服务和用户微服务都必须有各自的数据库,相互独立
- 订单服务和用户服务都对外暴露Restful的接口
- 订单服务如果需要查询用户信息,只能调用用户服务的Restful接口,不能查询用户数据库
数据库:
2.2 远程调用
在order-service中的根据id查询订单业务,要求在查询订单的同时,根据订单中包含的userId查询出用户信息,一起返回。
需要在order-service中 向user-service发起一个http的请求,调用http://localhost:8081/user/{userId}这个接口。
步骤如下:
- 1、注册一个RestTemplate的实例到Spring容器(在启动类上)
- 修改order-service服务中的OrderService类中的queryOrderById方法,根据Order对象中的userId查询User
- 将查询的User填充到Order对象,一起返回
2.3 提供者与消费者
在服务调用关系中,会有两个不同的角色:
服务提供者:一次业务中,被其它微服务调用的服务。(提供接口给其它微服务)
服务消费者:一次业务中,调用其它微服务的服务。(调用其它微服务提供的接口)
但是,服务提供者与服务消费者的角色并不是绝对的,而是相对于业务而言。
如果服务A调用了服务B,而服务B又调用了服务C,服务B的角色是什么?
- 对于A调用B的业务而言:A是服务消费者,B是服务提供者
- 对于B调用C的业务而言:B是服务消费者,C是服务提供者
因此,服务B既可以是服务提供者,也可以是服务消费者。
3、Eureka注册中心(略)
问题1:order-service如何得知user-service实例地址?
获取地址信息的流程如下:
- user-service服务实例启动后,将自己的信息注册到eureka-server(Eureka服务端)。这个叫服务注册
- eureka-server保存服务名称到服务实例地址列表的映射关系
- order-service根据服务名称,拉取实例地址列表。这个叫服务发现或服务拉取
问题2:order-service如何从多个user-service实例中选择具体的实例?
- order-service从实例列表中利用负载均衡算法选中一个实例地址
- 向该实例地址发起远程调用
问题3:order-service如何得知某个user-service实例是否依然健康,是不是已经宕机?
- user-service会每隔一段时间(默认30秒)向eureka-server发起请求,报告自己状态,称为心跳
- 当超过一定时间没有发送心跳时,eureka-server会认为微服务实例故障,将该实例从服务列表中剔除
- order-service拉取服务时,就能将故障实例排除了
总结:
3.1 服务注册
- 1、引入依赖
- 2、配置文件
- 3、启动多个user-service实例
3.2 服务发现
- 1、引入依赖
- 2、配置文件
- 3、服务拉取和负载均衡(添加对应的注解)
在order-service的OrderApplication中,给RestTemplate这个Bean添加一个**@LoadBalanced**注解:
4、Ribbon负载均衡(略)
负载均衡的规则都定义在IRule接口中,而IRule有很多不同的实现类:
(默认的实现就是ZoneAvoidanceRule,是一种轮询方案)
自定义负载均衡策略
通过定义IRule实现可以修改负载均衡规则,有两种方式:
- 代码方式:在order-service中的OrderApplication类中,定义一个新的IRule:
@Bean
public IRule randomRule(){return new RandomRule();
}
- 配置文件方式:在order-service的application.yml文件中,添加新的配置也可以修改规则:
userservice: # 给某个微服务配置负载均衡规则,这里是userservice服务ribbon:NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则
注意,一般用默认的负载均衡规则,不做修改。
饥饿加载
Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。
而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载:
ribbon:eager-load:enabled: trueclients: userservice
=================================================================================================
Ribbon和Feign都是用于调用其他服务的,不过方式不同。
1.启动类使用的注解不同,Ribbon用的是@RibbonClient,Feign用的是@EnableFeignClients。
2.服务的指定位置不同,Ribbon是在@RibbonClient注解上声明,Feign则是在定义抽象方法的接口中使用@FeignClient声明。
3.调用方式不同,Ribbon需要自己构建http请求,模拟http请求然后使用RestTemplate发送给其他服务,步骤相当繁琐。
Feign则是在Ribbon的基础上进行了一次改进,采用接口的方式,将需要调用的其他服务的方法定义成抽象方法即可,
不需要自己构建http请求。不过要注意的是抽象方法的注解、方法签名要和提供服务的方法完全一致。
5、Nacos注册中心(※)
是SpringCloud中的一个组件。相比Eureka功能更加丰富。
5.1 服务注册到nacos
在cloud-demo父工程的pom文件中的<dependencyManagement>
中引入SpringCloudAlibaba的依赖:
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alibaba-dependencies</artifactId><version>2.2.6.RELEASE</version><type>pom</type><scope>import</scope>
</dependency>
然后在user-service和order-service中的pom文件中引入nacos-discovery依赖:
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
在user-service和order-service的application.yml中添加nacos地址:
spring:cloud:nacos:server-addr: localhost:8848
5.2 获取服务列表
除了可以在Nacos 控制台直观的查看到 Nacos 注册中心中注册的微服务信息外,也可以
在代码中通过DiscoveryClient API
获取服务列表。
1、在controller中
在任何微服务的提供者或消费者处理器中,只要获取到“服务发现Client ”,即可读取到注册中心的微服务列表。
2、运行结果
3、注册表缓存
服务在启动后,当发生调用时 会自动从 Nacos 注册中心下载并缓存注册表到本地。所以,即使 Nacos 发生宕机,会发现消费者仍然是可以调用到提供者的。只不过此时已经不能再有服务进行注册了,服务中缓存的注册列表信息无法更新。
5.3 临时实例与持久实例
Nacos中的实例分为临时实例与持久实例。
在服务注册时有一个属性ephemeral 用于描述当前实例在注册时是否以临时实例出现。为 true 则为临时实例,默认值;为 false 则为持久实例。
区别:
临时实例与持久实例的实例存储的位置与健康检测机制是不同的。
- 临时实例: 默认情况。服务实例仅会注册在 Nacos 内存,不会持久化到 Nacos 磁盘。其健康检测机制为 Client 模式,即 Client 主动向 Server 上报其健康状态。默认心跳间隔为5 秒。在 15 秒内Server 未收到 Client 心跳,则会将其标记为“不健康”状态;在 30 秒内若收到了 Client 心跳,则重新恢复“健康”状态, 否则该实例将从 Server 端内存清除。
- 持久实例: 服务实例不仅会注册到 Nacos 内存,同时也会被持久化到 Nacos 磁盘。其健康检测机制为 Server 模式,即 Server 会主动去检测 Client 的健康状态,默认每 20 秒检测一次。健康检测失败后服务实例会被标记为“不健康”状态,但不会被清除,因为其是持久化在磁盘的。
5.4 集群搭建
给user-service配置集群
修改user-service的application.yml文件,添加集群配置:
spring:cloud:nacos:server-addr: localhost:8848discovery:cluster-name: HZ # 集群名称
同集群优先的负载均衡
默认的ZoneAvoidanceRule
并不能实现根据同集群优先来实现负载均衡。
因此Nacos中提供了一个NacosRule
的实现,可以优先从同集群中挑选实例。
1)给order-service配置集群信息
修改order-service的application.yml文件,添加集群配置:
spring:cloud:nacos:server-addr: localhost:8848discovery:cluster-name: HZ # 集群名称
2)修改负载均衡规则
修改order-service的application.yml文件,修改负载均衡规则:
userservice:ribbon:NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则
5.4.1 集群搭建步骤:
- 1、创建目录
创建一个nacos_cluster
目录。然后再复制原来配置好的单机版的 Nacos 到这个目录,并重命名为 nacos8847 。
(将来要这里要存放三个子目录,分别为 nacos8847 、 nacos8849 、 nacos8851) 。
2、修改基本配置
打开 nacos8847/conf ,重命名其中的 cluster.conf.example 为 cluster.conf 。然后打开该文件,在其中写入三个 nacos 的 ip:port 。
(注意,不能写为 localhost 与 127.0.0.1 ,且这三个端口号不能连续。否则会报地址被占用异常。)
然后再打开 nacos8847/conf/application.properties 文件,修改端口号为 8847
3、复制目录
将 nacos8847 目录复制三份,分别命名为 nacos8849 、 nacos8851。并修改各自目录中 conf/application.properties 文件中 nacos 的 端口号为 8849 与 8851 。
4、启动集群
逐个 双击三个 nacos 目录中的 bin/start up.cmd 命令 逐个 启动三台 nacos 。从启动日志上可以看到其提供的 SLB 服务访问地址及三台 Nacos 节点地址。
5.4.2 Client 连接 Nacos 集群
将微服务配置文件 application .yml 中的 nacos 地址更换为 Nacos 集群 的 VIP
5.4.3 Nacos 的 CAP 模式
默认情况下,Nacos Discovery 集群的数据一致性采用的是 AP 模式。但其也支持 CP 模式,需要进行转换。若要转换为 CP 的,可以提交如下 PUT 请求,完成 AP 到 CP 的转换。
5.5 权重配置
Nacos提供了权重配置来控制访问频率,权重越大则访问频率越高。
注意:如果权重修改为0,则该实例永远不会被访问
5.6 环境隔离
Nacos 中的服务是由三元组唯一确定的: namespace 、 group 与服务名称 service 。namespace 与 group 的作用是相同的,用于划分不同的区域范围,隔离服务。不同的是,namespace 的范围更大,不同的 namespace 中可以包含相同的 group 。不同的 group 中可以包含相同的 service 。namespace 的默认值为 public group 的默认值为 DEFAULT_GROUP
Nacos提供了namespace来实现环境隔离功能。
- nacos中可以有多个namespace
- namespace下可以有group、service等
- 不同namespace之间相互隔离,例如不同namespace的服务互相不可见
6、Nacos Config服务配置中心(※)
Nacos一方面可以将配置集中管理,另一方可以在配置变更时,及时通知微服务,实现配置的热更新。
6.1 在nacos中添加配置文件(获取远程配置)
注意:项目的核心配置,需要热更新的配置才有放到nacos管理的必要。基本不会变更的一些配置还是保存在微服务本地比较好。
6.2 从微服务拉取配置
spring引入了一种新的配置文件:bootstrap.yaml文件,会在application.yml之前被读取,流程如下:
添加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,来读取配置。
6.3 配置共享
其实微服务启动时,会去nacos读取多个配置文件,例如:
-
[spring.application.name]-[spring.profiles.active].yaml
,例如:userservice-dev.yaml -
[spring.application.name].yaml
,例如:userservice.yaml
而[spring.application.name].yaml
不包含环境,因此可以被多个环境共享。
配置共享优先级:当nacos、服务本地同时出现相同属性时,优先级有高低之分
6.4 关于配置文件的扩展
当前服务配置、共享配置与扩展配置的加载顺序为:共享配置,扩展配置,当前服务配置。若在三个配置中具有相同属性设置,但它们具有不同的值,那么,后加载的会将先加载的给覆盖。 即这三类配置的优先级由低到高是:共享配置,扩展配置,当前服务配置当前服务配置可以存在于三个地方:
- 远程配置文件:(Nacos config 中)
- 快照文件:
- 本地配置文件:(主动写入的配置文件)
这三个同名文件也存在加载顺序问题,它们的加载顺序为:本地配置文件、远程配置文件、快照配置文件。只要系统加载到了配置文件,那么后面的就不再加载。
6.5 配置热更新(动态更新配置)
修改nacos中的配置后,微服务中无需重启即可让配置生效,也就是配置热更新。
这里要实现的需求是:从数据库中根据id 查询到的 depart 的 name 值,在浏览器上显示的并不是 DB 中的 name ,而是来自于 nacos 的动态配置 depart.name 。
直接在nacos config 配置页面修改配置信息,例如添加一个 depart.name 属性。
方式一:
在@Value注入的变量所在类上添加注解@RefreshScope:
方式二
使用@ConfigurationProperties注解代替@Value注解。
在user-service服务中,添加一个类,读取patterrn.dateformat(/depart.name)属性:
package cn.itcast.user.config;import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;@Component
@Data
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {private String dateformat;
}
7、OpenFeign与负载均衡
前面消费者对于微服务的消费是通过RestTemplate 完成的,这种方式的弊端是很明显的:
- 消费者对提供者的调用无法与业务接口完全吻合。例如,原本 Service 接口中的方法是有返回值的,但经过 RestTemplate 相关 API 调用后没有了其返回值,最终执行是否成功用户并不清楚。再例如 RestTemplate 的对数据的删除与修改操作方法都没有返回值。
- 代码编写不方便,不直观。提供者原本是按照业务接口提供服务的,而经过 RestTemplate一转手,变为了 URL ,使得程序员在编写消费者对提供者的调用代码时,变得不直接、不明了。没有直接通过业务接口调用方便、清晰。
OpenFeign总结:
- OpenFeign 只涉及 Consumer 与 Provider 无关。因为其是用于 Consumer 调用 Provider 的
- OpenFeign 仅仅就是一个伪客户端,其不会对请求做任务的处理
- OpenFeign 是通过注解的方式实现 RESTful 请求的
OpenFeign具有负载均衡功能,其可以对指定的微服务采用负载均衡方式进行消费、访问。采用 SpringCloud 自行研发的 Spring Cloud Loadbalancer 作为负载均衡器。
7.1 Feign替代RestTemplate
使用Feign的步骤:
- ① 引入依赖
- ② 添加
@EnableFeignClients
注解 - ③ 编写FeignClient接口,使用
@FeignClient
注解 - ④ 使用FeignClient中定义的方法代替RestTemplate
1、添加依赖:
<!--feign依赖-->
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2、添加注解
在order-service的启动类添加 @EnableFeignClients
注解开启Feign的功能
3、编写Feign的客户端,使用@FeignClient
注解
在order-service中新建一个接口,内容如下:
package cn.itcast.order.client;import cn.itcast.order.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;@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来发送了。
注意,这里的接口名可以是任意的名称,接口中的方法名也可以是任意的名称。
但@FeignClient 参数指定的提供者服务名称是不能修改的,接口与方法上添加的 @XxxMapping中的参数是不能修改的,必须与提供者相应的请求 URI 相同。
由于其充当的是业务接口,所以一般其定义在service 包中。
别的例子:
4、测试,@Autowired导入Feign客户端
在order-service中的OrderService类中的queryOrderById方法,使用Feign客户端代替RestTemplate:
别的例子:
7.2 自定义配置
一般情况下,默认值就能满足我们使用,如果要自定义时,只需要创建自定义的@Bean覆盖默认Bean即可。
7.2.1 配置文件方式
基于配置文件修改feign的日志级别可以针对单个服务:
feign: client:config: userservice: # 针对某个微服务的配置loggerLevel: FULL # 日志级别
也可以针对所有服务:
feign: client:config: default: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置loggerLevel: FULL # 日志级别
而日志的级别分为四种:
- NONE:不记录任何日志信息,这是默认值。
- BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
- HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
- FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
7.2.2.Java代码方式
也可以基于Java代码来修改日志级别,先声明一个类,然后声明一个Logger.Level的对象:
public class DefaultFeignConfiguration {@Beanpublic Logger.Level feignLogLevel(){return Logger.Level.BASIC; // 日志级别为BASIC}
}
如果要全局生效,将其放到启动类的 @EnableFeignClients
这个注解中:
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration .class)
如果是局部生效,则把它放到对应的 @FeignClient
这个注解中:
@FeignClient(value = "userservice", configuration = DefaultFeignConfiguration .class)
7.3 超时设置
7.3.1 全局超时设置
7.3.2 局部超时设置
在全局设置的基础之上,若想单独对某些微服务单独设置超时时间,只需要将前面配置中的 default 修改为微服务名称即可。局部设置的优先级要高于全局设置的。
7.4 Gzip 压缩设置
OpenFeign可对请求与响应进行压缩设置。
7.5 Feign使用优化
Feign的优化:
-
1.日志级别尽量用basic
-
2.使用HttpClient或OKHttp代替URLConnection
① 引入feign-httpClient依赖
② 配置文件开启httpClient功能,设置连接池参数
7.6 解决扫描包问题
方式一:
指定Feign应该扫描的包:
@EnableFeignClients(basePackages = "cn.itcast.feign.clients")
方式二:
指定需要加载的Client接口:
@EnableFeignClients(clients = {UserClient.class})
7.7 负载均衡
默认 负载均衡策略,OpenFeign 的负载均衡器 Ribbon 默认采用的是轮询算法。
更换负载均衡策略:
(1)定义一个 Config 类
(2)修改启动类
在启动类上添加 @LoadBalancerClients
注解,并指定前面定义的配置类。
8、Spring Cloud Gateway微服务网关
在Spring 生态系统之上的 API 网关,包括: Spring 6 、 Spring Boot3 和 project Reactor 。
Spring Cloud Gateway 旨在提供一种简单而有效的方法来路由到 api ,并为它们提供跨领域的关注点,例如:安全性、监控/度量和弹性。
网关的核心功能特性:
-
权限控制:网关作为微服务入口,需要校验用户是是否有请求资格,如果没有则进行拦截。
-
路由和负载均衡:一切请求都必须先经过gateway,但网关不处理业务,而是根据某种规则,把请求转发到某个微服务,这个过程叫做路由。当然路由的目标服务有多个时,还需要做负载均衡。
-
限流:当请求流量过高时,在网关中按照下流的微服务能够接受的速度来放行请求,避免服务压力过大。
在 Spring Cloud Gateway 中有三个非常重要的概念:
-
(1)route 路由
路由是网关的最基本组成,由一个路由id 、一个目标地址 url ,一组断言工厂及一组 filter组成。若断言为 true ,则请求将经由 filter 被路由到目标 url 。 -
(2)predicate 断言
断言即一个条件判断,根据当前的 http 请求进行 指定 规则的匹配,比如说 http 请求头,请求时间等 。 只有当匹配上规则时,断言才为 true ,此时请求才会被直接路由到目标地址(目标服务器),或先路由到某过滤器链,经过过滤器链的层层处理后,再路由到相应的目标地址(目标服务器)。 -
(3)filter 过滤器
对请求进行处理的逻辑部分。当请求的断言为true 时,会被路由到设置好的过滤器,以对请求或 响应进行处理。例如,可以为请求添加一个请求参数,或对请求 URI 进行修改,或为响应添加 header 等。总之,就是对请求或响应进行处理。
8.1 创建一个gateway服务
步骤:
- 创建SpringBoot工程gateway,引入网关依赖
- 编写启动类
- 编写基础配置和路由规则
- 启动网关服务进行测试
1、引入依赖
<!--网关-->
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos服务发现依赖-->
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
2、编写启动类
在启动类中添加一个@Bean 方法,用于设置路由策略。
3、编写基础配置和路由规则
server:port: 10010 # 网关端口
spring:application:name: gateway # 服务名称cloud:nacos:server-addr: localhost:8848 # nacos地址gateway:routes: # 网关路由配置- id: user-service # 路由id,自定义,只要唯一即可# uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址uri: lb://userservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称predicates: # 路由断言,也就是判断请求是否符合路由规则的条件- Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
我们将符合Path
规则的一切请求,都代理到 uri
参数指定的地址。
本例中,我们将 /user/**
开头的请求,代理到lb://userservice
,lb是负载均衡,根据服务名拉取服务列表,实现负载均衡。
总结:
网关搭建步骤:
-
创建项目,引入nacos服务发现和gateway依赖
-
配置application.yml,包括服务基本信息、nacos地址、路由
路由配置包括:
-
路由id:路由的唯一标示
-
路由目标(uri):路由的目标地址,http代表固定地址,lb代表根据服务名负载均衡
-
路由断言(predicates):判断路由的规则,
-
路由过滤器(filters):对请求或响应做处理
8.2 路由断言工厂
SpringCloud Gateway 将路由 匹配 作为 最基本的功能 。 而这个功能是通过路由断言工厂完成的。 Spring Cloud Gateway 中 包含 了很多种 内置的路由 断言 工厂。所有这些 断言都可以匹配 HTTP 请求的不同属性,并且可以根据逻辑与状态, 将多个路由 断言 工厂 复合使用 。
比如:在配置文件中写的断言规则只是字符串,这些字符串会被Predicate Factory读取并处理,转变为路由判断的条件例如Path=/user/**是按照路径匹配,这个规则是由org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory
类来处理的,像这样的断言工厂在SpringCloudGateway还有十几个。
这里只说明Path 路由断言工厂。
-
(1)规则
该断言工厂用于判断请求路径中是否包含指定的uri 。若包含,则匹配成功,断言为 true。此时会将该匹配上的 uri 拼接到要转向的目标 uri 的后面,形成一个统一的 uri 。 -
(2)配置式配置文件
-
(3)API 式启动类
直接修改路由方法。添加了两个路由策略。
8.3 过滤器工厂
GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理:
Spring提供了31种不同的路由过滤器工厂。例如:
名称 | 说明 |
---|---|
AddRequestHeader | 给当前请求添加一个请求头 |
RemoveRequestHeader | 移除请求中的一个请求头 |
AddResponseHeader | 给响应结果中添加一个响应头 |
RemoveResponseHeader | 从响应结果中移除有一个响应头 |
RequestRateLimiter | 限制请求的流量 |
8.3.1 AddRequestHeader 过滤工厂
需求:给所有进入userservice的请求添加一个请求头:Truth=itcast is freaking awesome!
只需要修改gateway服务的application.yml文件,添加路由过滤即可:
spring:cloud:gateway:routes:- id: user-service uri: lb://userservice predicates: - Path=/user/** filters: # 过滤器- AddRequestHeader=Truth, Itcast is freaking awesome! # 添加请求头
当前过滤器写在userservice路由下,因此仅仅对访问userservice的请求有效。
- (1)规则
该过滤器工厂会对匹配上的请求添加指定的header 。 - (2)配置式配置文件
向请求添加了一个请求头信息 X-Request Color=blue 。
- 3、修改 showinfo 处理器
- 4、API 式启动类
8.3.2 默认过滤器
如果要对所有的路由都生效,则可以将过滤器工厂写到default下。格式如下:
spring:cloud:gateway:routes:- id: user-service uri: lb://userservice predicates: - Path=/user/**default-filters: # 默认过滤项- AddRequestHeader=Truth, Itcast is freaking awesome!
8.3.3 优先级
对于相同filter 工厂,在不同位置设置了不同的值,则优先级为:
- 局部 filter 的优先级高于默认 filter 的
- API 式的 filter 优先级高于配置式 filter 的
8.3.4.总结
过滤器的作用是什么?
-
① 对路由的请求或响应做加工处理,比如添加请求头
-
② 配置在路由下的过滤器只对当前路由的请求生效
defaultFilters的作用是什么?
- ① 对所有路由都生效的过滤器
8.4 自定义路由断言工厂
8.4.1 Auth 认证
-
(1)需求
当请求头中携带有用户名与密码的key value 对,且其用户名与配置文件中 Auth 路由断言工厂中指定的 username 相同,密码中 包含 Auth 路由断言工厂中指定的 password 时才能通过认证,允许访问系统。 -
(2)修改配置文件
-
(3)定义factory
该类类名由两部分构成:后面必须是RoutePredicateFactory ,前面为功能前缀,该前缀将来要用在配置文件中 。(eg:AuthRoutePredicateFactory)
package com.abc.factory;import lombok.Data;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;@Component
public class AuthRoutePredicateFactory extends AbstractRoutePredicateFactory<AuthRoutePredicateFactory.Config> {public AuthRoutePredicateFactory() {super(Config.class);}@Overridepublic Predicate<ServerWebExchange> apply(Config config) {return exchange -> {// 获取到请求中的所有headerHttpHeaders headers = exchange.getRequest().getHeaders();// 一个请求头可以包含多个值List<String> pwds = headers.get(config.getUsername());// 只要请求头中指定的多个密码值中包含了配置文件中指定的密码,就可以通过String[] values = pwds.get(0).split(",");for (String value : values) {if (value.equalsIgnoreCase(config.getPassword())) {return true;}}return false;};}@Datapublic static class Config {private String username;private String password;}@Overridepublic List<String> shortcutFieldOrder() {return Arrays.asList("username", "password");}
}
- (4)测试
8.4.2 Token 认证
-
(1)需求
当请求中携带有一个token 请求参数,且参数值包含配置文件中 Token 路由断言工厂指定的token 值时才能通过认证,允许访问系统。 -
(2)配置文件
-
(3)定义 Factory
package com.abc.factory;import lombok.Data;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;import java.net.InetSocketAddress;
import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;@Component
public class TokenRoutePredicateFactory extends AbstractRoutePredicateFactory<TokenRoutePredicateFactory.Config> {public TokenRoutePredicateFactory() {super(Config.class);}@Overridepublic Predicate<ServerWebExchange> apply(Config config) {return exchange -> {InetSocketAddress remoteAddress = exchange.getRequest().getRemoteAddress();InetSocketAddress localAddress = exchange.getRequest().getLocalAddress();System.out.println(remoteAddress);System.out.println(localAddress);// 获取请求中的所有请求参数MultiValueMap<String, String> params = exchange.getRequest().getQueryParams();List<String> values = params.get("token");if (values.contains(config.getToken())) {return true;}return false;};}@Datapublic static class Config {private String token;}@Overridepublic List<String> shortcutFieldOrder() {return Collections.singletonList("token");}
}
- (4)测试
8.5 自定义 网关过滤工厂
8.5.1 添加请求头
这里要实现的需求是,在自定义的Filter 中为请求添加指定的请求头。
-
(1)配置文件
-
(2)自定义 Factory
该类类名由两部分构成:后面必须是GatewayFilterFactory ,前面为功能前缀,该前缀将来要用在配置文件中。(eg:AddHeaderGatewayFilterFactory 类)
package com.abc.factory;import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractNameValueGatewayFilterFactory;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;import java.util.Map;@Component
public class AddHeaderGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {@Overridepublic GatewayFilter apply(NameValueConfig config) {return (exchange, chain) -> {Map<String, String> uriVariables = ServerWebExchangeUtils.getUriTemplateVariables(exchange);String bean = uriVariables.get("bean");String id = uriVariables.get("id");System.out.println("bean-id " + bean + "-" + id);ServerHttpRequest changedRequest = exchange.getRequest().mutate().header(config.getName(), config.getValue()).build();ServerWebExchange changedExchange = exchange.mutate().request(changedRequest).build();return chain.filter(changedExchange);};}
}
- (3)测试
8.5.2 GatewayFilter 的 pre 与 post
每个 Filter 都具有 pre 与 post 两部分。
Spring Cloud Gateway同 zuul 类似,有“ pre ”和 post ”两种方式的 filter 。客户端的请求先按照 filter 的优先级顺序(优先级相同,则按注册顺序),执行 filter 的“ pre ”部分。然后将请求转发到相应的目标服务器,收到目标服务器的响应之后,再按照 filter 的优先级顺序的逆序(优先级相同,则按注册顺序逆序),执行 filter 的“ post ”部分 最后返回响应到客户端。
filter的“ pre ”部分指的是 chain.filter() 方法执行之前的代码,而“ post ”部分,则是定义在 chain.filter().then() 方法中的代码。
(1)配置文件
(2)定义三个FilterFactory
package com.abc.factory;import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractNameValueGatewayFilterFactory;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;@Component
@Slf4j
public class OneGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {@Overridepublic GatewayFilter apply(NameValueConfig config) {return (exchange, chain) -> {// pre-filterlong start = System.currentTimeMillis();log.info(config.getName() + "-" + config.getValue() + "-pre,开始执行的时间:" + start);exchange.getAttributes().put("startTime", start);return chain.filter(exchange).then(// post-filterMono.fromRunnable(() -> {long startTime = (long) exchange.getAttributes().get("startTime");long endTime = System.currentTimeMillis();long elapsedTime = endTime - startTime;log.info(config.getName() + "-" + config.getValue() + "-post,执行完毕的时间:" + endTime);log.info("该filter执行用时(毫秒):" + elapsedTime);}));};}
}
package com.abc.factory;import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractNameValueGatewayFilterFactory;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;@Component
@Slf4j
public class TwoGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {@Overridepublic GatewayFilter apply(NameValueConfig config) {return (exchange, chain) -> {// pre-filterlog.info(config.getName() + "-" + config.getValue() + "-pre");return chain.filter(exchange).then(// post-filterMono.fromRunnable(() -> {log.info(config.getName() + "-" + config.getValue() + "-post");}));};}
}
package com.abc.factory;import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractNameValueGatewayFilterFactory;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;@Component
@Slf4j
public class ThreeGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {@Overridepublic GatewayFilter apply(NameValueConfig config) {return (exchange, chain) -> {// pre-filterlog.info(config.getName() + "-" + config.getValue() + "-pre");return chain.filter(exchange).then(// post-filterMono.fromRunnable(() -> {log.info(config.getName() + "-" + config.getValue() + "-post");}));};}
}
(3)修改 ShowInfo 工程的处理器
(4)测试
8.6 自定义异常处理器
package com.abc.handler;import org.springframework.boot.autoconfigure.web.WebProperties;
import org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.*;
import reactor.core.publisher.Mono;import java.util.Map;@Component
@Order(-1)
public class CustomErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler {public CustomErrorWebExceptionHandler(ErrorAttributes errorAttributes,ApplicationContext applicationContext,ServerCodecConfigurer serverCodecConfigurer) {super(errorAttributes, new WebProperties.Resources(), applicationContext);super.setMessageWriters(serverCodecConfigurer.getWriters());super.setMessageReaders(serverCodecConfigurer.getReaders());}@Overrideprotected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);}private Mono<ServerResponse> renderErrorResponse(ServerRequest request) {// 获取异常信息Map<String, Object> map = getErrorAttributes(request, ErrorAttributeOptions.defaults());// 构建响应return ServerResponse.status(HttpStatus.NOT_FOUND) // 404状态码.contentType(MediaType.APPLICATION_JSON) // 以JSON格式显示响应.body(BodyInserters.fromValue(map)); // 响应体(响应内容)}
}
测试:
8.7 全局过滤器
全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用一样。区别在于GatewayFilter通过配置定义,处理逻辑是固定的;而GlobalFilter的逻辑需要自己写代码实现。
定义方式是实现GlobalFilter接口。
public interface GlobalFilter {/*** 处理当前请求,有必要的话通过{@link GatewayFilterChain}将请求交给下一个过滤器处理** @param exchange 请求上下文,里面可以获取Request、Response等信息* @param chain 用来把请求委托给下一个过滤器 * @return {@code Mono<Void>} 返回标示当前过滤器业务结束*/Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}
在filter中编写自定义逻辑,可以实现下列功能:
- 登录状态判断
- 权限校验
- 请求限流等
8.7.1 AuthorizeFilter
需求:定义全局过滤器,拦截请求,判断请求的参数是否满足下面条件:
-
参数中是否有authorization,
-
authorization参数值是否为admin
如果同时满足则放行,否则拦截
.
实现:
在gateway中定义一个过滤器:
package cn.itcast.gateway.filters;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.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;@Order(-1)
@Component
public class AuthorizeFilter implements GlobalFilter {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 1.获取请求参数MultiValueMap<String, String> params = exchange.getRequest().getQueryParams();// 2.获取authorization参数String auth = params.getFirst("authorization");// 3.校验if ("admin".equals(auth)) {// 放行return chain.filter(exchange);}// 4.拦截// 4.1.禁止访问,设置状态码exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);// 4.2.结束处理return exchange.getResponse().setComplete();}
}
8.7.2 URLValidateGlobalFilter
这里要实现的需求是,访问当前系统的任意模块的URL 都需要是合法的 URL 。这里所谓
合法的 URL 指的是请求中携带了 token 请求参数。由于是对所有请求的URL 都要进行验证,所以这里就需要定义一个 Global Filter ,可以应用到所有路由中。
package com.abc.filter;import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;// @Component
public class UrlValidateGlobalFilter implements GlobalFilter, Ordered {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 从请求中获取请求参数token的值String token = exchange.getRequest().getQueryParams().getFirst("token");// 若token为空,则响应客户端状态码401,未授权。否则通过验证if (!StringUtils.hasText(token)) {exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);return exchange.getResponse().setComplete();}return chain.filter(exchange);}@Overridepublic int getOrder() {// 为当前GlobalFilter赋予最高优先级return Ordered.HIGHEST_PRECEDENCE;}
}
8.8 跨域配置
8.8.1 跨域与同源
为了安全,浏览器中启用了一种称为同源策略的安全机制,禁止从一个域名页面中请求另一个域名下的资源。
当两个请求的访问协议、域名与端口号三者都相同时,才称它们是同源的。只要有一个不同,就称为跨源请求。
源: http://sports.abc.com/content/kb
跨源: https : //sports.abc.com/content/kb
跨源: http:// news .abc.com/content/kb
跨源: http://sports.abc.com: 8080 /content/kb
同源: http://sports.abc.com/content/abc
.
8.8.2 CORS
CORS(Cross Origin Resource Sharing ,跨域资源共享 是一种允许当前域的资源 例如,html 、 js 、 web service) 被其他域的脚本请求访问的机制 。
8.8.3 Gateway 解决跨域问题
在gateway服务的application.yml文件中,添加下面的配置:
spring:cloud:gateway:# 。。。globalcors: # 全局的跨域处理add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题corsConfigurations:'[/**]':allowedOrigins: # 允许哪些网站的跨域请求 - "http://localhost:8090"allowedMethods: # 允许的跨域ajax的请求方式- "GET"- "POST"- "DELETE"- "PUT"- "OPTIONS"allowedHeaders: "*" # 允许在请求中携带的头信息allowCredentials: true # 是否允许携带cookiemaxAge: 360000 # 这次跨域检测的有效期
全局解决方案:
在其中添加全局 cors 配置。该解决方案对当前配置文件中的所有路由均起作用。
局部解决方案:
在某个具体的路由中添加局部 cors 配置。该解决方案仅对当前路由起作用。