深入理解SpringCloud之Gateway,小白都能看懂的保姆级教学
ztj100 2024-10-29 18:18 15 浏览 0 评论
虽然在服务网关有了zuul(在这里是zuul1),其本身还是基于servlet实现的,换言之还是同步阻塞方式的实现。就其本身来讲它的最根本弊端也是再此。而非阻塞带来的好处不言而喻,高效利用线程资源进而提高吞吐量,基于此Spring率先拿出针对于web的杀手锏,对,就是webflux。而Gateway本身就是基于webflux基础之上实现的。毕竟spring推出的技术,当然要得以推广嘛。不过就国内的软件公司而言为了稳定而选择保守,因此就这项技术的广度来说我本身还是在观望中。
1. Gateway快速上手
添加依赖:
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
这里请注意,springcloud-gateway是基于netty运行的环境,在servlet容器环境或者把它构建为war包运行的话是不允许的,因此在项目当中没有必要添加spring-boot-starter-web。在gateway当中有三个重要的元素他们分别是:
- Route 是最核心的路由元素,它定义了ID,目标URI ,predicates的集合与filter的集合,如果Predicate聚合返回真,则匹配该路由
- Predicate 基于java8的函数接口Predicate,其输入参数类型ServerWebExchange,其作用就是允许开发人员根据当前的http请求进行规则的匹配,比如说http请求头,请求时间等,匹配的结果将决定执行哪种路由
- Filter为GatewayFilter,它是由特殊的工厂构建,通过Filter可以在下层请求路由前后改变http请求与响应
我们编辑application.yaml,定义如下配置:
spring:
application:
name: gateway
cloud:
gateway:
routes:
- id: before_route
uri: http://www.baidu.com
predicates:
- Path=/baidu
server:
port: 8088
此时当我们访问路径中包含/baidu的,gateway将会帮我们转发至百度页面
2. 工作流程
在这里我贴上官网的一张图:
在这里我想结合源代码来说明其流程,这里面有个关键的类,叫RoutePredicateHandlerMapping,我们可以发现这个类有如下特点:
public class RoutePredicateHandlerMapping extends AbstractHandlerMapping {
// ....省略部分代码
@Override
protected Mono<?> getHandlerInternal(ServerWebExchange exchange) {
// don't handle requests on management port if set and different than server port
if (this.managementPortType == DIFFERENT && this.managementPort != null
&& exchange.getRequest().getURI().getPort() == this.managementPort) {
return Mono.empty();
}
exchange.getAttributes().put(GATEWAY_HANDLER_MAPPER_ATTR, getSimpleName());
return lookupRoute(exchange)
// .log("route-predicate-handler-mapping", Level.FINER) //name this
.flatMap((Function<Route, Mono<?>>) r -> {
exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);
if (logger.isDebugEnabled()) {
logger.debug(
"Mapping [" + getExchangeDesc(exchange) + "] to " + r);
}
exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r);
return Mono.just(webHandler);
}).switchIfEmpty(Mono.empty().then(Mono.fromRunnable(() -> {
exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);
if (logger.isTraceEnabled()) {
logger.trace("No RouteDefinition found for ["
+ getExchangeDesc(exchange) + "]");
}
})));
}
//...省略部分代码
}
- 此类继承了AbstractHandlerMapping,注意这里的是reactive包下的,也就是webflux提供的handlermapping,其作用等同于webmvc的handlermapping,其作用是将请求映射找到对应的handler来处理。
- 在这里处理的关键就是先寻找合适的route,关键的方法为lookupRoute():
protected Mono<Route> lookupRoute(ServerWebExchange exchange) {
return this.routeLocator.getRoutes()
// individually filter routes so that filterWhen error delaying is not a
// problem
.concatMap(route -> Mono.just(route).filterWhen(r -> {
// add the current route we are testing
exchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, r.getId());
return r.getPredicate().apply(exchange);
})
// instead of immediately stopping main flux due to error, log and
// swallow it
.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.<Route>empty().log("noroute"))
.next()
// TODO: error handling
.map(route -> {
if (logger.isDebugEnabled()) {
logger.debug("Route matched: " + route.getId());
}
validateRoute(route, exchange);
return route;
});
/*
* TODO: trace logging if (logger.isTraceEnabled()) {
* logger.trace("RouteDefinition did not match: " + routeDefinition.getId()); }
*/
}
- 其中RouteLocator的接口作用是获取Route定义,那么在GatewayAutoConfiguaration里有相关的配置,大家可自行查阅:
@Bean
public RouteLocator routeDefinitionRouteLocator(GatewayProperties properties,
List<GatewayFilterFactory> GatewayFilters,
List<RoutePredicateFactory> predicates,
RouteDefinitionLocator routeDefinitionLocator,
@Qualifier("webFluxConversionService") ConversionService conversionService) {
return new RouteDefinitionRouteLocator(routeDefinitionLocator, predicates,
GatewayFilters, properties, conversionService);
}
- 然后在注释add the current route we are testing处可以得到一个结论,其是根据Predicate的声明条件过滤出合适的Route
- 最终拿到FilteringWebHandler作为它的返回值,这个类是真正意义上处理请求的类,它实现了webflux提供的WebHandler接口:
public class FilteringWebHandler implements WebHandler {
//.....省略其它代码
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
//拿到当前的route
Route route = exchange.getRequiredAttribute(GATEWAY_ROUTE_ATTR);
//获取所有的gatewayFilter
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);
}
//....省略其它代码
}
在这里可以看到它的实际处理方式是委派给过滤器链进行处理请求操作的
3. Predicate
Spring Cloud Gateway包含许多内置的Predicate Factory。所有的Predicate都匹配HTTP请求的不同属性。如果配置类多个Predicate, 那么必须满足所有的predicate才可以,官网上列举的内置的Predicate,我在这里不做过多的说明,请大家参考:地址,predicate的实现可以在org.springframework.cloud.gateway.handler.predicate的包下找到。
3.1、自定义Predicate
先改一下application.yaml中的配置:
spring:
application:
name: gateway
cloud:
gateway:
routes:
- id: before_route
uri: http://www.baidu.com
predicates:
- Number=1
默认命名规则:名称RoutePredicateFactory,在这里我们可以看到如下代码规则用以解析Predicate的名称,该代码在NameUtils当中:
public static String normalizeRoutePredicateName(
Class<? extends RoutePredicateFactory> clazz) {
return removeGarbage(clazz.getSimpleName()
.replace(RoutePredicateFactory.class.getSimpleName(), ""));
}
那么在这里我们就按照如上规则建立对应的NumberRoutePredicateFactory,代码如下:
@Component
public class NumberRoutePredicateFactory extends AbstractRoutePredicateFactory<NumberRoutePredicateFactory.Config> {
public NumberRoutePredicateFactory() {
super(Config.class);
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("number");
}
@Override
public ShortcutType shortcutType() {
return ShortcutType.GATHER_LIST;
}
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return new GatewayPredicate() {
@Override
public boolean test(ServerWebExchange serverWebExchange) {
String number = serverWebExchange.getRequest().getQueryParams().getFirst("number");
return config.number == Integer.parseInt(number);
}
};
}
public static class Config {
private int number;
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
}
}
- 该类可以继承AbstractRoutePredicateFactory,同时需要注册为spring的Bean
- 在此类当中按照规范来讲,需要定义一个内部类,该类的作用用于封装application.yaml中的配置,Number=1这个配置会按照规则进行封装,这个规则由以下几项决定:ShortcutType,该值是枚举类型,分别是DEFAULT :按照shortcutFieldOrder顺序依次赋值GATHER_LIST:shortcutFiledOrder只能有一个值,如果参数有多个拼成一个集合GATHER_LIST_TAIL_FLAG:shortcutFiledOrder只能有两个值,其中最后一个值为true或者false,其余的值变成一个集合付给第一个值shortcutFieldOrder,这个值决定了Config中配置的属性,配置的参数都会被封装到该属性当中
4. Filter
Gateway中的filter可以分为(GlobalFilter)全局过滤器与普通过滤器,过滤器可以在路由到代理服务的前后改变请求与响应。在这里我会列举两个常见的filter给大家用作参考:
4.1、负载均衡的实现
与zuul类似,Gateway也可以作为服务端的负载均衡,那么负载均衡的处理关键就是与Ribbon集成,那么Gateway是利用GlobalFilter进行实现的,它的实现类是LoadBalancerClientFilter:
public class LoadBalancerClientFilter implements GlobalFilter, Ordered {
protected final LoadBalancerClient loadBalancer;
private LoadBalancerProperties properties;
//....
@Override
@SuppressWarnings("Duplicates")
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// preserve the original url
addOriginalRequestUrl(exchange, url);
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 uri = exchange.getRequest().getURI();
// if the `lb:<scheme>` mechanism was used, use `<scheme>` as the default,
// if the loadbalancer doesn't provide one.
//判断协议类型
String overrideScheme = instance.isSecure() ? "https" : "http";
if (schemePrefix != null) {
overrideScheme = url.getScheme();
}
//重构uri地址
URI requestUrl = loadBalancer.reconstructURI(
new DelegatingServiceInstance(instance, overrideScheme), uri);
//...
return chain.filter(exchange);
}
}
在这里我们可以看到这里它是基于Spring-Cloud-Commons规范里的LoadBalanceClient包装实现的。
4.2、集成Hystrix
Gateway同样也可以和Hystrix进行集成,这里面的关键类是HystrixGatewayFilterFactory,这里面的关键是RouteHystrixCommand该类继承了HystrixObservableCommand:
@Override
protected Observable<Void> construct() {
// 执行过滤器链
return RxReactiveStreams.toObservable(this.chain.filter(exchange));//1
}
@Override
protected Observable<Void> resumeWithFallback() {
if (this.fallbackUri == null) {
return super.resumeWithFallback();
}
// TODO: copied from RouteToRequestUrlFilter
URI uri = exchange.getRequest().getURI();
// TODO: assume always?
boolean encoded = containsEncodedParts(uri);
URI requestUrl = UriComponentsBuilder.fromUri(uri).host(null).port(null)
.uri(this.fallbackUri).scheme(null).build(encoded).toUri();//2
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
addExceptionDetails();
ServerHttpRequest request = this.exchange.getRequest().mutate()
.uri(requestUrl).build();
ServerWebExchange mutated = exchange.mutate().request(request).build();
return RxReactiveStreams.toObservable(getDispatcherHandler().handle(mutated));//3
}
- 在代码1处会执行滤器链,写到此处的代码会被统一加上hystrix的保护
- 在代码2处再是执行回退的方法,根据fallbackUri构建一个回退请求地址
- 在代码3处获取WebFlux的总控制器DispatcherHandler进行回退地址的处理
5、服务发现
服务发现对于Gateway来说也是个非常重要的内容,Gateway在这里定义了一个核心接口叫做:RouteDefinitionLocator,这个接口用于获取Route的定义,服务发现的机制实现了该接口:
public class DiscoveryClientRouteDefinitionLocator implements RouteDefinitionLocator {
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
//....省略部分代码
return Flux.fromIterable(discoveryClient.getServices())//获取所有服务
.map(discoveryClient::getInstances) //映射转换所有服务实例
.filter(instances -> !instances.isEmpty()) //过滤出不为空的服务实例
.map(instances -> instances.get(0)).filter(includePredicate)//根据properites里的include表达式过滤实例
.map(instance -> {
/*
构建Route的定义
*/
String serviceId = instance.getServiceId();
RouteDefinition routeDefinition = new RouteDefinition();
routeDefinition.setId(this.routeIdPrefix + serviceId);
String uri = urlExpr.getValue(evalCtxt, instance, String.class);
routeDefinition.setUri(URI.create(uri));
final ServiceInstance instanceForEval = new DelegatingServiceInstance(
instance, properties);
//添加Predicate
for (PredicateDefinition original : this.properties.getPredicates()) {
PredicateDefinition predicate = new PredicateDefinition();
predicate.setName(original.getName());
for (Map.Entry<String, String> entry : original.getArgs()
.entrySet()) {
String value = getValueFromExpr(evalCtxt, parser,
instanceForEval, entry);
predicate.addArg(entry.getKey(), value);
}
routeDefinition.getPredicates().add(predicate);
}
//添加filter
for (FilterDefinition original : this.properties.getFilters()) {
FilterDefinition filter = new FilterDefinition();
filter.setName(original.getName());
for (Map.Entry<String, String> entry : original.getArgs()
.entrySet()) {
String value = getValueFromExpr(evalCtxt, parser,
instanceForEval, entry);
filter.addArg(entry.getKey(), value);
}
routeDefinition.getFilters().add(filter);
}
return routeDefinition;
});
}
}
由此我们可以知道,这里面利用DiscoveryClient获取所有的服务实例并将每个实例构建为一个Route,不过在此之前,在自动装配的类GatewayDiscoveryClientAutoConfiguration里已经配置了默认的Predicate与Filter,它会预先帮我们配置默认的Predicate与Filter:
public static List<PredicateDefinition> initPredicates() {
ArrayList<PredicateDefinition> definitions = new ArrayList<>();
// TODO: add a predicate that matches the url at /serviceId?
// add a predicate that matches the url at /serviceId/**
PredicateDefinition predicate = new PredicateDefinition();
predicate.setName(normalizeRoutePredicateName(PathRoutePredicateFactory.class));
predicate.addArg(PATTERN_KEY, "'/'+serviceId+'/**'");
definitions.add(predicate);
return definitions;
}
public static List<FilterDefinition> initFilters() {
ArrayList<FilterDefinition> definitions = new ArrayList<>();
// add a filter that removes /serviceId by default
FilterDefinition filter = new FilterDefinition();
filter.setName(normalizeFilterFactoryName(RewritePathGatewayFilterFactory.class));
String regex = "'/' + serviceId + '/(?<remaining>.*)'";
String replacement = "'/${remaining}'";
filter.addArg(REGEXP_KEY, regex);
filter.addArg(REPLACEMENT_KEY, replacement);
definitions.add(filter);
return definitions;
}
这里面主要会根据ServiceId构建为 Path=/serviceId/**的Predicate和路由至对应服务前把ServiceId去掉的filter
6、总结
根据上述说明,我仅仅选取了两个比较典型意义的Predicate与Filter代码进行说明,由于官网上没有说明自定义Predicate,我在这里索性写了个简单的例子,那么自定义Filter的例子可以参考官网地址:
这里需要吐槽一下官方 什么时候能把TODO补充完整的呢?
Gateway是基于Webflux实现的,它通过扩展HandlerMapping与WebHandler来处理用户的请求,先通过Predicate定位到Router然后在经过FilterChain的过滤处理,最后定位到下层服务。同时官方给我们提供了许多Prdicate与Filter,比如说限流的。从这点来说它的功能比zuul还强大呢,zuul里有的服务发现,断路保护等,Gateway分别通过GlobalFilter与Filter来实现。
最后至于Gateway能普及到什么样的程度,亦或者能不能最终成为统一的网关标准,这个我也不能再这里有所保证,那么就交给时间来证明吧。
相关推荐
- 从IDEA开始,迈进GO语言之门(idea got)
-
前言笔者在学习GO语言编程的时候,GO语言在国内还没有像JAVA/Php/Python那样普及,绕了不少的弯路,要开始入门学习一门编程语言,最好就先从选择一个好的编程语言的开发环境开始,有了这个开发环...
- 基于SpringBoot+MyBatis的私人影院java网上购票jsp源代码Mysql
-
本项目为前几天收费帮学妹做的一个项目,JavaEEJSP项目,在工作环境中基本使用不到,但是很多学校把这个当作编程入门的项目来做,故分享出本项目供初学者参考。一、项目介绍基于SpringBoot...
- 基于springboot的个人服装管理系统java网上商城jsp源代码mysql
-
本项目为前几天收费帮学妹做的一个项目,JavaEEJSP项目,在工作环境中基本使用不到,但是很多学校把这个当作编程入门的项目来做,故分享出本项目供初学者参考。一、项目介绍基于springboot...
- 基于springboot的美食网站Java食品销售jsp源代码Mysql
-
本项目为前几天收费帮学妹做的一个项目,JavaEEJSP项目,在工作环境中基本使用不到,但是很多学校把这个当作编程入门的项目来做,故分享出本项目供初学者参考。一、项目介绍基于springboot...
- 贸易管理进销存springboot云管货管账分析java jsp源代码mysql
-
本项目为前几天收费帮学妹做的一个项目,JavaEEJSP项目,在工作环境中基本使用不到,但是很多学校把这个当作编程入门的项目来做,故分享出本项目供初学者参考。一、项目描述贸易管理进销存spring...
- SpringBoot+VUE员工信息管理系统Java人员管理jsp源代码Mysql
-
本项目为前几天收费帮学妹做的一个项目,JavaEEJSP项目,在工作环境中基本使用不到,但是很多学校把这个当作编程入门的项目来做,故分享出本项目供初学者参考。一、项目介绍SpringBoot+V...
- 目前见过最牛的一个SpringBoot商城项目(附源码)还有人没用过吗
-
帮粉丝找了一个基于SpringBoot的天猫商城项目,快速部署运行,所用技术:MySQL,Druid,Log4j2,Maven,Echarts,Bootstrap...免费给大家分享出来前台演示...
- SpringBoot+Mysql实现的手机商城附带源码演示导入视频
-
今天为大家带来的是基于SpringBoot+JPA+Thymeleaf框架的手机商城管理系统,商城系统分为前台和后台、前台用的是Bootstrap框架后台用的是SpringBoot+JPA都是现在主...
- 全网首发!马士兵内部共享—1658页《Java面试突击核心讲》
-
又是一年一度的“金九银十”秋招大热门,为助力广大程序员朋友“面试造火箭”,小编今天给大家分享的便是这份马士兵内部的面试神技——1658页《Java面试突击核心讲》!...
- SpringBoot数据库操作的应用(springboot与数据库交互)
-
1.JDBC+HikariDataSource...
- SpringBoot 整合 Flink 实时同步 MySQL
-
1、需求在Flink发布SpringBoot打包的jar包能够实时同步MySQL表,做到原表进行新增、修改、删除的时候目标表都能对应同步。...
- SpringBoot + Mybatis + Shiro + mysql + redis智能平台源码分享
-
后端技术栈基于SpringBoot+Mybatis+Shiro+mysql+redis构建的智慧云智能教育平台基于数据驱动视图的理念封装element-ui,即使没有vue的使...
- Springboot+Mysql舞蹈课程在线预约系统源码附带视频运行教程
-
今天发布的是由【猿来入此】的优秀学员独立做的一个基于springboot脚手架的Springboot+Mysql舞蹈课程在线预约系统,系统项目源代码在【猿来入此】获取!https://www.yuan...
- SpringBoot+Mysql在线众筹系统源码+讲解视频+开发文档(参考论文
-
今天发布的是由【猿来入此】的优秀学员独立做的一个基于springboot脚手架的在线众筹管理系统,主要实现了普通用户在线参与众筹基本操作流程的全部功能,系统分普通用户、超级管理员等角色,除基础脚手架外...
- Docker一键部署 SpringBoot 应用的方法,贼快贼好用
-
这两天发现个Gradle插件,支持一键打包、推送Docker镜像。今天我们来讲讲这个插件,希望对大家有所帮助!GradleDockerPlugin简介...
你 发表评论:
欢迎- 一周热门
- 最近发表
-
- 从IDEA开始,迈进GO语言之门(idea got)
- 基于SpringBoot+MyBatis的私人影院java网上购票jsp源代码Mysql
- 基于springboot的个人服装管理系统java网上商城jsp源代码mysql
- 基于springboot的美食网站Java食品销售jsp源代码Mysql
- 贸易管理进销存springboot云管货管账分析java jsp源代码mysql
- SpringBoot+VUE员工信息管理系统Java人员管理jsp源代码Mysql
- 目前见过最牛的一个SpringBoot商城项目(附源码)还有人没用过吗
- SpringBoot+Mysql实现的手机商城附带源码演示导入视频
- 全网首发!马士兵内部共享—1658页《Java面试突击核心讲》
- SpringBoot数据库操作的应用(springboot与数据库交互)
- 标签列表
-
- idea eval reset (50)
- vue dispatch (70)
- update canceled (42)
- order by asc (53)
- spring gateway (67)
- 简单代码编程 贪吃蛇 (40)
- transforms.resize (33)
- redisson trylock (35)
- 卸载node (35)
- np.reshape (33)
- torch.arange (34)
- node卸载 (33)
- npm 源 (35)
- vue3 deep (35)
- win10 ssh (35)
- exceptionininitializererror (33)
- vue foreach (34)
- idea设置编码为utf8 (35)
- vue 数组添加元素 (34)
- std find (34)
- tablefield注解用途 (35)
- python str转json (34)
- java websocket客户端 (34)
- tensor.view (34)
- java jackson (34)