springCloud/Alibaba常用中间件之GateWay网关
文章目录
- SpringCloud:
- 依赖版本补充
- GateWay:网关
- 三大核心之Router:路由
- 1、导入基础依赖
- 2、进行服务注册
- 3、路由映射
- 4、测试访问GateWay的端口是否可以访问
- 三大核心之Predicate:断言
- 配置文件
- 自定义Predicate(断言)
- 三大核心之Filter:过滤
- 配置文件
- 自定义全局过滤 —— 输出每一个路由所执行的时间
- 自定义路由过滤器 —— 输出指定的路由所执行的时间、设置路由必填参数
- 源码分析:自定义过滤器 —— apply()
- 过滤链列表的构建:
- 过滤器链的执行流程
SpringCloud:
微服务的中间件介绍与使用
微服务架构体系图:
依赖版本补充
下面所有代码中的依赖版本如下:
<properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><hutool.version>5.8.22</hutool.version><lombok.version>1.18.30</lombok.version><druid.version>1.2.18</druid.version><mybatis.springboot.version>3.0.3</mybatis.springboot.version><mysql.version>8.0.33</mysql.version><fastjson2.version>2.0.48</fastjson2.version><swagger3.version>2.2.0</swagger3.version><mapper.version>4.2.3</mapper.version><persistence-api.version>1.0.2</persistence-api.version><spring.boot.test.version>3.1.5</spring.boot.test.version><spring.boot.version>3.2.0</spring.boot.version><spring.cloud.version>2023.0.0</spring.cloud.version><spring.cloud.alibaba.version>2023.0.0.0-RC1</spring.cloud.alibaba.version><knife4j-openapi3.version>4.4.0</knife4j-openapi3.version> </properties>
GateWay:网关
什么是网关: 网关 简单的理解就是在请求和服务之间加了一个 中间层
作用是:
一、管控 路由的请求(反向代理、鉴权、流量控制、熔断、日志监控)
二、在每次路由中进行 增删内容。
图形化展示网关位置:
工作原理
简单总结就是:路由转发+断言判断+执行过滤链列表
参考官网:https://docs.spring.io/spring-cloud-gateway/reference/spring-cloud-gateway/how-it-works.html
三大核心之Router:路由
概念:路由是构建网管的基本模块,它有ID、目标URI、一系列断言和过滤器组成,如果断言为true则匹配该条路由
接下来直接开始配置一个基本的路由
1、导入基础依赖
<!--gateway依赖-->
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--服务注册发现consul discovery,网关也要注册进服务注册中心统一管控-->
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<!-- 指标监控健康检查的actuator,网关是响应式编程删除掉spring-boot-starter-web dependency-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
2、进行服务注册
server:port: 9567
spring:application:name: cloud-gatewaycloud:consul:port: 8500 #consul的端口号host: localhost #consul的网址discovery:service-name: ${spring.application.name} #配置服务名
启动类添加注册注解
@SpringBootApplication
@EnableDiscoveryClient/*服务组注册*/
public class Main9567 {public static void main(String[] args) {SpringApplication.run(Main9567.class, args);}
}
启动查看是否完成了服务注册
3、路由映射
这里可以先创建一个简单的业务[可以不需要数据库就返回一个值即可]做映射地址(别忘了要进行服务注册)
这里为了方便我就修改一下上面的业务的进行测试了。配置好测试业务之后,在路由服务的配置文件中进行配置:
server:port: 9567spring:application:name: cloud-gatewaycloud:consul:port: 8500 #consul的端口号host: localhost #consul的网址discovery:service-name: ${spring.application.name} #配置服务名gateway:routes:- id: gateway_router_1 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名#配置地址方法一(不推荐):# uri: localhost:8001 #填写业务服务的地址和端口号(缺点:灵活性差)#方法二(推荐):动态路由uri: lb://cloud-payment-service #填写业务服务名(优点:可动态的配置路由,lb:可以支持负载均衡)predicates: # 配置一个简单的断言,以便于演示。也就是访问此路由的条件- Path=/pay/gateway/get/** #满足此路由的才可以访问,**:通配符- id: gateway_router_2uri: lb://cloud-payment-servicepredicates:- Path=/pay/gateway/info/**
4、测试访问GateWay的端口是否可以访问
访问成功,说明映射成功了!
这里可以也试一下当有多个服务(yourServerName),下使用lb://yourServerName
是否有负载均衡的效果
三大核心之Predicate:断言
概念:开发人员可以匹配HTTP请求中的所有内容,如果请求与断言相匹配则进行路由
配置文件
gateway:routes:- id: gateway_router_1 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名#配置地址方法一(不推荐):# uri: localhost:8001 #填写业务服务的地址和端口号(缺点:灵活性差)#方法二(推荐):动态路由uri: lb://cloud-payment-service #填写业务服务名(优点:可动态的配置路由,lb:可以支持负载均衡)predicates: # 配置一个简单的断言,以便于演示。也就是访问此路由的条件- Path=/pay/gateway/get/** #满足此路由的才可以访问,**:通配符- id: gateway_router_2uri: lb://cloud-payment-servicepredicates:- Path=/pay/gateway/get/** # 断言,路径相匹配的进行路由- After=2025-04-20T16:04:22.672173200+08:00[Asia/Shanghai] #配置指定的时间之后才开启此路由- Before=2025-04-20T16:08:13.979939200+08:00[Asia/Shanghai] #与after相反,没有达到此时间之后都可以访问,过了之后便会直接关闭- Between=2025-04-20T16:09:38.676463500+08:00[Asia/Shanghai],2025-04-20T16:11:38.676463500+08:00[Asia/Shanghai] #区间- Cookie=phone,^1(3[0-9]|4[57]|5[0-35-9]|7[0678]|8[0-9])\d{8}$ #根据cookie参数进行验证,第一个是key值,第二个是规定value值正则- Header=X-Request-Id, \d+ #根据请求头的参数名和值进行判断,第一个值是name,第二个值是正则表达式- Host=**.chyb.com,**.chyb.org #根据Host这个参数进行模糊查询,**:通配符- Query=num, \d+ #必须要有num这个参数,且值为整数- RemoteAddr=192.168.235.0/24 #根据请求人的ip进行拦截,/24:是指前面的24位是不可以变的,24:前面的192.168.43是不可以变得,注意:这里请求的时候不要在使用localhost而是要输入自己的IP地址- Method=GET,POST #按照请求方式进行匹配
这里就相当于API的调用一样没有什么好说的,不清楚的可以直接参考官方文档:https://docs.spring.io/spring-cloud-gateway/reference/spring-cloud-gateway/request-predicates-factories.html
中文官网:https://springdoc.cn/spring-cloud-gateway/#gateway-request-predicates-factories
这里在单独将一个知识点领出来说:快捷方式的配置(常用) 、 完全展开的参数 语法如下:
# 快捷方式的配置
- Cookie=mycookie,mycookievalue
#完全展开的参数:完全展开的参数看起来更像标准的yaml配置,有名称/值对。一般来说,会有一个 name key和一个 args key。args key是一个键值对的映射,用于配置谓词或过滤器。
- name: Cookieargs:name: mycookieregexp: mycookievalue
自定义Predicate(断言)
创建 ~RoutePredicateFactory 类,其代码如下:
@Component /*注入Spring容器中*/
/*注意命名规范:~~~RoutePredicateFactory*/
public class MyRoutePredicateFactory extends AbstractRoutePredicateFactory<MyRoutePredicateFactory.Config> {public MyRoutePredicateFactory() {super(MyRoutePredicateFactory.Config.class);}@Validatedpublic static class Config {@Setter@Getter@NotEmptyprivate String userType; //钻、金、银等用户等级}/*** 写逻辑:根据userType进行判断是否可以访问此路由* @param config* @return*/@Overridepublic Predicate<ServerWebExchange> apply(MyRoutePredicateFactory.Config config) {return new Predicate<ServerWebExchange>() {@Overridepublic boolean test(ServerWebExchange serverWebExchange) {//检查request的参数里面,userType是否为指定的值,符合配置就通过String userType = serverWebExchange.getRequest().getQueryParams().getFirst("userType");if (userType == null) return false;//如果说参数存在,就和config的数据进行比较if (userType.equals(config.getUserType())) {return true;}return false;}};}/*** 用来支持断言的快捷方式* @return*/@Overridepublic List<String> shortcutFieldOrder() {return Collections.singletonList("userType");}
}
配置文件的设置
#....
predicates:#这里的"My"是根据你类名有关系,当遵守命名规范:~RoutePredicateFactory,则默认的名字就是:~- My=diamond #当参数userType的值等于diamond才可以访问此路由
三大核心之Filter:过滤
概念:指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求路由之前或之后对请求进行修改
过滤器类型分为:
- 全局默认过滤器 (Global Filters):每一个路由都要执行的过滤器
- 单一内置过滤器 (GateWayFilter),“又称路由过滤器”:指定某一个路由执行的过滤器
配置文件
filters:# RequestHeader的增删改- AddRequestHeader=chyb-name, chybName1 #添加一个请求头(chyb-name)值为:chybName1- AddRequestHeader=chyb-name2, chybName2- RemoveRequestHeader=chyb-name #删除一个请求头(user-agent)- SetRequestHeader=chyb-name2, chybName3 #修改某一个请求头(chyb-name2)的值为chybName3,若是没哟此请求头,则直接会自动添加# RequestParameter请求参数的增删- AddRequestParameter=param1, 2983 #增加参数- RemoveRequestParameter=param2 #删除参数# ResponseHeader 响应头的增删改- AddResponseHeader=chybName, chyb1 #创建一个响应头(chybName)值为:chyb1- SetResponseHeader=date, 2006-02-09 #设置date响应头的值为:2006-02-09- RemoveResponseHeader=Content-Type #删除Content-Type(接受类型)# 前缀、路径、相关的配置- PrefixPath=/pay #设置访问路径的前缀- SetPath=/pay/gateway/{param} #设置访问路径,{~}为占位符- RedirectTo=301, https://baidu.com #重定向到https://baidu.com,状态吗为302
参考官网:https://docs.spring.io/spring-cloud-gateway/reference/spring-cloud-gateway/gatewayfilter-factories.html
中文官网:https://springdoc.cn/spring-cloud-gateway/#gatewayfilter-%E5%B7%A5%E5%8E%82
自定义全局过滤 —— 输出每一个路由所执行的时间
1、创建一个~~GlobalFilter,并实现自定义过滤器所需的接口:GlobalFilter, Ordered
/*注入到Spring容器中,之后会将所有注入到容器中的过滤器进行合并,形成过滤器列表*/
@Component
@Slf4j
/*这里是自定配置全局过滤器,就是每一个路由都会去执行的*/
public class MyGlobalFilter implements GlobalFilter, Ordered {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {return null;}/*** 设置执行接口进行排序,返回值越小越靠前* @return*/@Overridepublic int getOrder() {return 0;}
}
2、在filter方法下写执行逻辑,实现:输出每一个路由所执行的时间
public static final String BEGIN_VISIT_TIME = "BEGIN_VISIT_TIME";/*初始化方法执行的开始时间*//***用来接受响应请求报文,以及控制放行操作(这里有点像将拦截器中的preHandle[访问前拦截方法]和postHandle[访问后拦截方法]结合到一起了)* @param exchange :类似于Server,它里面封装了三个属性:* ServerHttpResponse、ServerHttpRequest:这两个属性存储着:请求/响应的报文,所以可以通过exchange这个参数获取到这些属性* Map<String, Object>:这属性就是我们要进行额外的添加对象就可以添加到这里,运用的方法就直接.pus即可* @param chain : 这个属性可以控制过滤器的放行操作* @return 这里返回的类型是Mono这个类型是在SpringBoot响应式编程中所引出的类,适用于高并发和高吞吐量场景(在资源有限的情况下提高系统的吞吐量和伸缩性【并非提高性能】)*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {System.out.println("=====" + exchange.getAttributes().get(BEGIN_VISIT_TIME) + "=====");exchange.getAttributes().put(BEGIN_VISIT_TIME, System.currentTimeMillis());System.out.println("------" + exchange.getAttributes().get(BEGIN_VISIT_TIME) + "-----");/*** 这里的chain.filter(exchange)是放行操作,而.then()是任务串联(在 Mono完成后触发新任务,但不依赖前一个任务的输出结果。)*/return chain.filter(exchange).then(Mono.fromRunnable(() -> {Long beginTime = exchange.getAttribute(BEGIN_VISIT_TIME);URI uri = exchange.getRequest().getURI();if (beginTime != null) {log.info("访问接口主机:{}", uri.getHost());log.info("访问接口端口号:{}", uri.getPort());log.info("放文件接口URL:{}", uri.getPath());log.info("访问接口的URL参数:{}", uri.getQuery());log.info("访问时长:{}ms", (System.currentTimeMillis() - beginTime));log.info("============================================");System.out.println();}}));
}
这里因为是全局过滤器,所以并不需要进行单独配置yml。
3、测试(访问任意路由查看日志是否输出)
自定义路由过滤器 —— 输出指定的路由所执行的时间、设置路由必填参数
1、创建自定义路由过滤器类,注意类名~~GatewayFilterFactory
案例一:日志输出指定的路由所执行的时间
/*注入到Spring容器中,之后会将所有注入到容器中的过滤器进行合并,形成过滤器列表*/
@Component
@Slf4j
/*自定义路由过滤器,指定某一个路由执行的过滤器,无参数的写法(只需要编写apply方法即可,注意config的类型)*/
/*注意命名规范:~GatewayFilterFactory*/
public class MyGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {static final String BEGIN_TIME = "begin_time";@Overridepublic GatewayFilter apply(Object config) {System.out.println("进入了自定义网关过滤器MyGatewayFilterFactory,status:" + config);return new GatewayFilter() {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {exchange.getAttributes().put(BEGIN_TIME, System.currentTimeMillis());return chain.filter(exchange).then(Mono.fromRunnable(() -> {Long beginTime = exchange.getAttribute(BEGIN_TIME);URI uri = exchange.getRequest().getURI();if (beginTime != null) {log.info("------------------我是路由过滤器----------------------");log.info("访问接口主机:{}", uri.getHost());log.info("访问接口端口号:{}", uri.getPort());log.info("放文件接口URL:{}", uri.getPath());log.info("访问接口的URL参数:{}", uri.getQuery());log.info("访问时长:{}ms", (System.currentTimeMillis() - beginTime));log.info("============================================");System.out.println();}}));}};}
}
案例二:设置路由必填参数
@Component
/*定义路由有参过滤器*/
public class MustQueryGatewayFilterFactory extends AbstractGatewayFilterFactory<MustQueryGatewayFilterFactory.Config> {/*初始化*/public MustQueryGatewayFilterFactory() {super(MustQueryGatewayFilterFactory.Config.class);}/*用来配置快捷访问方式的传参的位置*/@Overridepublic List<String> shortcutFieldOrder() {return Arrays.asList("status");}/*定义一个内部类用来存放接受的参数,同时也是继承AbstractGatewayFilterFactory类的泛型*/public static class Config {@Getter@Setterprivate String status;//设定一个状态值/标志位,它等于多少,匹配和才可以访问}/*** 写过滤器逻辑的方法* @param config 传递参数值* @return*/@Overridepublic GatewayFilter apply(MustQueryGatewayFilterFactory.Config config) {return new GatewayFilter() {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {ServerHttpRequest request = exchange.getRequest();/*动态的将配置的参数设为传必传参数的name*/System.out.println("进入了自定义网关过滤器MyGatewayFilterFactory,status:" + config.getStatus());if (request.getQueryParams().containsKey(config.getStatus())) {return chain.filter(exchange);} else {exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);return exchange.getResponse().setComplete();}}};}
}
//单一内置过滤器GatewayFilter
2、配置yml
在指定的路由上添加路由过滤器
server:port: 9567
spring:application:name: cloud-gatewaycloud:consul:port: 8500 #consul的端口号host: localhost #consul的网址discovery:service-name: ${spring.application.name} #配置服务名gateway:routes:- id: gateway_router_1uri: lb://cloud-payment-servicepredicates:- Path=/pay/gateway/info/** # 断言,路径相匹配的进行路由filters:- My #配置日志输出指定的路由所执行的时间的路由过滤器- id: gateway_router_2uri: lb://cloud-payment-servicepredicates:- Path=/pay/gateway/get/**filters:- MustQuery=chyb #配置必传参数名,否则访问不了
测试
案例一:
案例二:
源码分析:自定义过滤器 —— apply()
我们以这段代码为例:
@Override
public GatewayFilter apply(Object config) {System.out.println("进入了自定义网关过滤器MyGatewayFilterFactory,status:" + config);return new GatewayFilter() {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {exchange.getAttributes().put(BEGIN_TIME, System.currentTimeMillis());return chain.filter(exchange).then(Mono.fromRunnable(() -> {
// .....}));}};
}
这里我们把重心放在:
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {}
这里的第一个参数exchange:里面封装了三个参数(上面自定义全局过滤器注释有,所以不多讲getRequest、getResponse、getAttributes
)
第二个参数chain:主要是用来进行放行操作的,而这里是怎么放行的我们就从过滤链列表构建 到 过滤器链的执行流程
过滤链列表的构建:
过滤链列表由来:全局过滤器列表 和 路由过滤器列表 动态合并而成
全局过滤器列表 :
是根据Spring容器扫描所有的 GlobalFilter 的实现类,
通过 @Order 注解或 Ordered 接口的 getOrder()
方法对全局过滤器进行排序,形成全局过滤器列表。从而形成的一个列表。路由过滤器列表:
从配置文件(如 YAML)或代码中解析路由规则,提取每个路由的 filters 列表。
这里还有一个点要注意:配置文件中的过滤器名称(如 AddRequestHeader),会默认转化为GatewayFilter实例
(也就是我们自定义的路由过滤器类),
然后根据所有的GatewayFilter实例
添加到 路由过滤器列表。动态合并而成:
当请求到达时,网关根据当前请求匹配的路由,将 全局过滤器列表 和 路由过滤器列表 合并。
路由过滤器按配置顺序追加到全局过滤器之后,便形成了过滤链。排序规则[全局过滤器按 @Order 排序(值越小优先级越高)]
源码示例(合并):FilteringWebHandler
类中的handle()
public Mono<Void> handle(ServerWebExchange exchange) {// 1. 获取全局过滤器和路由过滤器List<GatewayFilter> globalFilters = this.globalFilters;List<GatewayFilter> routeFilters = route.getFilters();// 2. 合并过滤器列表List<GatewayFilter> combined = new ArrayList<>(globalFilters);combined.addAll(routeFilters);// 3. 排序(全局过滤器按 Order,路由过滤器按配置顺序)AnnotationAwareOrderComparator.sort(combined);// 4. 创建过滤器链return new DefaultGatewayFilterChain(combined).filter(exchange); }
这就是形成过滤器列表的大致过程。
过滤器链的执行流程
- 链式调用模型:
- 责任链模式:通过 DefaultGatewayFilterChain (
这个类是FilteringWebHandler类的内部类,要想找的话可以直接搜索这个类) 递归调用每个过滤器,直到所有过滤器执行完毕。- 响应式编程:使用 Mono 和 .then() 实现异步非阻塞执行。(这里不懂的话可以看一下SpringBoot的响应式编程)
- 核心代码展示:
这里可以找到chain.filter(exchange)
【我们自定义过滤器中所返回的那个方法】的实现类FilteringWebHandler
找到内部类 DefaultGatewayFilterChain
核心代码如下:public class DefaultGatewayFilterChain implements GatewayFilterChain {private final List<GatewayFilter> filters;private final int index; // 当前执行过滤器的索引@Overridepublic Mono<Void> filter(ServerWebExchange exchange) {return Mono.defer(() -> {if (index < filters.size()) {// 1. 获取当前过滤器GatewayFilter filter = filters.get(index);// 2. 创建下一个链(index+1)GatewayFilterChain nextChain = new DefaultGatewayFilterChain(this, index + 1);// 3. 执行当前过滤器,传递下一个链return filter.filter(exchange, nextChain);} else {// 4. 所有过滤器执行完毕return Mono.empty();}});} }
- 执行顺序示例
假设合并后的过滤器列表为:[GlobalFilterA, GlobalFilterB, RouteFilter1, RouteFilter2]执行流程:
- GlobalFilterA 的前置逻辑 → 调用 chain.filter()
- GlobalFilterB 的前置逻辑 → 调用 chain.filter()
- RouteFilter1 的逻辑 → 调用 chain.filter()
- RouteFilter2 的逻辑 → 调用 chain.filter()
- 路由到目标服务 → 返回响应
- RouteFilter2 的后置逻辑(通过 .then())
- RouteFilter1 的后置逻辑
- GlobalFilterB 的后置逻辑
- GlobalFilterA 的后置逻辑。
这里可以自己打几个断点到DefaultGatewayFilterChain类中一步一步的查看源码查看,会更清晰一些
上述大部分代码以上传到gitee:https://gitee.com/banhuayue/springCloud-Alibaba-code.git
笔记参考来自尚硅谷