🌈 服务网关 Spring Cloud Gateway
🐙 为什么需要服务网关
微服务架构的系统,拥有众多子服务,如果客户端直接与各个微服务进行通信,会有以下的问题:
- 客户端需要记录各个服务的地址
- 各个服务都需要单独处理跨域
- 各个服务都需要单独权限认证
- 。。。
🐙 什么是服务网关
服务网关是介于客户端和服务端之间的中间层,所有的外部请求都会先经过网关这一层。由网关决定将请求路由到具体服务。
可以处理非业务的的逻辑,如安全、监控、限流、熔断、授权等等。
🐙 什么是 Spring cloud Gateway
一个构建在 Spring 生态系统之上的 API 网关,包括:Spring 6、Spring Boot 3、Spring Webflux 和 Project Reactor。Spring Cloud Gateway 旨在提供一种简单而有效的方法来路由到 API,并为它们提供横切关注点,例如:安全性、监控/指标和弹性。
🍅 1、术语
Route
:Gateway 的基本构建块。它由 ID、目标 URI、谓词集合和过滤器集合定义。如果聚合谓词为真,则路由匹配。
Predicate
:是一个Java 8 函数谓词。输入类型是Spring FrameworkServerWebExchange。可以匹配 HTTP 请求中的任何内容,例如请求头或参数。
Filter
:是GatewayFilter使用特定工厂构建的实例。可以在发送下游请求之前或之后修改请求和响应。
🍅 2、工作原理
官网提供了 Spring Cloud Gateway 工作原理的高级概述图:
客户端向 Spring Cloud Gateway 发出请求。如果 Gateway Handler Mapping 确定请求与路由匹配,则将其发送到 Gateway Web Handler。此处理程序通过特定于请求的过滤器链运行请求。过滤器被虚线分开的原因是过滤器可以在发送代理请求之前和之后运行逻辑。执行所有 “pre”
过滤器逻辑。然后进行代理请求。发出代理请求后,运行 “post”
过滤器逻辑。
🌈 Spring Cloud Gateway 实战
🐙 版本定义
使用的版本如下:
<spring.boot.version>2.6.13</spring.boot.version>
<spring.cloud.version>2021.0.5</spring.cloud.version>
<spring.cloud-alibaba.version>2021.0.5.0</spring.cloud-alibaba.version>
🐙 新建模块 gateway
1、添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
2、添加配置
spring:
application:
name: spring-cloud-gateway
cloud:
gateway:
# 路由配置
routes:
- id: user_routh # 路由ID,没有固定规则但要求唯一
uri: http://localhost:8001 # 匹配后提供服务的路由地址
predicates:
- Path=/user-service/** # 断言,路径相匹配的进行路由
3、启动用户服务,网关服务
先访问用户服务自身接口:http://localhost:8001/user-service/user/1,请求成功。
通过网关路由到用户服务:http://localhost/user-service/user/1,请求成功。
🌈 Spring Cloud Gateway 谓词
🐙 配置路由谓词和过滤器
有两种配置谓词和过滤器的方法:快捷方式和完全扩展的参数。
1、快捷方式配置
快捷方式配置由过滤器名称识别,后跟等号 =
,然后是用逗号 ,
分隔的参数值。
示例
spring:
cloud:
gateway:
routes:
- id: example_route
uri: http://localhost:8001
predicates:
- Cookie=mycookie,mycookievalue
2、完全扩展的参数
完全扩展的参数是带有名称/值的标准 yaml 配置。
示例
spring:
cloud:
gateway:
routes:
- id: example_route
uri: http://localhost:8001
predicates:
- name: Cookie
args:
name: mycookie
regexp: mycookievalue
🐙 路由谓词工厂
Spring Cloud Gateway 包含许多内置的路由谓词工厂。所有这些谓词都匹配 HTTP 请求的不同属性。
这些 Predict 的源码在 org.springframework.cloud.gateway.handler.predicate
包中。
Spring Cloud Gateway 中的谓词工厂命名都是有规范的,格式:xxxRoutePredicateFactory
。
比如 Cookie 的路由谓词工厂:CookieRoutePredicateFactory
,使用配置时直接取前面的 Cookie。
一些示例:
spring:
cloud:
gateway:
routes:
- id: payment_routh
uri: lb://payment-service
predicates:
- Path=/payment/**
- After=2020-02-21T15:51:37.485+08:00[Asia/Shanghai]
- Cookie=username,don
- Header=X-Request-Id, \d+ # 请求头要有X-Request-Id属性并且值为整数的正则表达式
🌈 Spring Cloud Gateway 过滤器
过滤器允许以某种方式修改传入的 HTTP 请求或传出的 HTTP 响应。
🍅 1、过滤器的生命周期
PRE
:过滤器在请求路由之前调用。可以用来实现身份验证、在集群中选择请求的微服务、记录调试信息等。
POST
:过滤器在请求路由到微服务执行响应后调用。可以为响应添加标准的HTTP Header、收集统计信息等。
🍅 2、过滤器的分类
分为路由过滤器 GatewayFilter
和 全局过滤器 GlobalFilter
。
路由过滤器的作用范围是特定路由;全局过滤器应用于所有路由。
🐙 路由过滤器
路由过滤器允许以某种方式修改传入的 HTTP 请求或传出的 HTTP 响应。路由过滤器的范围是特定路由。
Spring Cloud Gateway 内置了许多路由过滤器,多达 30 多个。详细参考文档:
https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gatewayfilter-factories
🍅 1、路由过滤器的使用
示例,StripPrefix 网关过滤器工厂采用一个参数 parts
。parts 参数指示在将请求发送到服务之前要从请求路径中移除的层数。
spring:
cloud:
gateway:
routes:
- id: user_routh # 路由ID,没有固定规则但要求唯一
uri: lb://user-service # 匹配后提供服务的路由地址
predicates:
- Path=/user-service/** # 断言,路径相匹配的进行路由
filters:
- StripPrefix=1 # 将路径第一层移除
当向网关发送请求 http://localhost/user-service/user/1 时,经过路由转发到服务的是 nameservice/user/1
,也即移除了第一层 user-service
。
🍅 2、自定义路由过滤器
新建一个 GatewayFilterFactory
类,继承抽象类 AbstractGatewayFilterFactory
,命名必须是xxxGatewayFilterFactory
。
示例,计算请求服务调用耗时:
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.Collections;
import java.util.List;
@Slf4j
@Component
public class TimeGatewayFilterFactory extends AbstractGatewayFilterFactory<TimeGatewayFilterFactory.Config> {
private static final String BEGIN_TIME = "beginTime";
public TimeGatewayFilterFactory() {
super(Config.class);
}
/**
* 读取配置文件中的参数赋值到配置类中
*/
@Override
public List<String> shortcutFieldOrder() {
return Collections.singletonList("showTime");
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
if (!config.showTime) {
// 如果配置类中的show为false表示放行
return chain.filter(exchange);
}
exchange.getAttributes().put(BEGIN_TIME, System.currentTimeMillis());
/*
* pre 的逻辑
* chain.filter().then(Mono.fromRunnable(()->{
* post 的逻辑
* }))
*/
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
Long startTime = exchange.getAttribute(BEGIN_TIME);
if (null != startTime) {
log.info(exchange.getRequest().getURI() + " (耗时: " + (System.currentTimeMillis() - startTime) + "ms)");
}
}));
};
}
@Data
static class Config {
//Put the configuration properties for your filter here
private boolean showTime;
}
}
配置:
spring:
cloud:
gateway:
routes:
- id: user_routh # 路由ID,没有固定规则但要求唯一
uri: lb://user-service # 匹配后提供服务的路由地址
predicates:
- Path=/user-service/** # 断言,路径相匹配的进行路由
filters:
- Time=true
访问请求,查看控制台:
http://localhost/user-service/user/1 (耗时: 496ms)
🐙 全局过滤器
GlobalGilter
全局过滤器接口与 GatewayFilter
网关过滤器接口具有相同的方法定义。全局过滤器是一系列特殊的过滤器,会根据条件应用到所有路由中。
多个 GlobalFilter
可以通过 @Order
或者 getOrder()
方法指定每个 GlobalFilter
的执行顺序,order
值越小,执行的优先级越高。
Spring Cloud Gateway 内置了多个全局过滤器对整个路由进行处理。
🍅 1、内置的 GlobalFilter
全局过滤器
ForwardRoutingFilter
:转发路由过滤器ReactiveLoadBalancerClientFilter
:负载均衡客户端过滤器NettyRoutingFilter
和NettyWriteResponseFilter
:通过HttpClient
客户端转发请求直实的URL并将响应写入到当前的请求响应中RouteToRequestUrlFilter
:路由到指定URL的过滤器WebsocketRoutingFilter
:负责处理Websocket
类型的清求响应信息WebClientHttpRoutingFilter
和WebClientWriteResponseFilter
:通过WebClient
客户端转发请求直实的URL并将响应写入到当前的请求响应中
🍅 2、自定义全局过滤器
新建过滤器实现 GlobalFilter
接口,并注入到容器中
示例:
/**
* 访问日志全局过滤器
*/
@Slf4j
@Component
@Order(value = Integer.MIN_VALUE)
public class AccessLogGlobalFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// filter 的前置处理
ServerHttpRequest request = exchange.getRequest();
String path = request.getPath().pathWithinApplication().value();
InetSocketAddress remoteAddress = request.getRemoteAddress();
return chain
// 继续调用 filter
.filter(exchange)
// filter 的后置处理
.then(Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
HttpStatus statusCode = response.getStatusCode();
log.info("请求路径:{}, IP地址:{}, 响应码:{}", path, remoteAddress, statusCode);
})
);
}
}
全局过滤器不必在路由上配置,注入到 IOC 容器中即可全局生效。
🌈 Spring Cloud Gateway 集成注册中心
上述在配置文件中路由的服务地址是固定的 uri
,这种配置存在一定缺点:
- 微服务的地址修改了,路由配置中的
uri
必须修改 - 微服务集群无法实现负载均衡
即需要集成注册中心,使得网关能够从注册中心自动获取服务地址实现负载均衡。
🐙 集成注册中心 Nacos
下文通过集成 Nacos 在注册中心来获取各个微服务的地址。
1、添加 Nacos 依赖
<!-- Nacos 注册中心 -->
<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>
2、在主启动类添加注解
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@EnableDiscoveryClient
3、修改配置文件
spring:
application:
name: spring-cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
lower-case-service-id: true # 小写
# 路由配置
routes:
- id: user_routh # 路由ID,没有固定规则但要求唯一
uri: lb://user-service # 从注册中心获取微服务地址,并遵循负载均衡策略
predicates:
- Path=/user-service/** # 断言,路径相匹配的进行路由
# Nacos 配置
nacos:
server-addr: 127.0.0.1:8848
路由配置中,路由的 uri 格式:lb://service-name
,这是固定写法:
-
lb
:指的是从注册中心按照名称获取微服务地址,并遵循负载均衡策略全局过滤器
ReactiveLoadBalancerClientFilter
处理路由和负载均衡 -
service-name
:注册中心的服务名称
4、启动用户服务,网关服务
成功注册到注册中心 Nacos:
通过网关路由到用户服务:http://localhost/user-service/user/1,请求成功。
🌈 Spring Cloud Gateway 集成配置中心
如果将网关的一系列配置写到项目的配置文件中,一旦路由发生改变必须要重启项目,这样维护成本很高。
可以将网关的配置存放到配置中心中,这样由配置中心统一管理,一旦路由发生改变,只需要在配置中心修改。
🐙 集成配置中心 Nacos 实现动态路由
下文通过集成 Nacos 在配置中心来管理路由信息。
1、添加依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
2、在 bootstrap.yml
配置 Nacos 作为配置中心
spring:
application:
name: spring-cloud-gateway
cloud:
### Nacos 配置
nacos:
server-addr: 127.0.0.1:8848
config:
# 指定文件后缀未yaml
file-extension: yaml
3、在 Nacos 中的默认命名空间中创建 Data ID 为 spring-cloud-gateway.yaml
的配置(未指定环境),编写路由信息
spring:
cloud:
gateway:
## 路由配置
routes:
- id: system-service # 路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: lb://system-service # 匹配后提供服务的路由地址,lb 负载均衡
predicates:
- Path=/system-service/** # 断言,路径相匹配的进行路由
filters:
- Time=true
4、启动网关服务
发送请求 http://localhost/system-service/dept 成功路由到系统服务
🌈 Spring Cloud Gateway 异常处理
当网关服务或业务服务出现异常时,Spring Cloud Gateway 会返回一堆错误信息,非常不友好。
Spring Boot 服务中都是使用 @ControllerAdvice
来包装全局异常处理的,当服务下线或宕机时,请求并没有到达服务。这时网关中也要定制一层全局异常处理,这样才能更加友好的和客户端交互。
🐙 自定义全局异常处理
创建一个类 GlobalErrorExceptionHandler
实现 ErrorWebExceptionHandler
重写其中的 handle 方法:
/**
* 网关的全局异常处理
* <p>
* 优先级一定要小于内置 ResponseStatusExceptionHandler 经过它处理的获取对应错误类的 响应码
*/
@Slf4j
@Order(-1)
@Component
public class GlobalErrorExceptionHandler implements ErrorWebExceptionHandler {
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
// 已经 commit 则直接返回异常
ServerHttpResponse response = exchange.getResponse();
if (response.isCommitted()) {
return Mono.error(ex);
}
// 转换成 Result
Result<?> result;
if (ex instanceof NotFoundException) {
result = notFoundExceptionHandler(exchange, (NotFoundException) ex);
} else if (ex instanceof ResponseStatusException) {
result = responseStatusExceptionHandler(exchange, (ResponseStatusException) ex);
} else {
result = defaultExceptionHandler(exchange, ex);
}
// 返回给前端
return WebUtil.writeJson(exchange, result);
}
/**
* 处理 Spring Cloud Gateway 抛出的 NotFoundException 异常
*/
private Result<?> notFoundExceptionHandler(ServerWebExchange exchange, NotFoundException ex) {
ServerHttpRequest request = exchange.getRequest();
log.error("[Gateway NotFoundException]. uri({} / {}), ex({})]", request.getURI(), request.getMethod(), ex.getMessage());
return Result.failed(ex.getRawStatusCode(), "服务未找到");
}
/**
* 处理 Spring Cloud Gateway 默认抛出的 ResponseStatusException 异常
*/
private Result<?> responseStatusExceptionHandler(ServerWebExchange exchange, ResponseStatusException ex) {
ServerHttpRequest request = exchange.getRequest();
log.error("[Gateway ResponseStatusException]. uri({} / {}), ex({})]", request.getURI(), request.getMethod(), ex.getMessage());
return Result.failed(ex.getRawStatusCode(), ex.getReason());
}
/**
* 兜底处理系统异常
*/
@ExceptionHandler(value = Exception.class)
public Result<?> defaultExceptionHandler(ServerWebExchange exchange, Throwable ex) {
ServerHttpRequest request = exchange.getRequest();
log.error("[Gateway Exception]. uri({} / {}), ex({})]", request.getURI(), request.getMethod(), ex.getMessage());
return Result.failed(CommonConstants.FAIL, ex.getMessage());
}
}
重启网关服务测试:
{
"code": 503,
"msg": "服务未找到"
}