1.为什么要使用Gateway?
大家都都知道在微服务架构中,一个系统会被拆分为很多个微服务。那么作为客户端要如何去调用这么多的微服务呢?如果没有网关的存在,我们只能在客户端记录每个微服务的地址,然后分别去调用。这样的架构,会存在着诸多的问题:
-
客户端请求不同的微服务,就要维护不同的ip
-
客户端无法实现负载均衡
上面的这些问题可以借助API网关来解决:
在业界比较流行的网关,有下面这些:
-
Ngnix+lua
使用nginx的反向代理和负载均衡可实现对api服务器的负载均衡及高可用 lua是一种脚本语言,可以来编写一些简单的逻辑,nginx支持lua脚本
-
Spring Cloud Gateway
Spring公司为了替换Zuul而开发的网关服务,将在下面具体介绍。
注意:SpringCloud alibaba技术栈中并没有提供自己的网关,我们可以采用Spring Cloud Gateway来做网关。
2.Gateway介绍
2.1.Gateway是什么
Spring Cloud Gateway是Spring公司基于Spring 5.0,Spring Boot 2.0 和 Project Reactor等技术开发的网关,目标是替代 Netflflix ZUUL,并且基于 Filter 链的方式提供了路由,过滤,和限流等功能。
组件 | RPS(request per second) |
---|---|
Spring Cloud Gateway | Requests/sec: 32213.38 |
Zuul1X | Requests/sec: 20800.13 |
上表为Spring Cloud Gateway与Zuul的性能对比,从结果可知,Spring Cloud Gateway的RPS是Zuul的1.6倍
2.2.Gateway执行流程
-
Gateway Client向Gateway Server发送请求
-
HandlerMapping负责路由查找,并根据路由断言判断路由是否可用
-
WebHandler创建过滤器链并调用
-
请求会一次经过PreFilter–微服务–PostFilter的方法,最终返回响应给Gateway Client
3.创建gateway工程
3.1.pom.xml
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springcloud_parent</artifactId>
<groupId>com.bjpowernode</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>api_gateway</artifactId>
<dependencies>
<!-- nacos启动器 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- gateway的启动器 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
</dependencies>
</project>
注意:不要添加spring-boot-starter-web启动器
<!--servlet运行的服务器是tomcat-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
3.2.application.yml
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: 192.168.209.129:8848
server:
port: 9527
3.3.App
package com.bjpowernode;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class GatewayApp {
public static void main(String[] args) {
SpringApplication.run(GatewayApp.class, args);
}
}
4.路由
4.1.通过断言条件路由
4.1.1.application.yml
spring:
cloud:
gateway:
routes:
- id: sentinel-consumer #自定义的路由ID,保持唯一
uri: http://localhost:80 # 请求要转发到的地址
predicates: #断言
- Path=/consumer/** #只有断言条件返回true(请求路径包含“/consumer”)时,才进行路由转发
4.1.2. 测试
1.开启服务和网关:
- api_gateway
- sentinel_consumer
- sentinel_provider
2.浏览器访问:http://127.0.0.1:9527/consumer/getUserById/1
4.2.通过服务名路由
4.2.1.application.yml
spring:
cloud:
gateway:
routes:
- id: sentinel-consumer #自定义的路由ID,保持唯一
uri: lb://sentinel-consumer #lb代表从注册中心获取服务
predicates: #断言
- Path=/consumer/** #只有断言条件返回true(请求路径包含“/consumer”)时,才进行路由转发
4.2.2. 测试
- 重启网关:
- api_gateway
- 浏览器访问:http://127.0.0.1:9527/sentinel-consumer/consumer/getUserById/1
5.断言
5.1.内置断言工厂
Spring Cloud Gateway 的功能很强大,前面我们只是使用了 predicates 进行了简单的条件匹配,其实Spring Cloud Gataway 帮我们内置了很多 Predicates 功能。
可参考spring官网:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gateway-request-predicates-factories
5.1.1.基于Datetime类型的断言工厂
- 说明
此类型的断言根据时间做判断,主要有三个:
AfterRoutePredicateFactory:接收一个日期参数,判断请求日期是否晚于指定日期。
BeforeRoutePredicateFactory:接收一个日期参数,判断请求日期是否早于指定日期。
BetweenRoutePredicateFactory:接收两个日期参数,判断请求日期是否在指定时间段内。
- 案例
- After=2022-04-09T17:20:54.957+08:00[Asia/Shanghai]
5.1.2.基于RemoteAddr的断言工厂
- 说明
RemoteAddrRoutePredicateFactory:接收一个IP地址段,判断请求主机地址是否在地址段中。
- 案例
- RemoteAddr=192.168.1.1/24
5.1.3.基于Cookie的断言工厂
- 说明
CookieRoutePredicateFactory:接收两个参数,cookie名字和一个正则表达式。判断请求cookie是否具有给定名称且值与正则表达式匹配。
- 案例
- Cookie=chocolate, ch.
5.1.4.基于Header的断言工厂
- 说明
HeaderRoutePredicateFactory:接收两个参数,标题名称和正则表达式。 判断请求Header是否 具有给定名称且值与正则表达式匹配。
- 案例
- Header=X-Request-Id, \d+
5.1.5.基于Host的断言工厂
- 说明
HostRoutePredicateFactory:接收一个参数,主机名模式。判断请求的Host是否满足匹配规则。
- 案例
- Host=**.testhost.org
5.1.6.基于Method请求方法的断言工厂
- 说明
MethodRoutePredicateFactory:接收一个参数,判断请求类型是否跟指定的类型匹配。
- 案例
- Method=GET
5.1.7.基于Path请求路径的断言工厂
- 说明
PathRoutePredicateFactory:接收一个参数,判断请求的URI部分是否满足路径规则。
- 案例
- Path=/foo/{segment}
5.1.8.基于Query请求参数的断言工厂
- 说明
QueryRoutePredicateFactory :接收两个参数,请求param和正则表达式, 判断请求参数是否具有给定名称且值与正则表达式匹配。
- 案例
- Query=baz, ba.
5.2.自定义路由断言工厂
5.2.1.内置断言工厂的实现原理
我们来设定一个场景: 假设我们的应用仅仅让age>18的人来访问,在自定义断言工厂之前,我们先看内置断言工厂的实现原理,打开AfterRoutePredicateFactory这个内置断言工厂:
5.2.2.创建断言工厂
/**
* 自定义断言工厂
* Config 是一个类 需要我们自己去定义
*/
@Component
public class AgeRoutePredicateFactory extends AbstractRoutePredicateFactory<AgeRoutePredicateFactory.Config> {
public AgeRoutePredicateFactory() {
super(AgeRoutePredicateFactory.Config.class);
}
//读取配置文件的中参数值 给他赋值到配置类中的属性上
@Override
public List<String> shortcutFieldOrder() {
//这里的顺序要跟配置文件中的参数顺序一致
return Arrays.asList("minAge", "maxAge");
}
//断言逻辑
@Override
public Predicate<ServerWebExchange> apply(AgeRoutePredicateFactory.Config config) {
return new Predicate<ServerWebExchange>() {
@Override
public boolean test(ServerWebExchange serverWebExchange) {
//1 接收前台传入的age参数
String ageStr =
serverWebExchange.getRequest().getQueryParams().getFirst("age");
//2 先判断是否为空
if (StringUtils.isNotEmpty(ageStr)) {
//3 如果不为空,再进行路由逻辑判断
int age = Integer.parseInt(ageStr);
if (age < config.getMaxAge() && age > config.getMinAge()) {
return true;
} else {
return false;
}
}
return false;
}
};
}
//自定义一个配置类, 用于接收配置文件中的参数
@Data
@NoArgsConstructor
public static class Config {
private int minAge;//18
private int maxAge;//60
}
}
5.2.3.配置断言
spring:
cloud:
gateway:
routes:
- id: sentinel-consumer
uri: lb://sentinel-consumer
predicates:
- Path=/consumer/**
- Age=18,60 #配置断言必须使用断言工厂类名的前缀
5.2.4.测试
- 浏览器访问:http://127.0.0.1:9527/consumer/getUserById/1?age=17
- 浏览器访问:http://127.0.0.1:9527/consumer/getUserById/1?age=19
6.过滤
Spring Cloud Gateway提供了过滤器的功能,可以对进入网关的请求
和响应
做处理:
Spring Cloud Gateway 的 Filter 从作用范围可分为另外两种GatewayFilter 与 GlobalFilter。
-
GatewayFilter:应用到单个路由或者一个分组的路由上。
-
GlobalFilter:应用到所有的路由上。
6.1.内置过滤器工厂
Spring Cloud Gateway中通过GatewayFilter的形式内置了很多不同类型的局部过滤器,可参考spring官网:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gatewayfilter-factories
过滤器工厂 | 作用 |
---|---|
AddRequestHeader | 给当前请求添加一个请求头 |
AddRequestParameter | 为原始请求添加请求参数 |
AddResponseHeader | 给响应结果中添加一个响应头 |
DedupeResponseHeader | 去掉重复请求头 |
Spring Cloud CircuitBreaker | 断路器 |
FallbackHeaders | 添加熔断后的异常信息到请求头 |
MapRequestHeader | 将上游请求头的值赋值到下游请求头 |
PrefixPath | 匹配的路由添加前缀 |
PreserveHostHeader | 保留原请求头 |
RequestRateLimiter | 限制请求的流量 |
RedirectTo | 重定向 |
RemoveRequestHeader | 移除请求中的一个请求头 |
RemoveResponseHeader | 从响应结果中移除有一个响应头 |
RemoveRequestParameter | 移除请求参数 |
RewritePath | 重写路径 |
RewriteLocationResponseHeader | 重写响应头中Location的值 |
RewriteResponseHeader | 重写响应头 |
SaveSession | 向下游转发请求前前置执行WebSession::save的操作 |
SecureHeaders | 禁用默认值 |
SetPath | 设置路径 |
SetRequestHeader | 重置请求头 |
SetResponseHeader | 修改响应头 |
SetStatus | 修改响应的状态码 |
StripPrefix | 对指定数量的路径前缀进行去除 |
Retry | 重试 |
RequestSize | 请求大小大于限制时,限制请求到达下游服务 |
SetRequestHostHeader | 重置请求头值 |
Modify a Request Body | 修改请求体内容 |
Modify a Response Body | 修改响应体内容 |
Relay | 将 OAuth2 访问令牌向下游转发到它所代理的服务 |
CacheRequestBody | 在请求正文发送到下游之前缓存请求正文并从 exchagne 属性获取正文 |
6.2.自定义过滤器工厂
需求:记录调用远程服务所需要的时间
6.2.1.创建过滤器工厂
@Component
public class LogGatewayFilterFactory
extends AbstractGatewayFilterFactory<LogGatewayFilterFactory.Config> {
public LogGatewayFilterFactory() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {
if (config.paramValue) {
long beginTime = System.currentTimeMillis();
return chain.filter(exchange).then(Mono.fromRunnable(new Runnable() {
@Override
public void run() {
long endTime = System.currentTimeMillis();
System.out.println("time of call service :" +
(endTime - beginTime));
}
}));
}
return chain.filter(exchange);
}
};
}
//读取配置文件中的参数 赋值到配置类中
@Override
public List<String> shortcutFieldOrder() {
//这里的顺序要跟配置文件中的参数顺序一致
return Arrays.asList("paramValue");
}
//自定义一个配置类, 用于接收配置文件中的参数
@Data
@NoArgsConstructor
public static class Config {
private boolean paramValue;
}
}
6.2.2.配置过滤器
spring:
cloud:
routes:
- id: sentinel-consumer
uri: lb://sentinel-consumer
predicates:
- Path=/consumer/**
filters: #过滤
- Log=true
6.2.3.测试
- 浏览器访问:http://127.0.0.1:9527/consumer/getUserById/1
6.3.自定义全局过滤器
- 需求:
在网关过滤器中通过Token 判断用户是否登录
- 全局过滤器(GlobalFilter):
作用于所有路由,Spring Cloud Gateway 定义了Global Filter接口,用户可以自定义实现自己的Global Filter。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能,并且全局过滤器也是程序员使用比较多的过滤器。Spring Cloud Gateway内部也是通过一系列的内置全局过滤器对整个路由转发进行处理如下:
6.2.1.创建过滤器
在服务网关中定义过滤器只需要实现 GlobalFilter, Ordered接口就可对请求进行拦截与过滤。
@Component
public class LoginFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
String token = request.getQueryParams().getFirst("token");
if (token == null) {
BaseResult data = new BaseResult(401, "未登录");
return response(response,data);
}
//放行
return chain.filter(exchange);
}
private Mono<Void> response(ServerHttpResponse response, BaseResult data) {
String jsonData = null;
try {
jsonData = new ObjectMapper().writeValueAsString(data);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
DataBuffer buffer =
response.bufferFactory().wrap(jsonData.getBytes(StandardCharsets.UTF_8));
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
return response.writeWith(Mono.just(buffer));
}
/**
* 过滤器的执行顺序:通过整数表示顺序,数值越小,优先级越高
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
class BaseResult {
private int status;
private String msg;
private Object data;
public BaseResult() {
}
public BaseResult(int status, String msg) {
this.status = status;
this.msg = msg;
}
public BaseResult(int status, String msg, Object data) {
this.status = status;
this.msg = msg;
this.data = data;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
6.2.2.测试
- 不加token:http://127.0.0.1:9527/gateway/consumer/getUserById?id=1&nam=zs
- 加token:http://127.0.0.1:9527/consumer/getUserById/1?token=123
7.限流
网关是所有请求的公共入口,所以可以在网关进行限流,而且限流的方式也很多,我们本次采用前 面学过的Sentinel组件来实现网关的限流。
7.1.api_gateway
7.1.1.pom.xml
<!--sentinel的启动器-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--spring cloud gateway整合sentinel的启动器-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
7.1.2.application.yml
spring:
cloud:
sentinel:
transport:
dashboard: 127.0.0.1:8080 #指定sentinel的地址
7.1.3.自定义异常处理
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import com.alibaba.csp.sentinel.slots.system.SystemBlockException;
import com.alibaba.fastjson.JSON;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class GatewayConfig {
@PostConstruct
public void init() {
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange exchange,
Throwable t) {
// 自定义异常信息
Map map = new HashMap<>();
map.put("status", 200);
map.put("msg", "接口被限流了");
return ServerResponse.status(HttpStatus.OK)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(map));
}
};
//自定义异常处理
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
}
7.2.测试
- 新增流控规则
- 高并发访问:http://127.0.0.1:9527/consumer/getUserById/1