网关
网络的关口,负责请求的路由、转发、身份验证
Spring Cloud Gateway
基于Spring
的WebFlux
技术,完全支持响应式编程,吞吐能力更强
-
创建独立的微服务,网关微服务
gateway
-
导入相关依赖
<dependencies>
<!--common,这里是常用类-->
<dependency>
<groupId>com.heima</groupId>
<artifactId>hm-common</artifactId>
<version>1.0.0</version>
</dependency>
<!--网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos discovery-->
<dependency>
<groupId>com.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>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build> -
配置启动类-
GatewayApplication
package com.xxx.gateway;
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
} -
配置路由-
application.yml
server:
port: 8080
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 192.168.88.130:8848
gateway:
routes:
- id: 微服务名称 # 路由规则id,自定义,唯一
uri: lb://微服务名称 # 路由目标微服务,lb表示负载均衡
predicates: # 路由断言,判断请求是否符合规则,符合则路由到目标
- Path=/路由名/**, /路由名/**
路由属性
网关路由对应的Java
类型是RouteDefinition
常见属性:
id
:路由唯一标示uri
:路由目 标地址predicates
:路由断言,判断请求是否符合当前路由filters
:路由过滤器,对请求或响应做特殊处理
路由断言-predicates
名称 | 说明 | 示例 |
---|---|---|
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 | 权重处理 |
路由过滤器
名称 | 说明 | 示例 |
---|---|---|
addRequestHeader | 给当前请求添加一个请求头 | AddrequestHeader=headerName,headerValue |
RemoveRequestHeader | 移除请求中的一个请求头 | RemoveRequestHeader=headerName |
AddResponseHeader | 给响应结果中添加一个响应头 | AddResponseHeader=headerName,headerValue |
RemoveResponseHeader | 从响应结果中移除一个响应头 | RemoveResponseHeader=headerName |
RewritePath | 请求路径重写 | RewritePath=/red/?(?<segment>.*), /$\{segment} |
StripPrefix | 去除请求路径中的N段前缀 | StripPrefix=1,则路径/a/b 转发时只保留/b |
…… |
网关登录校验
-
客户端请求进入网关后由
HandlerMapping
对请求做判断,找到与当前请求匹配的路由规则(Route
),然后将请求交给WebHandler
去处理。 -
WebHandler
则会加载当前路由下需要执行的过滤器链(Filter chain
),然后按照顺序逐一执行过滤器(后面称为Filter
)。 -
图中
Filter
被虚线分为左右两部分,是因为Filter
内部的逻辑分为pre
和post
两部分,分别会在请求路由到微服务之前和之后被执行。 -
只有所有
Filter
的pre
逻辑都依次顺序执行通过后,请求才会被路由到微服务。 -
微服务返回结果后,再倒序执行
Filter
的post
逻辑。 -
最终把响应结果返回。
网关过滤器
网关过滤器链中的过滤器有两种:
-
GatewayFilter
:路由过滤器,作用范围比较灵活,可以是任意指定的路由Route
. -
GlobalFilter
:全局过滤器,作用范围是所有路由,不可配置。
GatewayFilter
和GlobalFilter
这两种过滤器的方法签名完全一致:
/**
* 处理请求并将其传递给下一个过滤器
* @param exchange 当前请求的上下文,其中包含request、response等各种数据
* @param chain 过滤器链,基于它向下传递请求
* @return 根据返回值标记当前请求是否被完成或拦截,chain.filter(exchange)就放行了。
*/
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
FilteringWebHandler
在处理请求时,会将GlobalFilter
装饰为GatewayFilter
,然后放到同一个过滤器链中,排序以后依次执行。
Gateway
内置的GatewayFilter
过滤器使用起来非常简单,无需编码,只要在yaml
文件中简单配置即可。而且其作用范围也很灵活,配置在哪个Route
下,就作用于哪个Route
.
例如,有一个过滤器叫做AddRequestHeaderGatewayFilterFacotry
,顾明思议,就是添加请求头的过滤器,可以给请求添加一个请求头并传递到下游微服务。
使用的使用只需要在application.yaml
中这样配置:
spring:
cloud:
gateway:
routes:
- id: test_route
uri: lb://test-service
predicates:
-Path=/test/**
filters:
- AddRequestHeader=key, value # 逗号之前是请求头的key,逗号之后是value
如果想要让过滤器作用于所有的路由,则可以这样配置:
spring:
cloud:
gateway:
default-filters: # default-filters下的过滤器可以作用于所有路由
- AddRequestHeader=key, value
routes:
- id: test_route
uri: lb://test-service
predicates:
-Path=/test/**
自定义网关过滤器
GatewayFilter
自定义GatewayFilter
不是直接实现GatewayFilter
,而是实现AbstractGatewayFilterFactory
。最简单的方式是这样的:
@Component
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
@Override
public GatewayFilter apply(Object config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取请求
ServerHttpRequest request = exchange.getRequest();
// 编写过滤器逻辑
System.out.println("过滤器执行了");
// 放行
return chain.filter(exchange);
}
};
}
}
注意:该类的名称一定要以GatewayFilterFactory
为后缀!
然后在yaml
配置中这样使用:
spring:
cloud:
gateway:
default-filters:
- PrintAny # 此处直接以自定义的GatewayFilterFactory类名称前缀类声明过滤器
另外,这种过滤器还可以支持动态配置参数,不过实现起来比较复杂,示例:
@Component
public class PrintAnyGatewayFilterFactory // 父类泛型是内部类的Config类型
extends AbstractGatewayFilterFactory<PrintAnyGatewayFilterFactory.Config> {
@Override
public GatewayFilter apply(Config config) {
// OrderedGatewayFilter是GatewayFilter的子类,包含两个参数:
// - GatewayFilter:过滤器
// - int order值:值越小,过滤器执行优先级越高
return new OrderedGatewayFilter(new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取config值
String a = config.getA();
String b = config.getB();
String c = config.getC();
// 编写过滤器逻辑
System.out.println("a = " + a);
System.out.println("b = " + b);
System.out.println("c = " + c);
// 放行
return chain.filter(exchange);
}
}, 100);
}
// 自定义配置属性,成员变量名称很重要,下面会用到
@Data
static class Config{
private String a;
private String b;
private String c;
}
// 将变量名称依次返回,顺序很重要,将来读取参数时需要按顺序获取
@Override
public List<String> shortcutFieldOrder() {
return List.of("a", "b", "c");
}
// 返回当前配置类的类型,也就是内部的Config
@Override
public Class<Config> getConfigClass() {
return Config.class;
}
}
然后在yaml
文件中使用:
spring:
cloud:
gateway:
default-filters:
- PrintAny=1,2,3 # 注意,这里多个参数以","隔开,将来会按照shortcutFieldOrder()方法返回的参数顺序依次复制
上面这种配置方式参数必须严格按照shortcutFieldOrder()
方法的返回参数名顺序来赋值。
还有一种用法,无需按照这个顺序,就是手动指定参数名:
spring:
cloud:
gateway:
default-filters:
- name: PrintAny
args: # 手动指定参数名,无需按照参数顺序
a: 1
b: 2
c: 3
GlobalFilter
自定义GlobalFilter
则简单很多,直接实现GlobalFilter
即可,而且也无法设置动态参数:
@Component
public class PrintAnyGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 编写过滤器逻辑
System.out.println("未登录,无法访问");
// 放行
// return chain.filter(exchange);
// 拦截
ServerHttpResponse response = exchange.getResponse();
response.setRawStatusCode(401);
return response.setComplete();
}
@Override
public int getOrder() {
// 过滤器执行顺序,值越小,优先级越高
return 0;
}
}
配置管理
配置共享
设置NACOS
的配置列表
例子:
spring:
datasource:
url: jdbc:mysql://${hm.db.host:192.168.150.101}:${hm.db.port:3306}/${hm.db.database}?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
username: ${hm.db.un:root}
password: ${hm.db.pw:123}
mybatis-plus:
configuration:
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
global-config:
db-config:
update-strategy: not_null
id-type: auto
这里的jdbc
的相关参数并没有写死,例如:
-
数据库
ip
:通过${hm.db.host:192.168.150.101}
配置了默认值为192.168.150.101
,同时允许通过${hm.db.host}
来覆盖默认值 -
数据库端口:通过
${hm.db.port:3306}
配置了默认值为3306
,同时允许通过${hm.db.port}
来覆盖默认值 -
数据库
database
:可以通过${hm.db.database}
来设定,无默认值
拉取共享配置
读取Nacos
配置是SpringCloud
上下文(ApplicationContext
)初始化时处理的,发生在项目的引导阶段。然后才会初始化SpringBoot
上下文,去读取application.yaml
。
SpringCloud
在初始化上下文的时候会先读取一 个名为bootstrap.yaml
(或者bootstrap.properties
)的文件,将nacos
地址配置到bootstrap.yaml
中,在项目引导阶段就可以读取nacos中
的配置了。
-
引入依赖
<!--nacos配置管理-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--读取bootstrap文件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency> -
新建
bootstrap.yaml
,与application.yaml
同级spring:
application:
name: cart-service # 服务名称
profiles:
active: dev
cloud:
nacos:
server-addr: IP:8848 # nacos地址
config:
file-extension: yaml # 文件后缀名
shared-configs: # 共享配置
- dataId: shared-jdbc.yaml # 共享mybatis配置
- dataId: …… -
修改
application.yaml
server:
port: 8082
feign:
okhttp:
enabled: true # 开启OKHttp连接池支持
hm:
swagger:
title: "标题"
package: com.xxx.xx.controller # controller包
db:
database: xxx # 数据库名
配置热更新
当修改配置文件中的配置时,微服务无需重启即可使配置生效
前提:
-
nacos
中要有一个与微服务名有关的配置文件spring.application.name-spring.active.profile.file-extension
微服务名称-项目profile(可选 ).文件后缀名
user-service-dev.yaml如
cart-service.yaml
hm:
cart:
maxItems: 1 -
微服务中要以特定方式读取需要热更新的配置属性,
cart.config
包下-
@Data
@ConfigurationProperties(prefix = "hm.cart")
public class CartProperties {
private int maxItems;
}或
-
@Data
@RefreshScope
public class CartProperties {
@Value("${hm.cart.maxItems}")
private int maxItems;
}
-
动态路由
将路由配置保存到Nacos
,当Nacos
中的路由配置变更时,推送最新配置到网关,实时更新网关中的路由信息
监听Nacos配置变更
Nacos官网中给出了手动监听Nacos配置变更的SDK:https://nacos.io/zh-cn/docs/sdk.html
示例,网关微服务下:
-
引入依赖
<!--nacos配置管理-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--读取bootstrap文件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>配置
bootstrap.yaml
文件spring:
application:
name: gateway
profiles:
active: dev
cloud:
nacos:
server-addr: 192.168.88.130:8848
config:
file-extension: yaml
shared-configs:
- data-id: shared-log.yaml修改对应的
application.yaml
server:
port: 8080
hm:
jwt:
location: classpath:hmall.jks
alias: hmall
password: hmall123
tokenTTL: 30m
auth:
excludePaths:
- /search/**
- /users/login
- /items/**
- /hi -
创建类编写监听器
@PostConstruct
:在项目初始化后执行装饰的类@Component
@Slf4j
@RequiredArgsConstructor
public class DynamicRouteLoader {
private final NacosConfigManager nacosConfigManager;
private final RouteDefinitionWriter writer;
private final String dataId = "gateway-routes.json";
private final String group = "DEFAULT_GROUP";
private final Set<String> routeIds = new HashSet<>();
@PostConstruct
public void initRouteConfigListener() throws NacosException {
// 1.项目启动,先拉取配置,添加配置监听器
String configInfo = nacosConfigManager.getConfigService()
.getConfigAndSignListener(dataId, group, 5000, new Listener() {
@Override
public Executor getExecutor() {
return null;
}
@Override
public void receiveConfigInfo(String configInfo) {
// 2.监听配置变更,更新路由表
updateConfigInfo(configInfo);
}
});
// 3.监听初始配置,更新路由表
updateConfigInfo(configInfo);
}
// 解析JSON信息
public void updateConfigInfo(String configInfo) {
log.debug("监听到路由配置信息:{}", configInfo);
// 1.解析配置信息,转为RouteDefinition
List<RouteDefinition> routeDefinitions = JSONUtil.toList(configInfo, RouteDefinition.class);
// 2.删除旧的路由表
for (String routeId : routeIds) {
writer.delete(Mono.just(routeId)).subscribe();
}
routeIds.clear();
// 3.更新路由表
for (RouteDefinition routeDefinition : routeDefinitions) {
writer.save((Mono.just(routeDefinition))).subscribe();
// 记录路由id
routeIds.add(routeDefinition.getId());
}
}
}Nacos
里的配置 文件的格式[
{
"id": "路由id,唯一标识",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"/路由名/**", "_genkey_1":"/路由名/**"}
}],
"filters": [],
"uri": "lb://微服务名称"
},
{……}
]