在上一篇文章中,我们了解了 Spring Cloud Gateway 作为网关所具备的基础功能:路由。本篇我们将关注它的另一个功能:过滤器。

Spring Cloud Gateway 已经内置了很多实用的过滤器,但并不能完全满足我们的需求。本文我们就来实现自定义过滤器。虽然现在 Spring Cloud Gateway 的文档还不完善,但是我们依旧可以照猫画虎来定制自己的过滤器。

Filter 的作用

其实前边在介绍 Zuul 的的时候已经介绍过 Zuul 的 Filter 的作用了,同作为网关服务,Spring Cloud Gateway 的 Filter 作用也类似。

这里就简单用两张图来解释一下吧。

当使用微服务构建整个 API 服务时,一般有许多不同的应用在运行,如上图所示的mst-user-servicemst-good-servicemst-order-service,这些服务都需要对客户端的请求的进行 Authentication。最简单粗暴的方法就是像上图一样,为每个微服务应用都实现一套用于校验的过滤器或拦截器。

对于这样的问题,更好的做法是通过前置的网关服务来完成这些非业务性质的校验,就像下图

Filter 的生命周期

Spring Cloud Gateway 的 Filter 的生命周期不像 Zuul 的那么丰富,它只有两个:“pre” 和 “post”。

“pre”和 “post” 分别会在请求被执行前调用和被执行后调用,和 Zuul Filter 或 Spring Interceptor 中相关生命周期类似,但在形式上有些不一样。

Zuul 的 Filter 是通过filterType()方法来指定,一个 Filter 只能对应一种类型,要么是 “pre” 要么是“post”。Spring Interceptor 是通过重写HandlerInterceptor中的三个方法来实现的。而 Spring Cloud Gateway 基于 Project Reactor 和 WebFlux,采用响应式编程风格,打开它的 Filter 的接口GatewayFilter你会发现它只有一个方法filter

仅通过这一个方法,怎么来区分是 “pre” 还是 “post” 呢?我们下边就通过自定义过滤器来看看。

自定义过滤器

现在假设我们要统计某个服务的响应时间,我们可以在代码中

long beginTime = System.currentTimeMillis();
// do something...
long elapsed = System.currentTimeMillis() - beginTime;
log.info("elapsed: {}ms", elapsed);

自定义过滤器需要实现GatewayFilterOrdered。其中GatewayFilter中的这个方法就是用来实现你的自定义的逻辑的每次都要这么写是不是很烦?Spring 告诉我们有个东西叫 AOP。但是我们是微服务啊,在每个服务里都写也很烦。这时候就该网关的过滤器登台表演了。

Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);

好了,让我们来撸代码吧而Ordered中的int getOrder()方法是来给过滤器设定优先级别的,值越大则优先级越低。

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;public class ElapsedFilter implements GatewayFilter, Ordered {private static final Log log = LogFactory.getLog(GatewayFilter.class);private static final String ELAPSED_TIME_BEGIN = "elapsedTimeBegin";@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {exchange.getAttributes().put(ELAPSED_TIME_BEGIN, System.currentTimeMillis());return chain.filter(exchange).then(Mono.fromRunnable(() -> {Long startTime = exchange.getAttribute(ELAPSED_TIME_BEGIN);if (startTime != null) {log.info(exchange.getRequest().getURI().getRawPath() + ": " + (System.currentTimeMillis() - startTime) + "ms");}}));}@Overridepublic int getOrder() {return Ordered.LOWEST_PRECEDENCE;}
}

我们在请求刚刚到达时,往ServerWebExchange中放入了一个属性elapsedTimeBegin,属性值为当时的毫秒级时间戳。然后在请求执行结束后,又从中取出我们之前放进去的那个时间戳,与当前时间的差值即为该请求的耗时。因为这是与业务无关的日志所以将Ordered设为Integer.MAX_VALUE以降低优先级。

现在再来看我们之前的问题:怎么来区分是 “pre” 还是 “post” 呢?其实就是chain.filter(exchange)之前的就是 “pre” 部分,之后的也就是then里边的是 “post” 部分。

创建好 Filter 之后我们将它添加到我们的 Filter Chain 里边

@Bean
public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) {// @formatter:offreturn builder.routes().route(r -> r.path("/fluent/customer/**").filters(f -> f.stripPrefix(2).filter(new ElapsedFilter()).addResponseHeader("X-Response-Default-Foo", "Default-Bar")).uri("lb://CONSUMER").order(0).id("fluent_customer_service")).build();// @formatter:on
}

现在再尝试访问 http://localhost:10000/customer/hello/windmt 即可在控制台里看到请求路径与对应的耗时

2018-05-08 16:07:04.197  INFO 83726 --- [ctor-http-nio-4] o.s.cloud.gateway.filter.GatewayFilter   : /hello/windmt: 40ms

实际在使用 Spring Cloud 的过程中,我们会使用 Sleuth+Zipkin 来进行耗时分析。

自定义全局过滤器

前边讲了自定义的过滤器,那个过滤器只是局部的,如果我们有多个路由就需要一个一个来配置,并不能通过像下面这样来实现全局有效(也未在 Fluent Java API 中找到能设置 defaultFilters 的方法)

@Bean
public ElapsedFilter elapsedFilter(){return new ElapsedFilter();
}

这在我们要全局统一处理某些业务的时候就显得比较麻烦,比如像最开始我们说的要做身份校验,有没有简单的方法呢?这时候就该全局过滤器出场了。

有了前边的基础,我们创建全局过滤器就简单多了。只需要把实现的接口GatewayFilter换成GlobalFilter,就完事大吉了。比如下面的 Demo 就是从请求参数中获取token字段,如果能获取到就 pass,获取不到就直接返回401错误,虽然简单,但足以说明问题了。

import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;public class TokenFilter implements GlobalFilter, Ordered {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {String token = exchange.getRequest().getQueryParams().getFirst("token");if (token == null || token.isEmpty()) {exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);return exchange.getResponse().setComplete();}return chain.filter(exchange);}@Overridepublic int getOrder() {return -100;}
}

然后在 Spring Config 中配置这个 Bean

@Bean
public TokenFilter tokenFilter(){return new TokenFilter();
}

重启应用就能看到效果了

2018-05-08 20:41:06.528 DEBUG 87751 --- [ctor-http-nio-2] o.s.c.g.h.RoutePredicateHandlerMapping   : Mapping [Exchange: GET http://localhost:10000/customer/hello/windmt?token=1000] to Route{id='service_customer', uri=lb://CONSUMER, order=0, predicate=org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory$$Lambda$334/1871259950@2aa090be, gatewayFilters=[OrderedGatewayFilter{delegate=org.springframework.cloud.gateway.filter.factory.StripPrefixGatewayFilterFactory$$Lambda$337/577037372@22e84be7, order=1}, OrderedGatewayFilter{delegate=org.springframework.cloud.gateway.filter.factory.AddResponseHeaderGatewayFilterFactory$$Lambda$339/1061806694@1715f608, order=2}]}
2018-05-08 20:41:06.530 DEBUG 87751 --- [ctor-http-nio-2] o.s.c.g.handler.FilteringWebHandler      : Sorted gatewayFilterFactories: [OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=com.windmt.filter.TokenFilter@309028af}, order=-100}, OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyWriteResponseFilter@70e889e9}, order=-1}, OrderedGatewayFilter{delegate=org.springframework.cloud.gateway.filter.factory.StripPrefixGatewayFilterFactory$$Lambda$337/577037372@22e84be7, order=1}, OrderedGatewayFilter{delegate=org.springframework.cloud.gateway.filter.factory.AddResponseHeaderGatewayFilterFactory$$Lambda$339/1061806694@1715f608, order=2}, OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter@51351f28}, order=10000}, OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.LoadBalancerClientFilter@724c5cbe}, order=10100}, OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter@418c020b}, order=2147483637}, OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.WebsocketRoutingFilter@15f2eda3}, order=2147483646}, OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyRoutingFilter@70101687}, order=2147483647}, OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardRoutingFilter@21618fa7}, order=2147483647}]

官方说,未来的版本将对这个接口作出一些调整:
This interface and usage are subject to change in future milestones.
from Spring Cloud Gateway - Global Filters

自定义过滤器工厂

如果你还对上一篇关于路由的文章有印象,你应该还得我们在配置中有这么一段

filters:- StripPrefix=1- AddResponseHeader=X-Response-Default-Foo, Default-Bar

StripPrefixAddResponseHeader这两个实际上是两个过滤器工厂(GatewayFilterFactory),用这种配置的方式更灵活方便。

我们就将之前的那个ElapsedFilter改造一下,让它能接收一个boolean类型的参数,来决定是否将请求参数也打印出来。

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.List;public class ElapsedGatewayFilterFactory extends AbstractGatewayFilterFactory<ElapsedGatewayFilterFactory.Config> {private static final Log log = LogFactory.getLog(GatewayFilter.class);private static final String ELAPSED_TIME_BEGIN = "elapsedTimeBegin";private static final String KEY = "withParams";@Overridepublic List<String> shortcutFieldOrder() {return Arrays.asList(KEY);}public ElapsedGatewayFilterFactory() {super(Config.class);}@Overridepublic GatewayFilter apply(Config config) {return (exchange, chain) -> {exchange.getAttributes().put(ELAPSED_TIME_BEGIN, System.currentTimeMillis());return chain.filter(exchange).then(Mono.fromRunnable(() -> {Long startTime = exchange.getAttribute(ELAPSED_TIME_BEGIN);if (startTime != null) {StringBuilder sb = new StringBuilder(exchange.getRequest().getURI().getRawPath()).append(": ").append(System.currentTimeMillis() - startTime).append("ms");if (config.isWithParams()) {sb.append(" params:").append(exchange.getRequest().getQueryParams());}log.info(sb.toString());}}));};}public static class Config {private boolean withParams;public boolean isWithParams() {return withParams;}public void setWithParams(boolean withParams) {this.withParams = withParams;}}
}

过滤器工厂的顶级接口是GatewayFilterFactory,我们可以直接继承它的两个抽象类来简化开发AbstractGatewayFilterFactoryAbstractNameValueGatewayFilterFactory,这两个抽象类的区别就是前者接收一个参数(像StripPrefix和我们创建的这种),后者接收两个参数(像AddResponseHeader)。

GatewayFilter apply(Config config)方法内部实际上是创建了一个GatewayFilter的匿名类,具体实现和之前的几乎一样,就不解释了。

静态内部类Config就是为了接收那个boolean类型的参数服务的,里边的变量名可以随意写,但是要重写List<String> shortcutFieldOrder()这个方法。

这里注意一下,一定要调用一下父类的构造器把Config类型传过去,否则会报ClassCastException

public ElapsedGatewayFilterFactory() {super(Config.class);
}

工厂类我们有了,再把它注册到 Spring 当中

@Bean
public ElapsedGatewayFilterFactory elapsedGatewayFilterFactory() {return new ElapsedGatewayFilterFactory();
}

然后添加配置(主要改动在第 8 行)

spring:cloud:gateway:discovery:locator:enabled: truedefault-filters:- Elapsed=trueroutes:- id: service_customeruri: lb://CONSUMERorder: 0predicates:- Path=/customer/**filters:- StripPrefix=1- AddResponseHeader=X-Response-Default-Foo, Default-Bar

然后我们再次访问 http://localhost:10000/customer/hello/windmt?token=1000 即可在控制台看到以下内容

2018-05-08 16:53:02.030  INFO 84423 --- [ctor-http-nio-1] o.s.cloud.gateway.filter.GatewayFilter   : /hello/windmt: 656ms params:{token=[1000]}

总结

本文主要介绍了 Spring Cloud Gateway 的过滤器,我们实现了自定义局部过滤器、自定义全局过滤器和自定义过滤器工厂,相信大家对 Spring Cloud Gateway 的过滤器有了一定的了解。之后我们将继续在过滤器的基础上研究 如何使用 Spring Cloud Gateway 实现限流和 fallback。

示例代码可以从 Github 获取:https://github.com/zhaoyibo/spring-cloud-study

参考

spring-cloud/spring-cloud-gateway
Spring Cloud Gateway

  • 本文作者: Yibo
  • 本文链接: https://windmt.com/2018/05/08/spring-cloud-14-spring-cloud-gateway-filter/
  • 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!

Spring Cloud Gateway(过滤器)相关推荐

  1. Spring Cloud Gateway 过滤器详解

    一.概述 Spring Cloud Gateway根据作用范围划分为:GatewayFilter和GlobalFilter 1.filter的作用和生命周期 由filter工作流程点,可以知道filt ...

  2. Spring Cloud Gateway 过滤器执行顺序原理分析

    过滤器类型 GlobalFilter:全局过滤器,对所有路由生效.通过实现GlobalFilter接口创建 GatewayFilter:网关过滤器,也可以说是局部过滤器.自定义过滤器,只对配置了此过滤 ...

  3. Spring Cloud Gateway配置详解-过滤器

    Spring Cloud Gateway-过滤器 本节将为大家详细介绍Spring Could Gateway 内置过滤器相关内容. Spring Cloud Gateway 过滤器为大家提供了修改特 ...

  4. Spring Cloud Gateway之Predict篇

    Spring Cloud gateway工作流程 在之前的文章的Spring Cloud GateWay初体验中,大家已经对Spring Cloud Gateway的功能有一个初步的认识,网关作为一个 ...

  5. 微服务网关spring cloud gateway入门详解

    1.API网关 API 网关是一个处于应用程序或服务( REST API 接口服务)之前的系统,用来管理授权.访问控制和流量限制等,这样 REST API 接口服务就被 API 网关保护起来,对所有的 ...

  6. Spring Cloud Gateway 之Predict篇

    转载请标明出处: http://blog.csdn.net/forezp/article/details/84926662 本文出自方志朋的博客 个人博客纯净版:https://www.fangzhi ...

  7. Spring Cloud Gateway(一)为什么用网关、能做什么、为什么选择Gateway、谓词工厂、过滤器配置

    1.为什么用网关?能做什么?为什么选择Gateway? 1.1.为什么用网关 网关api:封装了系统内部架构,为每个客户端提供一个定制的 API.在微服务架构中,服务网关的核心要点是,所有的客户端和消 ...

  8. Spring Cloud Gateway(十):网关过滤器工厂 GatewayFilterFactory

    本文基于 spring cloud gateway 2.0.1 1.GatewayFilterFactory 简介 路由过滤器允许以某种方式修改传入的HTTP请求或传出的HTTP响应. 路径过滤器的范 ...

  9. java 配置全局过滤器,如何为Spring Cloud Gateway加上全局过滤器

    既然是一个网关.那么全局过滤器肯定是少不了的一个存在.像是鉴权.认证啥的不可能每个服务都做一次,一般都是在网关处就搞定了. Zuul他就有很强大的过滤器体系来给人使用. Gateway当然也不会差这么 ...

最新文章

  1. CSS自定义鼠标样式。JS获取鼠标坐标,实现提示气泡框跟随鼠标移动
  2. HttpWebRequest WebResponse 对象简单了解
  3. mapinfo制作地图_用QGIS代替Mapinfo软件
  4. c语言什么是内联函数,C语言中内联函数inline的使用方法
  5. 【云炬大学生创业基础笔记】第1章第3节 什么是创业测试
  6. 半径为r的均匀带电球体_半径为R的均匀带电球面,总带电量为Q,设无穷远处的电势为零,则距离球心为r(r=R)的P点处的电场强度的大小和电势为...
  7. java 合并单元格 把数据合并没了_合并单元格
  8. Linux性能优化之“关闭Ctrl+Alt+Del”
  9. php socket 不能用,PHP无法用Socket方式连接MySQ
  10. centos安装软件【google浏览器,QQ】【拷贝旧的文件源作为备份】【软件源更换为清华源】
  11. 2014年辛星starphp第一节设置入口文件以及App类
  12. access建立er图_Visio绘制ER图教程
  13. MapStruct 详解
  14. Win10更新后BUG——任务栏点不动、卡死、加载不出来解决办法
  15. 学说不能选计算机专硕的课,初试前先选组,选定离手还不能改?北京邮电大学计算机...
  16. 【语义分割】Smoothed Dilated Convolutions for Improved Dense Prediction阅读笔记
  17. 服务器装系统报0x0000005d,安装Win8系统出现error code 0x0000005d如何解决?
  18. firefox火狐同步windows和linux书签
  19. Arcgis api for JavaScript 4.12解读-----symbol
  20. vsCode格式化标签属性不换行

热门文章

  1. Python中获取字典中最值对应的键
  2. C++ 类里面,函数占用存储空间问题
  3. 火狐已阻止载入混合活动内容“http://www.XXX/index.php?app=serviceac=authts=isauthurl=...
  4. 创建一个提供数据 API 的 Node.js 网站
  5. Nginx 学习笔记(四) Nginx+keepalived
  6. UNIX学习笔记(七) 后台执行命令3 命令
  7. 热点聚焦:精益生产模式的最大优势在哪?
  8. 【CVPR 2018】Learning Rich Features for Image Manipulation Detection(图像篡改检测)
  9. 思考--为何早晨型人更容易成功
  10. 用OpenGL进行曲线、曲面的绘制