Springgateway网关
奈非框架简介
早期(2020年前)奈非提供的微服务组件和框架受到了很多开发者的欢迎
这些框架和SpringCloud Alibaba的对应关系我们要了解
现在还有很多旧项目维护是使用奈非框架完成的微服务架构
Nacos对应Eureka都是注册中心
Dubbo对应Ribbon+feign都是实现微服务远程RPC调用的组件
Sentinel对应Hystrix都是做项目限流熔断降级的组件
SpringGateway对应Zuul都是网关组件
Gateway框架不是阿里写的,是Spring提供的
什么是网关
"网"指网络,"关"指关口或关卡
网关:就是指网络中的关口\关卡
网关就是当前微服务项目的"统一入口"
程序中的网关就是当前微服务项目对外界开放的统一入口
所有外界的请求都需要先经过网关才能访问到我们的程序
提供了统一入口之后,方便对所有请求进行统一的检查和管理
网关的主要功能有
- 将所有请求统一经过网关
- 网关可以对这些请求进行检查
- 网关方便记录所有请求的日志
- 网关可以统一将所有请求路由到正确的模块\服务上
“路由"的近义词就是"分配”
Spring Gateway简介
我们使用Spring Gateway作为当前项目的网关框架
Spring Gateway是Spring自己编写的,也是SpringCloud中的组件
Spring Gateway官网
https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/
网关实例项目git地址
https://gitee.***/jtzhanghl/gateway-demo.git
为什么选择 Gateway
Gateway 的特性
Gateway 与 Zuul 的区别
Gateway 的三大核心概念
Route (路由)
Predicate (断言)
Filter (过滤)
总结
Gateway 的工作流程
简单网关演示
SpringGateway网关是一个依赖,不是一个软件
所以我们要使用它的话,必须先创建一个SpringBoot项目
这个项目也要注册到Nacos注册中心,因为网关项目也是微服务项目的一个组成部分
beijing和shanghai是编写好的两个项目
gateway项目就是网关项目,需要添加相关配置
<dependencies>
<!-- SpringGateway的依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- Nacos依赖 -->
<dependency>
<groupId>***.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 网关负载均衡依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
</dependencies>
我们从yml文件配置开始添加
server:
port: 9000
spring:
application:
name: gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
# route就是路由的意思,下面就是配置路由信息
# 一个网关项目大多会配置很多路由
# 所以这个网关配置是一个List集合类型
routes:
# List类型元素赋值时,每个元素都要以"-"开头,在这个"-"之后,
# 编写的所有内容,都是同一个对象的属性值
# id设置当前路由的名称,也是唯一标识,和其它配置没有对应关系,注意不能和之后的id名称重复即可
- id: gateway-beijing
# uri属性配置的是路由目标服务器的名称,"beijing"指注册到Nacos名称为"beijing"的模块
# lb就是负载均衡LoadBalance的缩写,标识路由支持负载均衡
uri: lb://beijing
# predicate是断言的意思,断言指某些条件满足时,执行某些操作
# predicates配置也是一个List类型的属性,所以它赋值也要以"-"开头
predicates:
# 下面是断言的内容,Path表示判断路径,"/bj/**"表示判断路径是否以"/bj/"开头
# 当断言条件满足时,就会按上面uri的配置,路由到该服务器模块
# ↓ P是大写的!!!!!
- Path=/bj/**
先启动nacos
再启动beijing
最后启动gateway
网关多路由配置
上面只配置了一个beijing的路由设置
下面我们修改yml文件也实现shanghai的路由设置
gateway:
# route就是路由的意思,下面就是配置路由信息
# 一个网关项目大多会配置很多路由
# 所以这个网关配置是一个List集合类型
routes:
- id: gateway-shanghai
uri: lb://shanghai
predicates:
- Path=/sh/**
# beijing配置略
在保证nacos启动的情况下
beijing服务器如果启动无需重启
启动shanghai项目
最后重启网关
测试网关路由到两个模块的效果
http://localhost:9000/bj/show可以访问beijing服务器的资源
http://localhost:9000/sh/show可以访问shanghai服务器的资源
以此类推,再有很多服务器时,我们都可以仅使用9000端口号来将请求路由到正确的服务器
就实现了gateway成为项目的统一入口的效果
Gateway配置路由的两种方式
- 在配置文件yml中配置
- 代码中注入RouteLocator的Bean
以第二种为例, 业务需求 - 通过端口为9527网关访问到百度网址
百度国内新闻网址,需要外网 - http://baidu.***
编码: 创建 config.GateWayConfig 类
package ***.atguigu.springcloud.config;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class GateWayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder) {
RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
routes.route("path_route_atguigu",
r -> r.path("/guonei")
.uri("http://baidu.***")).build();
return routes.build();
}
}
浏览器输入http://localhost:9527/guonei,返回http://baidu.***相同的页面。
动态路由
网关项目的配置会随着微服务模块数量增多而变得复杂,维护的工作量也会越来越大
所以我们希望gateway能够设计一套默认情况下自动路由到每个模块的路由规则
这样的话,不管当前项目有多少个路由目标,都不需要维护yml文件了
这就是我们SpringGateway的动态路由功能
配置文件中开启即可
gateway:
discovery:
locator:
# 开启动态路由功能,默认值是关闭的
# 动态路由规则:在网关端口号后,先编写要路由目标服务器注册到Naocs的名称
# 在编写访问这个服务器的具体路径
# 例如要访问 localhost:9001/bj/show -> localhost:9000/beijing/bj/show
enabled: true
按上面修改完配置之后
我们可以重启gateway来测试动态路由路径是否生效
动态路由生成规则为:在网关端口号后先写要路由到的目标服务器在nacos注册的名称,再编写具体路径
内置断言
我们上面章节在网关配置中使用了predicates(断言)的配置
断言的意思就是判断某个条件是否满足
我们之前使用了Path断言,判断请求的路径是不是满足条件,例如是不是/sh/** /bj/**
如果路径满足这个条件,就路由到指定的服务器
但是Path实际上只是SpringGateway提供的多种内置断言中的一种
还有很多其它断言
- After
- Before
- Between
- Cookie
- Header
- Host
- Method
- Path
- Query
- Remoteaddr
时间相关
After,Before,Between
判断当前时间在指定时间之前,之后或之间的操作
如果条件满足可以执行路由操作,否则拒绝访问
表示时间的格式比较特殊,先使用下面代码获得时间
ZonedDateTime.now()
运行程序输出,可获得当前时间,这个时间的格式可能是
2023-04-21T11:13:54.967+08:00[Asia/Shanghai]
下面在yml配置中添加新的断言配置
使用After设置必须在指定时间之后访问
routes:
- id: gateway-shanghai
uri: lb://shanghai
predicates:
- Path=/sh/**
# After是时间断言,判断当前请求访问时的时间是否晚于配置的时间
# 如果成立正常访问,如果判断不成立,返回404错误,多个断言之间是"与"的关系
- After=2023-04-21T11:21:30.967+08:00[Asia/Shanghai]
使用Before设置必须在指定时间之前才能访问服务
否则发生404错误拒绝访问
predicates:
- Path=/sh/**
- Before=2023-04-21T11:23:20.967+08:00[Asia/Shanghai]
使用Between设置必须在指定时间之间访问
predicates:
- Path=/sh/**
- Between=2023-04-21T11:25:25.967+08:00[Asia/Shanghai],2023-04-21T11:25:45.967+08:00[Asia/Shanghai]
要求指定参数的请求
Query断言,判断是否包含指定的参数名称,包含参数名称才能通过路由
predicates:
- Path=/sh/**
# Query断言判断请求中是否包含指定参数名称,这里设置为username,如果没有发生404错误
- Query=username
重启gateway测试
必须是包含username参数的请求才能访问到指定的页面
例如:http://localhost:9000/sh/show?username=tom
带 Cookie 参数的断言
spring:
cloud:
gateway:
routes:
- id: cookie_route
uri: https://example.org
predicates:
- Cookie=username, yyx
不带 Cookie 访问
携带 Cookie 访问
带 Header 参数的断言
spring:
cloud:
gateway:
routes:
- id: header_route
uri: https://example.org
predicates:
- Header=X-Request-Id, \d+ # 请求头要有 X-Request-Id 属性并且值为整数的正则表达式
Host 断言
Method 断言
内置过滤器
Gateway还提供的内置过滤器
不要和我们学习的filter混淆
内置过滤器允许我们在路由请求到目标资源的同时,对这个请求进行一些加工或处理
常见过滤器也有一些
AddRequestParameter过滤器
我们给大家演示一下AddRequestParameter过滤器
它的作用是在请求中添加参数和它对应的值
routes:
- id: gateway-shanghai
uri: lb://shanghai
filters:
# 如果路由顺利成功,这个内置过滤器会自动在请求中添加下面的参数
# 属性名称为age,默认值18,如果请求中包含age的值,这个值就不会生效了
- AddRequestParameter=age,18
predicates:
- Path=/sh/**
# Query断言判断请求中是否包含指定参数名称,这里设置为username,如果没有发生404错误
- Query=username
在shanghai的控制器方法中添加代码接收username,age的值
@GetMapping("/show")
public String show(String username,Integer age){
// 2023-04-21T11:13:54.967+08:00[Asia/Shanghai]
System.out.println(ZonedDateTime.now());
return "这里是上海!username:"+username+",age:"+age;
}
重启shanghai和gateway进行测试
http://localhost:9000/sh/show?username=jerry
因为过滤器的存在,控制器可以获取网关过滤器添加的参数值
自定义过滤器
创建类 filter.MyLogGateWayFilter
package ***.atguigu.springcloud.filter;
import lombok.extern.slf4j.Slf4j;
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.stereotype.***ponent;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Date;
@***ponent
@Slf4j
public class MyLogGateWayFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("*********** ***e in MyLogGateWayFilter: " + new Date());
String uname = exchange.getRequest().getQueryParams().getFirst("uname");
if(uname == null)
{
log.info("******* 用户名为null,非法用户,o(╥﹏╥)o");
exchange.getResponse().setStatusCode(HttpStatus.NOT_A***EPTABLE);
return exchange.getResponse().set***plete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0; // 数字越小,过滤器优先级越高
}
}
启动项目,正常访问携带参数,不携带报错
正常访问如:http://localhost:9527/payment/lb?uname=123
错误访问如:http://localhost:9527/payment/lb
路由配置的设计规则
路由规则解释
路由规则一定是在开发之前就设计好的
一般可以使用约定好的路径开头来实现的
例如
gateway项目
如果路径以 /bj开头,就是要访问beijing项目
如果路径以 /sh开头.就是养访问shanghai项目
csmall项目
如果路径是 /base/business开头的, 就去找nacos-business服务器
如果路径是 /base/cart开头的, 就去找nacos-cart服务器
如果路径是 /base/order开头的, 就去找nacos-order服务器
如果路径是 /base/stock开头的, 就去找nacos-stock服务器
csmall项目网关
创建网关项目,然后父子相认
修改子项目pom文件和依赖
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>***.tedu</groupId>
<artifactId>csmall</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>***.tedu</groupId>
<artifactId>gateway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gateway</name>
<description>Demo project for Spring Boot</description>
<dependencies>
<!-- web实例 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Nacos注册依赖 -->
<dependency>
<groupId>***.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>***.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>
</dependencies>
</project>
也删除test测试文件夹
application.properties换为yml
配置如下
server:
port: 19000
spring:
application:
name: gateway-server
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
discovery:
locator:
# 开启动态路由
enabled: true
main:
# 防止SpringMVC和SpringGateway依赖冲突的配置
web-application-type: reactive
网关项目的knife4j配置
我们希望配置网关之后,在使用knife4j测试时
就不来回切换端口号了
我们需要在网关项目中配置Knife4j才能实现
而这个配置是固定的,
只要是网关项目配置各个子模块的knife4j功能,就直接复制这几个类即可
csmall-finish中直接复制config\controller\filter
***.tedu.gateway.config
SwaggerProvider
@***ponent
public class SwaggerProvider implements SwaggerResourcesProvider {
/**
* 接口地址
*/
public static final String API_URI = "/v2/api-docs";
/**
* 路由加载器
*/
@Autowired
private RouteLocator routeLocator;
/**
* 网关应用名称
*/
@Value("${spring.application.name}")
private String applicationName;
@Override
public List<SwaggerResource> get() {
//接口资源列表
List<SwaggerResource> resources = new ArrayList<>();
//服务名称列表
List<String> routeHosts = new ArrayList<>();
// 获取所有可用的应用名称
routeLocator.getRoutes().filter(route -> route.getUri().getHost() != null)
.filter(route -> !applicationName.equals(route.getUri().getHost()))
.subscribe(route -> routeHosts.add(route.getUri().getHost()));
// 去重,多负载服务只添加一次
Set<String> existsServer = new HashSet<>();
routeHosts.forEach(host -> {
// 拼接url
String url = "/" + host + API_URI;
//不存在则添加
if (!existsServer.contains(url)) {
existsServer.add(url);
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setUrl(url);
swaggerResource.setName(host);
resources.add(swaggerResource);
}
});
return resources;
}
}
***.tedu.gateway.controller
SwaggerController类
@RestController
@RequestMapping("/swagger-resources")
public class SwaggerController {
@Autowired(required = false)
private SecurityConfiguration securityConfiguration;
@Autowired(required = false)
private UiConfiguration uiConfiguration;
private final SwaggerResourcesProvider swaggerResources;
@Autowired
public SwaggerController(SwaggerResourcesProvider swaggerResources) {
this.swaggerResources = swaggerResources;
}
@GetMapping("/configuration/security")
public Mono<ResponseEntity<SecurityConfiguration>> securityConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()), HttpStatus.OK));
}
@GetMapping("/configuration/ui")
public Mono<ResponseEntity<UiConfiguration>> uiConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()), HttpStatus.OK));
}
@GetMapping("")
public Mono<ResponseEntity> swaggerResources() {
return Mono.just((new ResponseEntity<>(swaggerResources.get(), HttpStatus.OK)));
}
}
***.tedu.gateway.filter
SwaggerHeaderFilter类
@***ponent
public class SwaggerHeaderFilter extends AbstractGatewayFilterFactory {
private static final String HEADER_NAME = "X-Forwarded-Prefix";
private static final String URI = "/v2/api-docs";
@Override
public GatewayFilter apply(Object config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
if (!StringUtils.endsWithIgnoreCase(path,URI )) {
return chain.filter(exchange);
}
String basePath = path.substring(0, path.lastIndexOf(URI));
ServerHttpRequest newRequest = request.mutate().header(HEADER_NAME, basePath).build();
ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
return chain.filter(newExchange);
};
}
}
gateway项目删除test文件夹
测试网关路由效果,和knife4j效果
启动Nacos\Seata\Sentinel
启动cart\stock\order\business
最后启动gateway
可以通过19000端口测试各个业务模块的功能
http://localhost:19000/nacos-stock/doc.html
http://localhost:19000/nacos-cart/doc.html
http://localhost:19000/nacos-order/doc.html
http://localhost:19000/nacos-business/doc.html
如果不使用网关一切正常,但是启动网关访问失败的话,就是gateway项目配置问题
Gateway和SpringMvc依赖冲突问题和解决
之前网关的演示项目我们添加的网关依赖
<!-- Spring Gateway 网关依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
当前csmall项目需要配置knife4j的路由配置,需要编写一个控制器
所以我们添加了SpringMVC的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
这两个依赖在同一个项目中时,默认情况下启动会报错
SpringMVC框架依赖中自带一个Tomcat服务器
而SpringGateway框架中自带一个***ty的服务器
在启动项目时,两个框架中包含的服务器都想占用相同端口,因为争夺端口号的主动权而发生冲突
导致启动服务时报错
要想能够正常启动必须在yml文件配置
spring:
main:
web-application-type: reactive
reactive:反应的
添加这个配置之后,会Tomcat服务器会变成非阻塞的运行