一.前言
本章分享内容着重工作中遇到的场景,大致聊一下gateWay的基本原理,不做源码的深入剖析,项目搭建环节直接使用工作中最常用的方式,一些不常用的方式大致介绍一下,不会详细编码。本分享内容旨在帮助大家快速理解工作中gateWay的使用,完成常见开发任务,及能在面试中和面试官讲清gateWay。
项目搭建基于本专题系列前面的内容,目前已经启动了nacos服务,启动并在nacos中注册了两个服务,没有时间看前面文章的道友可以直接看一下本章节搭建完成后的项目结构,如下图:
二.gateWay项目搭建
1.理解gateWay
gateWay是什么: gateWay是目前springcloud使用最多的网关组件,它能够对请求进行转发,将一个访问路径转发到真实的服务中,并在这一过程中对请求进行一些进入服务之前的前置处理。gateWay提供了很多的谓词,断言和过滤器,能够应对各种场景的业务开发。我们开发中,一般会独立出一个gateWay服务,它也是一个springboot服务,但是它使用的不是mvc,而是webflux,因此拥有更高的吞吐量。
gateWay工作原理:直接引用官方的一张图:
简单来说,就是客户端向gateWay服务发起请求,如果 Gateway Handler Mapping 确定请求与路由匹配,则将其发送到 Gateway Web Handler,然后请求通过一系列的过滤器,最终发送到服务上。
gateWay和Nginx的区别:很多面试会问这个。首先,是技术是技术实现上的差别,nginx是c,gateWay是java。然后,是作用范围,我们常说,nginx是反向代理,gateWay是网关,但是他们都能做到通过路径找到对应服务器的功能,但是,他们的作用范围有所区别,我们一般说的反向代理,面向应用级别,接收到的客户端请求转发给后端服务器;而网关,偏向开发者,偏向API层级。使用上,我们通常将nginx作为对外的一层,用户请求先通过nginx,然后由nginx转发到gateWay服务,再由gateWay服务将请求转发给具体服务。
2. 搭建gateWay项目
step1:创建一个maven工程 gateWay(名字随便起)
step2:pom中引入依赖
<dependencies>
<!-- SpringBoot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- SpringCloud Alibaba 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>
<!-- springCloud 提供的一个负载均衡器 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId> spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- cloud提供的一个配置文件加载和解析组件 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
此时项目结构如下:
各个依赖包的讲解:
1)spring-boot-start包是必须的,因为gateWay项目也是一个springboot项目
2)nacos-discovery包是为在nacos上对gateWay进行注册
3)starter-gateway gateWay服务的核心包
4)loadbalancer gateWay在高版本想要使用注册中心做负载均衡需要手动引入这个,如果没有的话,使用lb://这种语法时候会报503
5)bootstrap 想用bootstrap.yml必备
step3:配置文件配置
# tomcat
server:
port: 6213
# spring
spring:
application:
name: rui-gateway
profiles:
# 环境配置
active: dev
# cloud
cloud:
nacos:
discovery:
# 服务注册地址
server-addr: 127.0.0.1:8848
# gateway的配置
gateway:
discovery:
locator:
enabled: true
routes:
- id: admin-service
uri: lb://rui-admin
predicates:
- Path=/admin/**
filters:
- StripPrefix=1
- id: buness-server
uri: lb://rui-buness
predicates:
- Path=/buness/**
filters:
- StripPrefix=1
step4:启动gateWay服务,可以看在nacos中看到,gateWay服务已经注册到了nacos中
3. 配置文件详解
** 重点在6 **
1. server.port 指定服务启动端口
2.spring.application.name: 设定服务名称,这个名称在nacos中会被作为为服务名
3.spring.profile:active: 指定环境,工作中会根据不同环境加载不同配置文件
4.spring.cloud.nacos.discovery: 指定nacos服务注册地址,引入了nacos-discovery包需要配置这个
5.gateway.discovery.locator.enable: 允许gateWay使用自动路由,即lb://这种语法
6.gateway.routes:
gateWay的路由配置,每个路由有几个重要属性,分别是 id, uri, predicates, filters
id 是路由的唯一标识
uri 是转到到的路径,可以写固定的路径,也可以使用上面配置中lb://rui-admin 这种语法,lb://rui-admin的意思是,lb是loadBalance的缩写,rui-admin是nacos中配置的一个service,可能是集群,所以可以理解,lb://rui-admin的意思就是 将请求转到rui-admin并做负载均衡。
predicates 断言,也叫谓词。可以简单理解它是一种判断条件,gateWay内置了许多的条件供我们使用,上文配置中的 - Path = /admin/** 的意思就是,将请求路径是 /admin/任意内容 的请求转发到当前这个路由配置的uri上,除了 Path = /admin/**之外,gateWay还支持很多种断言,比如时间的,host的等。具体的可以在官网查看。附地址 https://docs.spring.io/spring-cloud-gateway/docs/3.1.9/reference/html/#gateway-request-predicates-factories
没有必要全部记住,工作中基本上只是用path这个方法,再记一两个用来在防身就够了
filters gateWay的内置过滤器。其作用是在一些特定场景下修改请求或者响应,以上文配置中内容为例 filters = - StripPrefix=1 ,它的意思就是 将请求路径中的第一段截取掉。例如,有一个请求, {域名}/admin/health/hello,这个请求到了gateWay,gateWay识别发现/admin 符合 predicates中配置的 Path = /admin/**,就会将 /admin/health/hello这个请求转给 admin服务,但是admin服务中,实际的接口地址是/health/hello,所以,需要截取掉/admin这段。而filters = - StripPrefix=1这段配置就帮我们做了这件事。除了介绍的这一种,gateWay还有许多的内置过滤器,依旧是附上官网地址 Spring Cloud Gateway
大家可以看一下具体有哪些,不需要记住太多
三.使用gateWay进行拓展
1. gateWay的全局过滤器
gateWay提供了允许自定义的全局过滤器,并将自定义的全局过滤器添加到过滤器链中,当请求与路径匹配时,请求会经过所有的过滤器。因此,我们可以在我们的自定义过滤器中,对请求进行一些处理。在日常工作中,最常见的场景有:Token校验,黑名单拦截,限流,password解码。
以下以一个 伪token校验来了解以下全局过滤器的使用:
增加一个gateway配置对象
编写全局过滤器
写一个配置类:
@Configuration
public class GateWayConfig {
@Bean
public GlobalFilter ruiToknFilter() {
return new RuiTokenFilter();
}
}
编写全局过滤器:
/**
* 自定义全局过滤器需要实现
* GlobalFilter 的 filter方法
* 和
* Ordered 的 getOrder方法
* GlobalFilter 的 filter方法 是过滤方法的具体实现
* Ordered 的 getOrder方法 允许用户指定过滤器的优先级
*/
public class RuiTokenFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 从请求中获取token 并校验
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
Object accessToken = request.getHeaders().getFirst("Authorization");
if (Objects.isNull(accessToken)){
return exchange.getResponse().setComplete();
}
String token = accessToken.toString();
if (!chckToken(token)) {
// 校验不通过直接抛出异常
throw new IllegalArgumentException("token非法");
}
// 继续执行后续的过滤器链
return chain.filter(exchange);
}
/**
* 假装校验以下token 实际项目中会结合Security框架做这件事
*
* @param token
* @return
*/
private boolean chckToken(String token) {
if (!token.startsWith("Bearer ")) {
return false;
} else {
String tokenStr = token.substring(7);
return tokenStr.equals("testToken");
}
}
@Override
public int getOrder() {
return -1;
}
}
2.gateWay中统一异常处理
实际项目开发中,我们会定义全局统一的返回对象,但是,gateWay中出现的一些错误情况会返回自己的异常状态,导致前端不方便处理,我们希望无论哪种报错,都返回前端我们统一的返回对象。
假定我们和前端约定的同一对象是如下结构:
{
code: 响应码, 200为正常,其他为异常,
msg: 信息
data: 响应数据
}
首先分析,经过gateWay的请求可能发生三种错误
a. 对应的服务没有找到(服务没有注册到nacos),这种错误我们返回前端时code设置为400,msg返回一个“服务未找到”
b.gateWayt正确转发,但是服务内部发生异常,这种情况我们返回前端code设置为500,msg返回一个“服务器内部异常”
c.其他错误,比如token错误导致的503之类,这种情况我们返回前端code设置为500,msg返回异常的具体内容
对于这种开发场景,就会使用到gateWay的异常处理器 ErrorWebExceptionHandler
使用方法如下:
/**
* 网关统一异常处理
*
* @author rui
*/
@Order(-1)
@Configuration
public class GatewayExceptionHandler implements ErrorWebExceptionHandler {
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex)
{
ServerHttpResponse response = exchange.getResponse();
if (exchange.getResponse().isCommitted())
{
return Mono.error(ex);
}
String msg;
int code = 500;
if (ex instanceof NotFoundException)
{
msg = "服务未找到";
code = 400;
}
else if (ex instanceof ResponseStatusException)
{
ResponseStatusException responseStatusException = (ResponseStatusException) ex;
msg = responseStatusException.getMessage();
}
else
{
msg = "内部服务器错误";
}
// 设置 response 状态为 200,前端正常接收后端返回
response.setStatusCode(HttpStatus.OK);
response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
// 我们和前端约定的返回内容设定为
R commonRes = R.fail(code, msg);
DataBuffer dataBuffer = response.bufferFactory().wrap(JSON.toJSONString(commonRes).getBytes());
return response.writeWith(Mono.just(dataBuffer));
}
}
3.gateWay中定义普通的controller接口
有一些请求,我们并不需要特定的服务去处理,比如验证码登录功能,获取验证码。类似这一类的需求,我们可以直接让gateWay项目来处理我们的请求。但是gateWay项目使用的不是SpringMvc,而是webFlux+reactive,所以,我们需要使用reactive的HandlerFunction来编写请求入口,使用方法如下:
step1:自定义一个HandlerFunction
@Component
public class RuiReactiveHandler implements HandlerFunction<ServerResponse> {
@Override
public Mono<ServerResponse> handle(ServerRequest request) {
return ServerResponse
.status(HttpStatus.OK)
.body(BodyInserters.fromValue("这是一个普通的Controller"));
}
}
step2:配置路由
/**
* 路由配置信息
*
* @author rui
*/
@Configuration
public class RouterFunctionConfiguration {
@Autowired
private RuiReactiveHandler ruiReactiveHandler;
@Bean
public RouterFunction<ServerResponse> routerFunction() {
return RouterFunctions.route(
RequestPredicates.GET("/callg")
.and(RequestPredicates.accept(MediaType.TEXT_PLAIN)), ruiReactiveHandler);
}
}
通过以上两步,就可以通过 {地址}/callg来访问RuiReactiveHandler 中的handle方法(这种写法其实就是webFlux+reactive的玩法,大家有兴趣可以了解一下webFlux+reactive)
四. gateWay面试常见问题
gateWay问的最多的其实就是用gateWay的场景,大家多理解以下章节三中我列出的拓展点,之类问题其实就比较好回答了。
另外,就是聊一聊gateWay的几个组件,route,predicate, filter。这部分内容在第二章,3小节有详细说明。
极少遇到问gateWay具体原理细节的,猜测可能是因为gateway的技术栈是webFlux, reactive,netty。懂的面试官可能比较少吧。如果对这一部分有兴趣的话,建议先看一下webflux和netty,不清楚这一部分内容的话可能会看的比较吃力。源码细节我忙完这阵子可能会在写一篇博客来分享,最近烂怂项目把人能忙死 T^T。最后祝每一位道友能遇到靠谱的产品经理。