Gateway官方文档:https://cloud.spring.io/spring-cloud-gateway/2.2.x/reference/html/
在Spring Cloud 1.x中,网关使用的是Zuul 1,在Spring Cloud 2.x中,网关使用的是Gateway,因为Zuul 2版本进展缓慢,所以Spring Cloud自己研发了网关,Gateway是原Zuul 1的替代版。Gateway采用异步非阻塞模型开发,性能上不需要担心,虽然Netflix发布了Zuul 2版本,但是Spring Cloud并没有整合的计划,所以才自己推出了Gateway的方案。
Spring Cloud Gateway是Spring Cloud的一个全新项目,基于Spring 5+Spring Boot 2.x+Project Reactor等技术开发的网关,旨在为微服务架构提供一种简单有效的统一API路由管理方式。
Spring Cloud Gateway作为Spring Cloud生态系统中的网关,目标是替代Zuul 1,在Spring Cloud 2.x版本中,没有对新版本Zuul 2最新高性能版本进行集成,仍然使用的Zuul 1非Reactor模式的老版本,为了提升网关性能,Spring Cloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层使用了高性能的Reactor模式通信框架Netty。
Spring Cloud Gateway目标是提供统一的路由方式,并且基于Filter链方式提供网关基本功能:安全、监控、指标、限流。
网关的作用:反向代理、鉴权、流量控制、熔断、日志监控等。
在微服务架构中,网关的位置,位于各个微服务的上一层,通过网关后,就访问到了具体的微服务了。
Spring Cloud Gateway特性:
Spring Cloud Gateway和Zuul的区别:
Zuul 1模型缺点:Zuul 1采用传统Servlet I/O处理模型,但请求进入servlet容器时,servlet容器会为其绑定一个线程,在并发不高的情况下是适用的,当并发量增加,线程数就会增加,但是线程资源是非常昂贵的(线程上下文切换内存消耗大),会影响到请求处理时间。有些简单的业务场景,并不需要每个请求分配一个线程,简单业务的高并发下,实际可能只需要几个线程就能扛得住,因此,每个请求分配一个线程,在高并发环境下并没有优势。
Gateway模型:Gateway模型采用的是WebFlux框架,这个框架是一个典型的非阻塞异步的框架,并且在Servlet 3.1后,支持了异步非阻塞,框架的核心是基于Reactor相关API实现的。相对于传统Web框架,它可以运行在支持Servlet 3.1容器上的组件中(如Netty、Undertow等)。Spring WebFlux是Spring 5引入的新的响应式框架,区别于Spring MVC,不需要依赖Servlet API,完全异步非阻塞,并且基于Reactor来实现响应式流规范。
Spring WebFlux可以参考官网:https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html
路由是构建网关的基本模块,由ID,目标URI一系列断言和过滤器组成,如果断言为true,则匹配该路由。
参考Java8的java.util.function.Predicate,开发人员可以匹配HTTP请求中的所有内容(比如请求头,请求参数等),如果请求与断言匹配,则进行路由。
类似于Web开发中的过滤器,这里指的是Spring框架中GatewayFilter实例,使用过滤器可以在请求被路由之前或之后对请求进行修改。
路由的功能是由断言和过滤组合来实现的,一个Web请求发送后,先经过网关,网关里的断言和过滤用来判断这个请求是否需要路由转发,当断言为true,过滤器放行时候,这个请求进行路由转发,此时,请求才到达具体的微服务模块。
客户端向Spring Cloud Gateway发送请求,然后在Gateway Handler Mapping中找到与请求相匹配的路由,将其Web Handler。Handler通过指定的过滤器链将请求发送到实际服务,执行业务逻辑,然后返回。如果有post类型的过滤器,执行过滤器逻辑。
pre类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等功能。
post类型的过滤器可以做响应内容、响应头的修改、日志输出、流量监控等功能。
创建cloud-gateway-gateway9527模块,修改pom.xml,加入spring-cloud-starter-gateway和spring-cloud-starter-netflix-eureka-client的坐标。
cloud2020
com.atguigu.springcloud
1.0-SNAPSHOT
4.0.0
cloud-gateway-gateway9527
org.springframework.cloud
spring-cloud-starter-gateway
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.boot
spring-boot-devtools
runtime
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
添加application.yml配置文件。
server:
port: 9527
spring:
application:
name: cloud-gateway
eureka:
instance:
hostname: cloud-gateway-service
client:
register-with-eureka: true # true:将自己注册进Eureka
fetch-registry: true # true:需要去注册中心获取其他服务地址
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
添加主启动类。
package com.atguigu.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
public class GatewayMain9527 {
public static void main(String[] args) {
SpringApplication.run(GatewayMain9527.class, args);
}
}
通过yml给Gateway添加路由规则。
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
routes:
# - 是yml语法,代表数组的意思
- id: payment_route1 # 路由ID,没有固定规则,需要保证唯一
uri: http://localhost:8001 # 路由到哪个地址,实际提供微服务的地址
predicates:
- Path=/payment/get/** # 路径匹配断言
- id: payment_route2 # 路由ID,没有固定规则,需要保证唯一
uri: http://localhost:8001 # 路由到哪个地址,实际提供微服务的地址
predicates:
- Path=/payment/loadbalance # 路径匹配断言
eureka:
instance:
hostname: cloud-gateway-service
client:
register-with-eureka: true # true:将自己注册进Eureka
fetch-registry: true # true:需要去注册中心获取其他服务地址
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
相比之前的yml,这里的yml多了Gateway结点,配置这个的目的在于隐藏localhost:8001的地址,如果想访问payment8001服务的/payment/get/**地址,之前需要访问http://localhost:8001/payment/get/1,配置了路由之后,访问http://localhost:9527/payment/get/1也可以实现,于是,原来的8001端口就隐藏了。
配置路由有两种方式,一种是上面的yml配置文件,另一种是是编码方式配置,uri中记得带http,否则访问不到,这里的uri可以写任意地址,代表一个路由规则,访问path路径的时候,被路由到uri地址。
package com.atguigu.springcloud.config;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder routeLocatorBuilder) {
return routeLocatorBuilder.routes()
// 当访问http://localhost:9527/payment/get/**的时候,请求会被路由到http://localhost:8001/payment/get/**
.route("payment_route1", r -> r.path("/payment/get/**").uri("http://localhost:8001/payment/get/**"))
// 当访问http://localhost:9527/payment/loadbalance的时候,请求会被路由到http://localhost:8001/payment/loadbalance
.route("payment_route2", r -> r.path("/payment/loadbalance").uri("http://localhost:8001/payment/loadbalance"))
.build();
}
}
现在存在的问题:真实服务地址写死了,我们应该通过服务名来找服务,而不是通过服务地址找服务。
默认情况下,Gateway会根据注册中心注册的服务列表,以注册中心上微服务名为路径创建动态路由转发,从而实现动态路由的功能。
为了演示网关的负载均衡,修改Provider8001和Provider8002的yml配置文件,将它们的注册地址改成eureka7001的地址。启动Eureka7001和Provider8001和Provider8002服务。修改Gateway9527模块的application.yml配置文件,修改为如下内容。因为要修改application.yml,所以先把GatewayConfig类进行屏蔽,避免产生影响。
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
routes:
# - 是yml语法,代表数组的意思
- id: payment_route1 # 路由ID,没有固定规则,需要保证唯一
# uri: http://localhost:8001 # 路由到哪个地址,实际提供微服务的地址
uri: lb://cloud-payment-service # 使用微服务名称查找路由地址,lb是用于识别负载均衡的前缀,不能修改
predicates:
- Path=/payment/get/** # 路径匹配断言
- id: payment_route2 # 路由ID,没有固定规则,需要保证唯一
# uri: http://localhost:8001 # 路由到哪个地址,实际提供微服务的地址
uri: lb://cloud-payment-service # 使用微服务名称查找路由地址,lb是用于识别负载均衡的前缀,不能修改
predicates:
- Path=/payment/loadbalance # 路径匹配断言
discovery:
locator:
enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名称进行路由
eureka:
instance:
hostname: cloud-gateway-service
client:
register-with-eureka: true # true:将自己注册进Eureka
fetch-registry: true # true:需要去注册中心获取其他服务地址
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
通过浏览器访问http://localhost:9527/payment/loadbalance,在页面端可以看到端口号的不断变化,如果要实现负载均衡,注意uri中的lb是固定的,它是识别开启负载均衡的标志。
在Gateway9527启动的时候,在Console可以看到如下内容,代表执行加载Predicate,我们目前使用的Predicate是Path。
2020-06-17 07:31:15.501 INFO 10848 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [After]
2020-06-17 07:31:15.501 INFO 10848 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Before]
2020-06-17 07:31:15.501 INFO 10848 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Between]
2020-06-17 07:31:15.501 INFO 10848 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Cookie]
2020-06-17 07:31:15.501 INFO 10848 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Header]
2020-06-17 07:31:15.501 INFO 10848 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Host]
2020-06-17 07:31:15.501 INFO 10848 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Method]
2020-06-17 07:31:15.501 INFO 10848 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Path]
2020-06-17 07:31:15.501 INFO 10848 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Query]
2020-06-17 07:31:15.501 INFO 10848 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [ReadBodyPredicateFactory]
2020-06-17 07:31:15.501 INFO 10848 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [RemoteAddr]
2020-06-17 07:31:15.501 INFO 10848 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Weight]
2020-06-17 07:31:15.502 INFO 10848 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [CloudFoundryRouteService]
官方文档:https://cloud.spring.io/spring-cloud-gateway/2.2.x/reference/html/#gateway-request-predicates-factories
Spring Cloud Gateway将路由匹配作为Spring WebFlux HandlerMapping基础架构的一部分。
Spring Cloud Gateway包括许多内置的Route Predicate Factory,这些Predicate斗鱼HTTP请求的不同属性匹配,多个Route Predicate Factory可以进行组合。
Spring Cloud Gateway创建Route对象时,使用Route Predicate Factory创建Predicate对象,Predicate对象可以赋值给Route。
根据官方文档里的说明,在application.yml里配置上即可生效,这相当于加了一个条件,条件满足就做路由转发,不满足就不路由转发。
简单说下吧,After Route Predicate、Before Route Predicate、Between Route Predicate需要用到一个DateTime值,这个值要使用ZonedDateTime类来获取。Cookie Route Predicate、Header Route Predicate、Host Route Predicate的测试使用cmd来测试,通过发送curl命令即可完成,具体可以看一下curl命令的参数介绍。
测试Cookie Route Predicate
curl http://localhost:9527/payment/loadbalance --cookie "username=wangshaoyang"
测试Header Route Predicate
curl http://localhost:9527/payment/loadbalance -H "X-Request-Id:123"
测试Header Route Predicate
curl http://localhost:9527/payment/loadbalance -H "Host: www.atguigu.com"
Method Route Predicate是根据GET或POST做断言,Path Route Predicate就是我们最初使用的方式,Query Route Predicate是根据请求里所带Query参数进行判断的,RemoteAddr Route Predicate是对请求发起ip做的校验,Weight Route Predicate是给负载均衡分配权重的,可以指定请求发到某一个uri的权重。
所以说,Predicate可以提供一组匹配规则,每当一个请求过来的时候,去检验这些规则,如果满足规则,进行路由,否则,就提示出错。
路由过滤器可用于修改进入的HTTP请求和返回的HTTP响应,路由过滤器只能指定路由进行使用,Spring Cloud Gateway内置了多种路由过滤器,它们是通过Gateway Filter Factory产生的。
从生命周期来分类,可以分为Pre和Post,类似于Spring AOP的前置通知和后置通知。从种类上来分类,可以分为Gateway Filter和Global Filter。具体可以参考官网,这个的配置也是在application.yml里加配置即可。
Gateway Filter:https://cloud.spring.io/spring-cloud-gateway/2.2.x/reference/html/#gatewayfilter-factories
Global Filter:https://cloud.spring.io/spring-cloud-gateway/2.2.x/reference/html/#global-filters
下面来介绍自定义全局过滤器,我们可以根据业务需求做定制化处理。
package com.atguigu.springcloud.filter;
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.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class MyGatewayFilter implements GlobalFilter, Ordered {
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取Request,获取参数
MultiValueMap queryParams = exchange.getRequest().getQueryParams();
System.out.println(queryParams);
// 这里可以对queryParams里的值做一些校验
if ("".equals(queryParams.getFirst("username"))) {
System.out.println("非法用户");
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
// 表示该请求已经处理完成
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
/**
* 过滤器的优先级,值越小优先级越高
*/
@Override
public int getOrder() {
return 0;
}
}