网关是微服务架构应用系统的前置流量入口,有着非常重要的作用,下面上接前文,继续聊聊Spring Cloud Gateway网关。
1、为什么需要网关
传统的单体架构中只需要开发一个服务给客户端调用,但是在微服务架构中是将一个应用系统拆分成多个微服务,客户端需要在本地记录每个微服务的调用地址,当微服务数量或实例很多时,客户端调用时就会很麻烦,增加了维护微服务地址的工作量。
网关作为应用系统的唯一流量入口,封装了应用系统的架构,所有的请求都先经过网关,由网关服务将请求路由到合适的微服务,主要作用如下:
- 简化客户端工作:网关将微服务封装起来后,客户端只需同网关交互,而不必调用各个不同服务;
- 降低耦合度:一旦服务地址修改,只需要修改网关的路由策略,不必修改每个调用的客户端,从而降低耦合度;
- 让开发人员把精力专注于业务逻辑:由网关统一实现服务路由、负载均衡、访问控制、流控、熔断降级等非业务功能;
但是网关也存在不足之处,在微服务这种去中心化的架构中,网关又成为了一个中心点或瓶颈点,它增加了一个必须开发、部署和维护的组件,所以需要对网关的响应结果有数据缓存能力,通过返回缓存数据或默认数据屏蔽后端服务的失败。网关最好是支持 I/O 异步、同步非阻塞的,如果服务是同步阻塞调用,可以理解为微服务模块之间是没有彻底解耦的,即如果A依赖B提供的API,如果B提供的服务不可用将直接影响到A不可用。
2、网关基本功能
3、Spring Cloud Gateway网关
搭建网关时pom中引入如下依赖
<properties>
<spring-boot.version>2.7.0</spring-boot.version>
<spring-cloud.version>2021.0.4</spring-cloud.version>
<spring-cloud-alibaba.version>2021.1</spring-cloud-alibaba.version>
</properties>
<!-- 只声明依赖,不引入依赖 -->
<dependencyManagement>
<dependencies>
<!-- 声明springBoot版本 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 声明springCloud版本 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 声明 springCloud Alibaba 版本 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
</dependencies>
Spring Cloud Gateway概念:
- 断言:predicate,参照 Java8 的新特性Predicate,允许开发人员匹配 HTTP 请求中的任何内容,比如请求头或请求参数,最后根据匹配结果返回一个布尔值;
- 路由:route,由ID、目标URI、断言集合和过滤器集合组成。如果聚合断言结果为真,则转发到该路由;
- 过滤器:filter,可以在返回请求之前或之后修改请求和响应的内容;
Ⅰ、断言
Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。Predicate 可以用于接口请求参数校验、判断新老数据是否有变化需要进行更新操作。
权重配置示例:
spring:
cloud:
gateway:
# 路由数组:指当请求满足什么样的断言时,转发到哪个服务上
routes:
# 路由标识,要求唯一,名称任意
- id: gateway-provider_1
# 请求最终被转发到的目标地址
uri: http://localhost:9024
# 设置断言
predicates:
# Path Route Predicate Factory 断言,满足 /gateway/provider/** 路径的请求都会被路由到 http://localhost:9024 这个uri中
- Path=/gateway/provider/**
# Weight Route Predicate Factory 断言,同一分组按照权重进行分配流量,这里分配了80%
# 第一个group1是分组名,第二个参数是权重
- Weight=group1, 8
# 配置过滤器(局部)
filters:
# StripPrefix:去除原始请求路径中的前1级路径,即/gateway
- StripPrefix=1
- id: gateway-provider_2
uri: http://localhost:9025
predicates:
- Path=/gateway/provider/**
# Weight Route Predicate Factory,同一分组按照权重进行分配流量,这里分配了20%
- Weight=group1, 2
filters:
# StripPrefix:去除原始请求路径中的前1级路径,即/gateway
- StripPrefix=1
Ⅱ、路由
Route 主要由 路由id、目标uri、断言集合和过滤器集合组成。
- id:路由标识,要求唯一,名称任意(默认值 uuid,一般不用,需要自定义);
- uri:请求最终被转发到的目标地址;
- order:路由优先级,数字越小,优先级越高;
- predicates:断言数组,即判断条件,如果返回值是true,则转发请求到 uri 属性指定的服务中;
- filters:过滤器数组,在请求传递过程中,对请求做一些修改;
Ⅲ、过滤器
gateway过滤器的生命周期分为:
- PRE:这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等;
- POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的 HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等;
gateway过滤器的作用范围分为:
- GatewayFilter:应用到单个路由或者一个分组的路由上(需要在配置文件中配置);
Spring Cloud Gateway 中内置了许多的局部过滤器,详细查看官网。 - GlobalFilter:应用到所有的路由上(无需配置,全局生效);
全局过滤器应用全部路由上,无需开发者配置,Spring Cloud Gateway 也内置了一些全局过滤器,详细查看官网。
4、Spring Cloud Gateway集成nacos注册中心
上面的配置示例中并没有集成注册中心,每次路由配置都是指定固定的服务ip:port,回看如下
这样配置,网关服务需要知道所有服务的域名或ip+port,另外,一旦服务的域名或ip+port发生变化,路由配置中的uri也必须修改。再者,服务集群中无法实现负载均衡。
那么可以通过集成nacos注册中心实现服务发现,网关能够从注册中心自动获取uri,并实现负载均衡。
配置nacos如下:
nacos:
namespace: 856a40d7-6548-4494-bdb9-c44491865f63
url: 127.0.0.1:8081
spring:
cloud:
nacos:
discovery:
server-addr: ${nacos.url}
namespace: ${nacos.namespace}
register-enabled: true
配置gateway修改为:
spring:
cloud:
gateway:
routes:
- id: gateway-provider_1
## 使用了lb形式,从注册中心负载均衡的获取uri
uri: lb://gateway-provider
## 配置断言
predicates:
- Path=/gateway/provider/**
## 配置过滤器
filters:
- AddResponseHeader=X-Response-Foo, Bar
配置中唯一不同的就是路由uri,格式为
lb://service-name
其中,lb:固定格式,指的是从nacos中按照名称获取微服务,并遵循负载均衡策略;service-name:nacos注册中心的服务名称,这里并不是IP地址形式的;
为什么指定了 lb 就可以开启负载均衡,前面说过全局过滤器 LoadBalancerClientFilter 就是负责路由寻址和负载均衡的,若是lb形式的,就进行负载均衡处理了。
随着系统架构的不断发展,微服务的数量肯定会越来越多,不可能每添加一个微服务,就在网关配置一个新的路由规则,那样维护成本很大。这时可以开启自动路由功能,网关自动根据注册中心的服务名为每个服务创建一个router,将以服务名开头的请求路径转发到对应的服务,配置如下:
# enabled:默认为false,设置为true表明spring cloud gateway开启服务发现和路由的功能,网关自动根据注册中心的服务名为每个服务创建一个router,将以服务名开头的请求路径转发到对应的服务
spring.cloud.gateway.discovery.locator.enabled = true
# lowerCaseServiceId:启动 locator.enabled=true 自动路由时,路由的路径默认会使用大写ID,若想要使用小写ID,可将lowerCaseServiceId设置为true
spring.cloud.gateway.discovery.locator.lower-case-service-id = true
5、Spring Cloud Gateway自定义全局异常处理
在应用网关时可以看到一个现象,一旦路由的微服务下线或者失联了,Spring Cloud Gateway直接返回了一个错误页面,显然这种异常信息不友好,前后端分离架构中必须定制返回的异常信息。传统的Spring Boot 服务中都是使用 @RestControllerAdvice 来包装全局异常处理的,但是由于服务下线,请求并没有到达。因此必须在网关中也要定制一层全局异常处理,这样才能更加友好的和客户端交互。
实现 ErrorWebExceptionHandler,重写其中的 handle 方法,如下
@Slf4j
@Order(-1)
@Component
@RequiredArgsConstructor
public class GlobalErrorExceptionHandler implements ErrorWebExceptionHandler {
private final ObjectMapper objectMapper;
@SuppressWarnings({"rawtypes", "unchecked", "NullableProblems"})
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
ServerHttpResponse response = exchange.getResponse();
if (response.isCommitted()) {
return Mono.error(ex);
}
// Json格式返回
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
if (ex instanceof ResponseStatusException) {
response.setStatusCode(((ResponseStatusException) ex).getStatus());
}
return response.writeWith(Mono.fromSupplier(() -> {
DataBufferFactory bufferFactory = response.bufferFactory();
try {
ResponseData resultMsg = ResponseData.fail(ex.getMessage(),503);
return bufferFactory.wrap(objectMapper.writeValueAsBytes(resultMsg));
}
catch (JsonProcessingException e) {
log.error("Error writing response", ex);
return bufferFactory.wrap(new byte[0]);
}
}));
}
}
测试结果类似如下
浏览器访问
postman访问