人生可以有过错,但不能有错过;意志决定你的成功,进取决定你的未来;怀疑和等待你永远看不到未来,拼搏的人生才会更精彩 !



上篇文章主要分享了一下API网关相关理论知识,这篇文章我们主要介绍一下使用Spring Cloud的Gateway搭建API 网关。
首先我们先简单回顾一下,什么是API网关呢?通俗说就是介于客户端与服务端之间一个接受所有请求的系统服务,解决微服务架构中客户端直接请求服务端存在的一些问题。比如有这样一个场景:像一个电商网站的产品详情页面不仅包含基本的产品信息(如名称、描述、价格)还包括购物车中的商品数量、历史订单、客户评论、低库存预警、送货选项、各种推荐、其它的购物选择等,若采用微服务架构,显示在产品页上的数据会分布在不同的微服务上,可能有:购物车服务:提供购物车中的件数;订单服务提供历史订单数据;目录服务:提供商品基本信息,如名称、图片和价格等;评论服务提供对客户评论的支持;库存服务提供对低库存预警的支持;送货服务提供送货选项、期限和费用,这些信息单独从送货方API获取;推荐服务提供给用户推荐商品的支持等等。
上述场景如果客户端直接和服务端通信会存在什么问题呢?
问题一:客户端需要和每个微服务暴露的细粒度API去匹配。在上述场景中,显示一个产品页面客户端需要发送7个独立请求。在更复杂的应用程序中,可能要发送更多的请求,毋庸置疑页面加载效率很低而且客户端代码会很复杂。
问题二:各个服务使用的协议不同,可能是Thrift 二进制 RPC,AMQP 消息传递协议,或应用程序应该使用HTTP和 WebSocket 之类的协议,所以客户端需要实现多种协议的请求。
问题三:它会使得微服务难以重构,随着时间推移,我们可能想要更改系统拆分成服务的方式。例如,我们可能合并两个服务,或者将一个服务拆分成两个或更多服务。然而,如果客户端与微服务直接通信,那么执行这类重构就非常困难了。
使用API网关可以很好解决上述问题,API网关是提供系统唯一入口的服务,封装了内部系统架构,并向客户端提供API,解放了客户端的复杂编码,客户端不需要知道每个微服务的端口和ip;还有是可能各个服务的协议也是不同的,那么客户端也不需要关心请求服务的协议,网关对外统一暴露http协议,然后网关实现协议的转换,然后请求到不同的微服务;如果微服务进行了重构,客户端也不要跟着变,客户端调整也很方便。除此之外,它还可能负责诸如用户验证、监控、负载均衡、缓存、请求管理、静态响应处理等功能。
当然,任何事情都是有利有弊,使用API网关也有弊端:①增加了一个需要开发、部署和维护的高可用组件;②API网关有时候变成了开发的瓶颈。开发者为了暴露新的微服务必须更新API网关,API网关的更新流程要尽可能的简单,否则,开发人员不得不排队等待。尽管它有这些不足,但对于大部分的应用程序而言,使用 API 网关是合理的。
Spring Cloud 网关 Gateway接下来我们搭建 Spring Cloud的新一代网关Gateway,估计很多小伙伴之前接触多的是zuul,不过现在一般不用了,zuul现在社区活跃度不高,而且在升级到2版本的时候有分歧,所以SpringCloud借鉴了zuul的核心理念研发了自己的Gateway,说白了其实就是zuul1.x的升级。
SpringCloud Gateway是什么?SpringCloudGateway是SpringCloud的一个全新项目,基于Spring 5.0,SpringBoot2.0和Project Reactor等技术开发的网关,它旨在为微服务架构提供一种简单有效统一的API路由管理。它是基于WebFlux框架实现的,而WebFlux底层使用了高性能的Reactor模式通信框架Netty,非阻塞高性能框架。SpringCloud Gateway的目标是提供统一的路由方式且基于Filter链的方式提供了网关基本的功能,eg:安全、监控/指控、限流等。
SpringCloud Gateway的特性① 动态路由:能够匹配任何请求属性;
② 可以对路由指定Predicate(断言)和Filter(过滤器);
③ 集成Hystrix的断路器功能;
④ 集成Spring Cloud的服务发现功能;
⑤ 请求限流功能;
⑥ 支持路径重写 ......
SpringCloud Gateway的三大核心 1)Route 路由路由是构建网关的基本模块,它由ID、目标URL,一些列的断言和过滤器组成,如果断言为true,则匹配该路由。
2)Predicate 断言开发人员可以匹配HTTP请求中的所有内容,eg:请求头、请求参数,如果请求与断言相匹配,则进行路由。
3)Filter 过滤器指的是Spring框架中GatewayFilter 的,使用过滤器可以请求被路由前或后对请求参数进行修改。

SpringCloud Gateway的工作流程是什么样呢?其核心逻辑就是路由转发+执行过滤器链。在官网找了如下图片说明:

客户端向Gateway发出请求,然后在Gateway Handler Mapping中找到与请求匹配的路由然后再将其发给Gateway Web Handler;
Handler再通过过滤器链将请求发送到实际的服务执行业务逻辑,然后返回;(上图将过滤器链用虚线划分开是因为过滤器可能会在发送代理请求之前或之后执行业务逻辑)
在发送代理请求之前的过滤器中可以做:参数校验、权限校验、流量监控、日志输出、协议转换等;在发送代理请求之前的过滤器中可以做:响应内容、响应头的修改、日志输出、流量监控等。
SpringCloud Gateway 工程搭建下面我们搭建一个简单的工程看一下效果:项目工程结构如下:

首先搭建一个注册中心服务:eureka-demo,这里使用eurek搭建服务注册中心,服务端口:10086,配置代码如下:
# 应用名称,会在Eureka中显示spring: application: name: eureka-server server: port: 10086eureka: client: register-with-eureka: false # 是否注册自己的信息到EurekaServer,默认是true fetch-registry: false # 是否拉取其它服务的信息(集群),默认是true service-url: # EurekaServer的地址,现在是自己的地址,如果是集群,需要加上其它Server的地址。 defaultZone: http://127.0.0.1:${server.port}/eureka将服务提供者user-service-demo和user-service-slave-demo注册到服务中心,这里是两个相同的服务使用不同的端口,分别是8081端口和8082端口,模拟集群,这里贴出user-service-demo配置文件和提供的服务如下:
# 指定应用名称,将来会作为应用的id使用spring: application: name: user-service datasource: url: driver-class-name: data-username: password:server: port: 8081# 配置eureka客户端eureka: client: service-url: defaultZone: http://127.0.0.1:10086/eureka # eurekaserver的地址 instance: prefer-ip-address: true # 当调用getHostname获取实例的hostname时,返回ip而不是host名称 ip-address: 127.0.0.1 # 指定自己的ip信息,不指定的话会自己寻找//开启Eureka客户端功能功能@EnableEurekaClient@SpringBootApplicationpublic class UserServiceDemoApplication { public static void main(String[] args) { SpringApplication.run(UserServiceDemoApplication.class, args); }}@RestControllerpublic class UserController { @Autowired Environment environment; @RequestMapping("/index") public String index(){ return "index"; } @RequestMapping("/getPort") public String getPort(){ return environment.getProperty("local.server.port"); }}将网关服务gateway-demo也注册到服务中心,端口是9000:Gateway静态配置路由的时候有两种方式:一种是通过配置文件配置如下:
server: port: 9000spring: application: name: gateway-demo cloud: # 静态路由配置gateway 路由 gateway: routes: # 配置路由匹配:可以配置多个路由,一个集合对象用 - 表示 - id: user-service-demo-route # 路由id,没有固定规则,但要求唯一,建议配合服务名 uri: http://127.0.0.1:8081 # 提供服务的真实地址 predicates: - Path=/getPort/** # 断言 路径相匹配的进行路由(真实服务的接口地址) # 配置Eurekaeureka: client: service-url: defaultZone: http://127.0.0.1:10086/eureka # eurekaserver的地址 instance: prefer-ip-address: true # 当调用getHostname获取实例的hostname时,返回ip而不是host名称 ip-address: 127.0.0.1 # 指定自己的ip信息,不指定的话会自己寻找一种是通过配置类配置如下:
@Configurationpublic class GatewayConfiguration { /** * 配置路由规则 * @param builder * @return */ @Bean public RouteLocator routes(RouteLocatorBuilder builder){ return builder.routes() .route("user-service-route",r -> r.path("/index").uri("http://127.0.0.1:8081/index")) .build(); }实现的效果如下:我们请求服务user-service-demo提供的/getPort资源时,不是直接请求user-service-demo:http://127.0.0.1:8081/getPort,而是通过请求网关,然后满足路由匹配规则后网关路由到user-service-demo服务:eureka注册的服务:

请求网关服务:http://127.0.0.1:9000/getPort如下:通过yml配置

请求网关服务:http://127.0.0.1:9000/index如下:通过配置类配置

如果我们请求一个不匹配路由规则的请求返回什么呢?

没错,404找不到资源,上面我们简单看了一下效果,下面我们介绍一下如何配置动态路由:网关配置文件需要做如下调整:
cloud: # 动态路由 配置gateway 路由 gateway: discovery: locater: enable: true # 开启从注册中心动态创建路由的功能 routes: # 配置路由匹配:可以配置多个路由,一个集合对象用 - 表示 - id: user-service-route # 路由id,没有固定规则,但要求唯一,建议配合服务名 uri: lb://user-service # 注册到服务中心的名称,协议是用lb,表示开启负载均衡 predicates: - Path=/getPort/** # 断言 路径相匹配的进行路由(真实服务的接口地址)如上述配置文件我做了详细注释,这种场景主要用在某个服务存在集群服务的时候,网关如何实现负载均衡即将请求分发到不同的实例上呢,这里使用的是lb协议,开启Gateway的负载均衡功能。进行如上配置然后再将user-service-slave-demo服务也注册到注册中心,实现的效果就是:访问网关服务http://127.0.0.1:9000/getPort,输出的内容就是8081和8082轮流切换,相当于轮流请求集群中两个实例,这里就不在贴图啦,感兴趣小伙伴可以亲自试一试。
怎么配置断言Predicate呢?只需在配置文件中进行配置即可如下:
routes: # 配置路由匹配:可以配置多个路由,一个集合对象用 - 表示 - id: user-service-route # 路由id,没有固定规则,但要求唯一,建议配合服务名 uri: lb://user-service # 注册到服务中心的名称,协议是用lb,表示开启负载均衡 predicates: - Path=/getPort/** # 断言 路径相匹配的进行路由(真实服务的接口地址) - After=2020-03-28T19:43:56.110+08:00[Asia/Shanghai] # 在***之后访问,类似配置还有Before/Between (,) - Cookie=username,zs # 配置cookie # - Header=X-Request-Id,\d+ # 属性键 - 正则表达式值 # - Method=Get #请求方式 # - Query=username,\d+官网上常见的Predicate有如下:大家使用的时候在官网上找即可:

怎么配置过滤器Filter呢?Spring Cloud内置了多种路由过滤器,它们都由Gateway Filter的工厂类来产生,具体有哪些可以查阅官网文档,我们这里简单自定义一个验证用户身份和日志记录的 Gateway Filter,代码如下:
@Componentpublic class MyLogGatewayFilter implements GlobalFilter,Ordered { private Logger logger = LoggerFactory.getLogger(MyLogGatewayFilter.class); @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { logger.info("=============进入过滤器:MyLogGatewayFilter,时间:"+new Date()); //验证用户身份 String username = exchange.getRequest().getQueryParams().getFirst("username"); //不合法:返回不被接受的请求 if (Strings.isBlank(username)){ logger.info("=============用户名不合法:"); exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE); return exchange.getResponse().setComplete(); } //合法放行 return chain.filter(exchange); } /** * 该过滤器在过滤器链中顺序:数字越小,优先级越高 * @return */ @Override public int getOrder() { return 0; }}先介绍到这里,有感兴趣小伙伴非常欢迎留言交流哦~~


欢迎关注ITSK,每天进步一点点,我们追求在交流中收获成长和快乐
