本篇将开始初步认识gateway 网关的使用。网关也就是服务的边界,当任意请求发送到微服务项目上的时候,需要有一个网关来简单初步处理这些请求,比如:鉴权,然后再通过断言规则路由到各个不同的服务上,如果项目中有服务发现的中间件,比如nacos、zk,那么还需要负载选择之后,再路由到不同服务器上的服务。
同时要注意gateway 的项目本身也是一个微服务,它同样可以集群多节点部署,所以当gateway 项目集群部署的时候,网络请求的第一个到达点就不是gateway 了,而是如Nginx 之类的中间件,所以先是对应的中间件先进行负载选择到gateway 项目节点之后,才是gateway 的表演时间。
在gateway 的正式使用之前,我们简单说一下gateway 的基础概念,先弄清楚gateway 的作用是什么,为什么项目中要加入gateway。
Spring Cloud Gateway 是Spring Cloud 团队的一个全新项目,基于Spring5.0、SpringBoot2.0、Project Reactor 等技术开发的网关。旨在为微服务架构提供一种简单有效统一的API路由管理方式。
Spring Cloud Gateway 作为SpringCloud 生态系统中的网关,目标是替代Netflix Zuul。Gateway 不仅提供统一路由方式,并且基于Filter 链的方式提供网关的基本功能。例如:安全,监控/指标,和限流。
微服务网关就是一个系统,通过暴露该微服务网关系统,方便我们进行相关的鉴权,安全控制,日志统一处理,易于监控,限流等相关功能。
这个补充一点:因为所有的客户端请求过来时,都是先进过gateway 网关才能访问到对应的各个微服务,所以gateway 本身的服务要求就比较高,一定是要高性能的,其中的业务逻辑也是一定要少,这样才能做到高响应,同时后面源码分析的时候我们也可以知道gateway 也不是Tomcat 之类的容器来启动的,而是netty 来做消息之间的交互。
这里先简单说一下gateway 的工作原理
从上述的工作原理中,不难看出gateway 最只要的两块内容就是路由和Filter。对于这两块,下面做一下简单的演示,因为这里的内容只要大致知道怎么配置即可,后续的源码分析流程会详谈这些功能的具体效果。
在正式项目中gateway 的路由就是它最核心的功能,因为各个不同客户端的请求,在经过gateway 之后都会分到不同的微服务中,甚至根据请求方式的不一样,同样的请求内容也可以分到不同的微服务中。
gateway 的路由也分为两种配置方式,基于配置文件的静态路由设置和基于代码的动态路由设置,这两种都各有特点,但是本质其实都是通过gateway 本身的代码逻辑来实现的。
下面分别实现以下这两种配置的情况。
因为是spring cloud 项目,所以基本上都是配置在application.yml,或者bootstrap.yml 中,这两者没什么太大的区别,就是后者的执行时机要早于application.yml。
spring:
cloud:
gateway:
routes:
- id: hailtaxi-driver
uri: lb://hailtaxi-deriver
predicates:
- Path=/driver/**
上面就是一个简单的路由断言配置,我们自己如果要独立配置的话,其实可以点击routes
,这里会导到GatewayProperties
这个类,这里有一个属性是routes
,对应的对象是RouteDefinition
,definition 其实就可以想到这是一个配置信息对象,跟进去就可以看到它的一些属性,这些就是我们要配置的内容,其中有两个不为空的属性是一定要配置的,uri
和predicates
。
private String id;
@NotEmpty
@Valid
private List<PredicateDefinition> predicates = new ArrayList<>();
@Valid
private List<FilterDefinition> filters = new ArrayList<>();
@NotNull
private URI uri;
private Map<String, Object> metadata = new HashMap<>();
private int order = 0;
predicates
的配置,我在上面的配置文件内容中只有一个- Path
,但是这里是有很多内容的,可以跟着官方提供出来的文档 一步一步配置,比如:监听请求头的- Header
、监听Cookie 的- Cookie
等等很多,这里就不一个个配置了。
**这里总结一下:**其实总来的说配置文件的设置相对来说简单一点,因为具体的内容都是gateway 自己实现的,我们就是通过一些参数配置来构建出对应的routes。
这里如果要实现动态的配置,我具体没有操作过,但是应该是可以结合nacos 的配置中心来实现的,因为nacos 的配置中心文件有改动时,是有事件发布给spring 的事件多播器的,这样就可以做到动态刷新。
上面说了基于配置文件的设置,接着说的就是代码的设置了,还有这个为什么要说是动态的,原因就是可以将配置放在数据库中,用实时查询来做,我们这里就不配置数据库了,直接上代码。
@Configuration
public class RouterConfig {
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("hailtaxi-driver", r -> r
.path("/driver/**")
.uri("lb://hailtaxi-driver"))
.build();
}
}
解析一下:其实也很简单,这里就是将一个RouteLocator 接口的实现对象放到了容器中,上面说gateway 工作原理的时候就说过,它有一个RoutePredicateHandlerMapping 对象来匹配断言规则,其实就是通过RouteLocator 的注入实现类来判断匹配。
这里的实现类对象是通过RouteLocatorBuilder 对象来build 构建的,具体的写法就是跟我上面的一样,还有一点就是上面的.path
方法跟配置文件中的- Path
效果是一致的,跟着这个方法可以对应的PredicateSpec 对象中还有如:before
、cookie
之类的方法。
**这里再总结一下:**代码设置我感觉跟配置文件配置差不多,它所谓的结合数据库查询实现动态配置,我也不是很建议,因为gateway 要求的是一个高性能、高响应的服务,数据库的查询虽然说不慢,但是还是会有影响,所以不建议。一定要做到动态配置的话,也可以结合能动态刷新配置的配置中心,比如说阿里的nacos,还有携程的Apollo。
gateway 有意思的点就是filter 它也提供出了两种不同的配置方式,GatewayFilter 接口和GlobalFilter 接口,我们通过实现这两个接口来配置filter。
它们两者的区别在于:
**小结一下:**过滤器作为Gateway 的重要功能。常用于请求鉴权、服务调用时长统计、修改请求或响应header、限流、去除路径等等。它提供出的两种实现方式,区别也就是一个全局的,一个是单个路由的,具体的可以参考官方的文档。
下面我们手动配置一个自己的两个不同类型的filter,先来GatewayFilter。
如果我们这是第一次上手写这个GatewayFilter,那么其实可以在上面官方提供的GatewayFilter 中随便找一个看看,仿照还是可一个嘛。比如这个HystrixGatewayFilterFactory,注意命名规则,这里几乎所有的都加了GatewayFilterFactory 的尾缀,而且有点可以看到真正实现的也不是GatewayFilter,而是GatewayFilterFactory。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yCSVQxEl-1683883705387)(C:\Users\liwqsh\AppData\Roaming\Typora\typora-user-images\image-20230428155135518.png)]
现在我们才开始手写代码,可以注意到是继承了AbstractGatewayFilterFactory 对象,然后重写了apply 方法,但是注意这个返回其实还是一个GatewayFilter 的实现。
我这里在GatewayFilter 的filter 方法中是没有做任何操作的,但是鉴权,或者日志输出之类的都是在部分代码中,ServerWebExchange 中就存在所有的请求信息。
@Component
public class MyGatewayFilterFactory extends AbstractGatewayFilterFactory {
@Override
public GatewayFilter apply(Object config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 调用chain.filter 继续向下游执行
return chain.filter(exchange);
}
};
}
}
接下来就是bootstrap.yml 的配置了,当然如果是代码设置的断言规则,就是在代码中配置,我这里就用yml 就行了。在上面配置路由断言规则的时候,提倒的RouteDefinition
对象中有一个filters
的属性,这个里面就是放GatewayFilter。
spring:
cloud:
gateway:
routes:
- id: hailtaxi-driver
uri: lb://hailtaxi-deriver
filters:
- My
predicates:
- Path=/driver/**
**小结:**这样就配置好了一个GatewayFilter 的内容,但是一般情况下很少配置这个,因为gateway 已经提供出了很全面的这类filter,再者鉴权之类的内容都是放在全局的,没有必要放在这类filter 里面,所以这个只要大致了解就行。
这里跟上面就不一样了GlobalFilter 我们是直接实现的GlobalFilter 接口,然后将实现类注入到spring 的IOC 里面即可。
@Component
@Slf4j
public class MyGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("MyGlobalFilter-----");
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -1;
}
}
**小结:**相对于GatewayFilter 而言,GlobalFilter 的配置更加简单,因为是全局的,所以也不需要额外的配置,就是实现接口,然后注入IOC 容器,剩下的交给gateway 和spring 就行了。
**总结一下上述的使用内容:**到这里其实已经大致说完了gateway 的基本使用和配置,至于它的鉴权、跨域、限流配置的代码内容,有兴趣的可以自己去看下,这里就不多说了。gateway 的配置和编辑过程中,一定要注意的就是保证它的高性能和高响应,就是因为它是网关,要是请求在这里卡住了,后面的服务就不用再说了。
上面上述内容中,其实一开始就已经概述了gateway 的源码流程,gateway 的工作原理其实就是它的流程,我们跟代码的话,也是跟着这部分内容。
这里有一部是关于spring MVC 的内容,这里就不赘述了,所以下面直接就从DispatcherHandler 开始切入,我们要待着目的看源码,从上面的原理描述中,可以知道DispatcherHandler 中的目的是找到RoutePredicateHandlerMapping,具体切入代码看下。
public class DispatcherHandler implements WebHandler, ApplicationContextAware
DispatcherHandler 这里是实现了WebHandler,根据spring MVC 的逻辑,它一定会重写handler 方法,我们直接看它的handler 方法。
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
if (this.handlerMappings == null) {
return createNotFoundError();
}
return Flux.fromIterable(this.handlerMappings)
.concatMap(mapping -> mapping.getHandler(exchange))
.next()
.switchIfEmpty(createNotFoundError())
.flatMap(handler -> invokeHandler(exchange, handler))
.flatMap(result -> handleResult(exchange, result));
}
这部分handler 的代码,我们分三部分看,第一部分mapping.getHandler
,找到对应的handlerMapping,第二部分invokeHandler
,执行对应的handlerMapping,handleResult
处理返回结果。
既然我们已经知道这里要找的handlerMapping 就是RoutePredicateHandlerMapping,那么直接切入这边代码看。
public class RoutePredicateHandlerMapping extends AbstractHandlerMapping
这里是继承了AbstractHandlerMapping,但是这个对象还是实现了HandlerMapping,所以可以直接找到它的getHandler 方法,但是这里显示的是return getHandlerInternal(exchange).map....
,所以我们看RoutePredicateHandlerMapping 的方法就是它重写父类的getHandlerInternal
方法。
这个方法里面,我们重点找到的是lookupRoute(exchange)
方法的调用,代码直接切入。
这里其实就是根据this.routeLocator.getRoutes()
获取到我们配置的断言规则,也就是我们之前配置了的所有断言规则,然后通过下面的apply
方法就行断言。
当匹配成功之后,这里就会返回一个FilteringWebHandler 的对象,然后回到在DispatcherHandler 中执行invokeHandler
的方法,这里面也就是调用对应WebHandler 的handler 方法return handlerAdapter.handle(exchange, handler);
,这里可以直接切入到对应的FilteringWebHandler 对应的handler 方法中。
protected Mono<Route> lookupRoute(ServerWebExchange exchange) {
// 拿到所有的route
return this.routeLocator.getRoutes()
.concatMap(route -> Mono.just(route).filterWhen(r -> {
// add the current route we are testing
exchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, r.getId());
// 进行路由断言 Predicate
return r.getPredicate().apply(exchange);
})
.doOnError(e -> logger.error(
"Error applying predicate for route: " + route.getId(),
e))
.onErrorResume(e -> Mono.empty()))
// .defaultIfEmpty() put a static Route not found
// or .switchIfEmpty()
// .switchIfEmpty(Mono.empty().log("noroute"))
.next()
// TODO: error handling
.map(route -> {
if (logger.isDebugEnabled()) {
logger.debug("Route matched: " + route.getId());
}
validateRoute(route, exchange);
return route;
});
}
注意啊这里不是直接进入的FilteringWebHandler,而是通过SimpleHandlerAdapter 对象,然后调用WebHandler 的实现类的handler 方法。
@Override
public Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler) {
WebHandler webHandler = (WebHandler) handler;
Mono<Void> mono = webHandler.handle(exchange);
return mono.then(Mono.empty());
}
到这里,我们再切入FilteringWebHandler 的handler 方法代码。
这里首先是combined 集合的构建,它包含了所有的GlobalFilter 和GatewayFilter,然后将集合重新排序,构建一个DefaultGatewayFilterChain 对象,调用它的filter 方法。
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
Route route = exchange.getRequiredAttribute(GATEWAY_ROUTE_ATTR);
List<GatewayFilter> gatewayFilters = route.getFilters();
List<GatewayFilter> combined = new ArrayList<>(this.globalFilters);
combined.addAll(gatewayFilters);
// TODO: needed or cached?
AnnotationAwareOrderComparator.sort(combined);
if (logger.isDebugEnabled()) {
logger.debug("Sorted gatewayFilterFactories: " + combined);
}
return new DefaultGatewayFilterChain(combined).filter(exchange);
}
这里就需要提到一个设计模式:责任链模式,这个在spring 的aop 中其实就有体现,优点就是调用简单,在当前代码调用完成之后,在最后一行代码中会调用下一层代码逻辑,一直往复,单个filter 也只做一件事情,目的明确。缺点也明显:可读行较差,项目重构时会非常麻烦。
@Override
public Mono<Void> filter(ServerWebExchange exchange) {
return Mono.defer(() -> {
if (this.index < filters.size()) {
GatewayFilter filter = filters.get(this.index);
DefaultGatewayFilterChain chain = new DefaultGatewayFilterChain(this,
this.index + 1);
return filter.filter(exchange, chain);
}
else {
return Mono.empty(); // complete
}
});
}
后面就是filter 的逻辑调用了,这里涉及到filter 除了自己的,其余的官方给出的,我们这里就只需要关注三个具体的filter 即可,RouteToRequestUrlFilter、LoadBalancerClientFilter、NettyRoutingFilter,这三个都属于GlobalFilter,全局的,所有的路由都是要走着三个的,具体执行的顺序也就是这个。
我们下面一个个看,先看RouteToRequestUrlFilter,上面也说了这个写都是GlobalFilter 的实现类,所以我们要关注的就是这对象的filter 方法就行。
这里的代码虽然多,但是目的非常明确,就是根据断言的内容,还有请求的内容,获取到具体的URL,在下面代码中URI mergedUrl
属性的获取就能看出,获得之后,会将URL 放置到exchange 里面,最后传给下层filter,但是需要注意:这里的URL 是存在断言信息的,不是正在的URL。
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR); //获取当前的route
if (route == null) {
return chain.filter(exchange);
}
log.trace("RouteToRequestUrlFilter start");
//得到uri = http://localhost:8001/driver/info/1?token=123456
URI uri = exchange.getRequest().getURI();
boolean encoded = containsEncodedParts(uri);
URI routeUri = route.getUri(); // lb://hailtaxi-driver
if (hasAnotherScheme(routeUri)) {
// this is a special url, save scheme to special attribute
// replace routeUri with schemeSpecificPart
exchange.getAttributes().put(GATEWAY_SCHEME_PREFIX_ATTR,
routeUri.getScheme());
routeUri = URI.create(routeUri.getSchemeSpecificPart());
}
if ("lb".equalsIgnoreCase(routeUri.getScheme()) && routeUri.getHost() == null) {
// Load balanced URIs should always have a host. If the host is null it is
// most
// likely because the host name was invalid (for example included an
// underscore)
throw new IllegalStateException("Invalid host: " + routeUri.toString());
}
//将uri换成 lb://hailtaxi-driver/driver/info/1?token=123456
URI mergedUrl = UriComponentsBuilder.fromUri(uri)
// .uri(routeUri)
.scheme(routeUri.getScheme()).host(routeUri.getHost())
.port(routeUri.getPort()).build(encoded).toUri();
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, mergedUrl);
// 继续下一个过滤器
return chain.filter(exchange);
}
接着看LoadBalancerClientFilter 的filter 方法。
这里首先就是获取上层filter 得到的URL,然后判断URL 是否符合断言后的信息,然后重点就是获取到负载均衡的实例:final ServiceInstance instance = choose(exchange)
,这里如果没有特定配置的话,就是RibbonServer 的实现RibbonLoadBalancerClient 对象。
最后会根据负载均衡的对象去获取正在的URL,也就是URI requestUrl
属性的赋值,然后还是将URL 放在exchange 里面,最后传给下层filter。
@Override
@SuppressWarnings("Duplicates")
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR); // url:lb://hailtaxi-driver/driver/info/1?token=123456
String schemePrefix = exchange.getAttribute(GATEWAY_SCHEME_PREFIX_ATTR);
if (url == null
|| (!"lb".equals(url.getScheme()) && !"lb".equals(schemePrefix))) {
return chain.filter(exchange);
}
// preserve the original url
addOriginalRequestUrl(exchange, url);
if (log.isTraceEnabled()) {
log.trace("LoadBalancerClientFilter url before: " + url);
}
// 负载均衡选择服务实例
final ServiceInstance instance = choose(exchange);
if (instance == null) {
throw NotFoundException.create(properties.isUse404(),
"Unable to find instance for " + url.getHost());
}
//用户提交的URI = http://localhost:8001/driver/info/1?token=123456
URI uri = exchange.getRequest().getURI();
// if the `lb:` mechanism was used, use `` as the default,
// if the loadbalancer doesn't provide one.
String overrideScheme = instance.isSecure() ? "https" : "http";
if (schemePrefix != null) {
overrideScheme = url.getScheme();
}
// 真正要请求的url = http://172.16.17.251:18081/driver/info/1?token=123456
URI requestUrl = loadBalancer.reconstructURI(
new DelegatingServiceInstance(instance, overrideScheme), uri);
if (log.isTraceEnabled()) {
log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);
}
// 将真正要请求的url设置到上下文中
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
return chain.filter(exchange);
}
最后的一个NettyRoutingFilter,这个的详细代码就不用看,从类名就可以看出来,这里就是封装了Netty 调用的地方。这里就能体现出来Gateway 的底层不是用Tomcat 进行请求的,而且Netty,这样才能做到一个高响应的框架。
上述基本上就是gateway 的简单实用及大体的运行流程,其实可以看出gateway 的重点就是Route 断言规则、Filter 过滤责任链、以及最后的Netty 请求转发调用。
还有一点,gateway 是基于spring MVC 使用的,gateway 的filter 的调用是通过spring MVC 提供的handler 来调用,所以gateway 使用的第一点就是找到gateway 的FilteringWebHandler 对象,然后才会有后面的事情。
;
return chain.filter(exchange);
}
最后的一个NettyRoutingFilter,这个的详细代码就不用看,从类名就可以看出来,这里就是封装了Netty 调用的地方。这里就能体现出来Gateway 的底层不是用Tomcat 进行请求的,而且Netty,这样才能做到一个高响应的框架。
## 总结
上述基本上就是gateway 的简单实用及大体的运行流程,其实可以看出gateway 的重点就是Route 断言规则、Filter 过滤责任链、以及最后的Netty 请求转发调用。
还有一点,gateway 是基于spring MVC 使用的,gateway 的filter 的调用是通过spring MVC 提供的handler 来调用,所以gateway 使用的第一点就是找到gateway 的FilteringWebHandler 对象,然后才会有后面的事情。