spingcloud(8) gateway
一。简介
springcloud是第二代网关,取代zuul网关。具有强大的智能路由,过滤器功能。常见的功能有路由转发、权限校验、限流控制等作用
Spring Cloud Gateway 具有如下特性:
- 基于Spring Framework 5, Project Reactor 和 Spring Boot 2.0 进行构建;
- 动态路由:能够匹配任何请求属性;
- 可以对路由指定 Predicate(断言)和 Filter(过滤器);
- 集成Hystrix的断路器功能;
- 集成 Spring Cloud 服务发现功能;
- 易于编写的 Predicate(断言)和 Filter(过滤器);
- 请求限流功能;
- 支持路径重写。
相关概念
- Route(路由):路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由;
- Predicate(断言):匹配请求中的信息,与断言匹配成功则进行路由。
- Filter(过滤器):指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前后对请求进行修改。
二。代码
pom.xml添加依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency>
有两种配置路由的方式:一种是在yml文件来配置,另一种是通过java bean来配置。下面例子大部分用yml
java bean配置
SpringBootApplication @RestController public class SpringcloudGatewayApplication { public static void main(String[] args) { SpringApplication.run(SpringcloudGatewayApplication.class, args); } /** * java配置路由会覆盖application.yml中的配置 * @param builder * @return */ @Bean public RouteLocator myRoutes(RouteLocatorBuilder builder){ String httpUri = "http://httpbin.org:80"; return builder.routes() //route来做路由 .route("rout1",p -> p .path("/get") // 对/get请求做处理 .filters(f->f.addRequestHeader("hello", "yy")) //过滤器增加header .uri(httpUri)) //转发到这个请求上 .build(); } }
可以看到请求/get 的header添加了参数hello=yy
1.predicate实战
After Route Predicate Factory
可配置在指定时间后的请求,都交给router处理。否则不通过路由
server:
port: 8081
spring:
profiles:
active: add_request_header_route
---
spring:
cloud:
gateway:
routes:
- id: after_route
uri: http://httpbin.org:80/get #前面的路径替换为该值即http://xxx:8081这段
filters: #增加请求header的过滤器
- AddRequestHeader=X-Request-Foo, Bar
predicates:
- After=2017-01-20T17:42:47.789-07:00[America/Denver]
profiles: add_request_header_route
启动工程,在浏览器上访问http://localhost:8081/,会显示http://httpbin.org:80/get返回的结果,此时gateway路由到了配置的uri。
Header Route Predicate Factory
Header Route Predicate Factory需要两个参数,header名称和值,当断言匹配了请求中的header名称和值后,断言通过,就能进入路由规则
spring:
profiles:
active: header_route
---
spring:
cloud:
gateway:
routes:
- id: header_route
uri: https://www.baidu.com
predicates:
- Header=X-Request-Id, \d+ #当请求的Header中有X-Request-Id的header名,且header值为数字时
profiles: header_route
测试:
curl -H "X-Request-Id:1" localhost:8081
返回页面请求就通过了,404则没有通过
Cookie Route Predicate Factory
spring: profiles: active: cookie_route --- spring: cloud: gateway: routes: - id: cookie_route uri: http://httpbin.org:80/get predicates: - Cookie=name, yy profiles: cookie_route
请求带有cookie名称为name,值为yy就能通过,被转发到http://httpbin.org:80/get
测试
curl -H "Cookie:name=forezp" localhost:8081
Host Route Predicate Factory
需要以一个参数即hostname,会匹配请求头中的host值,匹配则转发
spring: profiles: active: host_route --- spring: cloud: gateway: routes: - id: host_route uri: http://httpbin.org:80/get predicates: - Host=**.yy.com profiles: host_route
测试:curl -H "Host:www.fangzhipeng.com" localhost:8081
Method Route Predicate Factory
需要一个参数请求类型,GET
spring: profiles: active: method_route --- spring: cloud: gateway: routes: - id: method_route uri: http://httpbin.org:80/get predicates: - Method=GET profiles: method_route
测试:curl localhost:8081
模拟POST请求:curl -XPOST localhost:8081
Path Route Predicate Factory
需要一个参数匹配路径
spring: profiles: active: path_route --- spring: cloud: gateway: routes: - id: path_route uri: http://localhost:9600/ predicates: - Path=/hello profiles: path_route
Query Route Predicate Factory
需要两个参数,参数名和参数值,也可以只有一个参数即参数名
spring: profiles: active: query_route --- spring: cloud: gateway: routes: - id: query_route uri: http://httpbin.org:80/get predicates: - Query=foo, ba. profiles: query_route
测试:curl localhost:8081?foo=bar
Predict作为断言,它决定了请求会被路由到哪个router 中。在断言之后,请求会被进入到filter过滤器的逻辑
2.filter实战
确定哪个路由执行后,在路由处理之前,先经过”pre“类型的过滤器,路由处理后,经过”post“类型的过滤器
”pre“类型的过滤器可以做参数校验,权限校验,流量监控,日志输出,协议转换等
”post“类型的过滤器可以做响应内容,响应头修改,日志输出,流量监控等。
springcloud包含许多内置的GatewayFilter工厂,和predicate一样配置在application.yml,遵循了约定大于配置的原则,只需要配置filter工厂的名称,不需要配置全类名。
比如AddRequestHeaderGatewayFilterFactory只需要在配置文件中写AddRequestHeader,而不是全部类名
AddRequestHeader GatewayFilter Factory
server: port: 8081 spring: profiles: active: add_request_header_route #指定配置文件 --- spring: cloud: gateway: routes: - id: add_request_header_route uri: http://httpbin.org:80/get filters: - AddRequestHeader=X-Request-Foo, Bar #AddRequestHeaderGatewayFilterFactory predicates: - After=2017-01-20T17:42:47.789-07:00[America/Denver] #AfterPredictFactory profiles: add_request_header_route
模拟请求:curl localhost:8081
最终显示了从 http://httpbin.org:80/get得到了请求,响应如下:
{
"args": {},
"headers": {
"Accept": "*/*",
"Connection": "close",
"Forwarded": "proto=http;host=\"localhost:8081\";for=\"0:0:0:0:0:0:0:1:56248\"",
"Host": "httpbin.org",
"User-Agent": "curl/7.58.0",
"X-Forwarded-Host": "localhost:8081",
"X-Request-Foo": "Bar"
},
"origin": "0:0:0:0:0:0:0:1, 210.22.21.66",
"url": "http://localhost:8081/get"
}
从响应知道,请求头中加入了 "X-Request-Foo": "Bar"
RewritePath GatewayFilter Factory
重写路径的功能,类似nginx的功能
spring: profiles: active: rewritepath_route --- spring: cloud: gateway: routes: - id: rewritepath_route uri: http://localhost:9600/ predicates: - Path=/foo/** filters: #将/foo/(?.*)重写为{segment},然后转发 - RewritePath=/foo/(?<segment>.*), /$\{segment} profiles: rewritepath_route
比如在网页上请求localhost:8081/foo/forezp,此时会将请求转发到https://blog.csdn.net/forezp的页面
Hystrix GatewayFilter
允许将断路器功能添加到网关中,使服务免受级联故障??这个如何实现??, 提供服务降级处理
pom.xml增加hystrix
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>
- 增加服务降级处理类
-
@RestController public class FallbackController { @GetMapping("/fallback") public Object fallback() { Map<String,Object> result = new HashMap<>(); result.put("data",null); result.put("message","Get request fallback!"); result.put("code",500); return result; } }
- 在application-filter.yml中添加相关配置,当路由出错时会转发到服务降级处理的控制器上:
spring:
cloud:
gateway:
routes:
- id: hystrix_route
uri: http://localhost:9600
predicates:
- Method=GET
filters:
- name: Hystrix
args:
name: fallbackcmd
fallbackUri: forward:/fallback
访问http://localhost:8081/user转发到http://localhost:9600/user, 404错误转发到服务降级处理的控制器上,返回降级处理的信息
现在yml中配置测试不行,在java bean中配置可以返回
@Bean public RouteLocator myRoutes(RouteLocatorBuilder builder){ String httpUri = "http://httpbin.org:80"; return builder.routes() //route来做断言 .route(p -> p .host("*.hystrix.com") .filters(f->f.hystrix(config -> config .setName("mycmd") .setFallbackUri("forward:/fallback"))) .uri(httpUri)) .build(); }
测试: curl -H "Host:www.hystrix.com" localhost:8081?token=1
返回: {"code":500,"data":null,"message":"Get request fallback!"}
自定义过滤器
springcloud内置了19种过滤器工厂,也可以自动逸过滤器。在spring Cloud Gateway中,过滤器需要实现GatewayFilter和Ordered2个接口
/** * 自定义实现过滤器 */ public class RequestTimeFilter implements GatewayFilter, Ordered { private static final Log log = LogFactory.getLog(RequestTimeFilter.class); private static final String REQUEST_TIME_BEGIN = "reqTimeBegin"; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 操作前执行 exchange.getAttributes().put(REQUEST_TIME_BEGIN, System.currentTimeMillis()); return chain.filter(exchange).then( // 操作后执行 Mono.fromRunnable(()->{ Long startTime = exchange.getAttribute(REQUEST_TIME_BEGIN); if(startTime!=null){ log.info(exchange.getRequest().getURI().getRawPath() + ":" + (System.currentTimeMillis() - startTime) + "ms"); } })); } /** * 给过滤器设定优先级别的,值越大则优先级越低 * @return */ @Override public int getOrder() { return 0; } }
java中配置路由,使用自定义RequestTimeFilter
@Bean public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) { // @formatter:off return builder.routes() .route(r -> r.path("/customer/**") .filters(f -> f.filter(new RequestTimeFilter()) .addResponseHeader("X-Response-Default-Foo", "Default-Bar")) .uri("http://httpbin.org:80/get") .order(0) .id("customer_filter_router") ) .build(); // @formatter:on }
测试:curl localhost:8081/customer/123
控制台输出请求时间的日志:
2020-10-14 22:17:48.528 INFO 107912 --- [ctor-http-nio-8] com.yy.gateway.RequestTimeFilter : /customer:28ms
自定义过滤器工厂
这样就能在配置文件中配置,自定义过滤器了
public class RequestTimeGatewayFilterFactory extends AbstractGatewayFilterFactory<RequestTimeGatewayFilterFactory.Config> { private static final Log log = LogFactory.getLog(RequestTimeGatewayFilterFactory.class); private static final String REQUEST_TIME_BEGIN = "requestTimeBegin"; private static final String KEY = "withParams"; @Override public List<String> shortcutFieldOrder() { return Arrays.asList(KEY); //给config传参 } public RequestTimeGatewayFilterFactory() { super(Config.class); } @Override public GatewayFilter apply(Config config) { return ((exchange, chain) -> { // 操作前执行 exchange.getAttributes().put(REQUEST_TIME_BEGIN, System.currentTimeMillis()); return chain.filter(exchange).then( // 操作后执行 Mono.fromRunnable(()->{ Long startTime = exchange.getAttribute(REQUEST_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()); } System.out.println(sb.toString()); log.info(sb.toString()); } })); }); } /** * 接收filter参数 */ public static class Config{ private boolean withParams; public boolean isWithParams() { return withParams; } public void setWithParams(boolean withParams) { this.withParams = withParams; } } }
静态内部类类Config就是为了接收参数的,里边的变量名可以随意写,但是要重写List shortcutFieldOrder()这个方法‘
注册RequestTimeGatewayFilterFactory bean
@Bean public RequestTimeGatewayFilterFactory elapsedGatewayFilterFactory() { return new RequestTimeGatewayFilterFactory(); }
application.yml 增加配置
spring:
profiles:
active: elapse_route
---
spring:
cloud:
gateway:
routes:
- id: elapse_route
uri: http://httpbin.org:80/get #前面的路径替换即http://xxx:8081这段
filters: #增加自定义Factory的filter
- RequestTime=true
predicates:
- Method=GET
profiles: elapse_route
测试:curl localhost:8081/customer/123
控制台输出请求时间的日志
global filter
全局过滤器,不用在配置文件中配置,作用在所有路由上
自定义全局路由,不包含token的请求不转发,需要实现GlobalFilter和Ordered接口
/** * 全局过滤器,如果不包含token参数直接返回不路由 */ public class TokenGlobalFilter implements GlobalFilter, Ordered { private static final Log log = LogFactory.getLog(TokenGlobalFilter.class); @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String token = exchange.getRequest().getQueryParams().getFirst("token"); if(token == null || token.isEmpty()){ log.info("token is empty"); exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } return chain.filter(exchange); } @Override public int getOrder() { return -100; } }
注入到容器
/** * 将自定义全局过滤器注入到IOC容器 * @return */ @Bean public TokenGlobalFilter tokenGlobalFilter(){ return new TokenGlobalFilter(); }
测试: curl localhost:8081
控制台打印日志:2020-10-14 22:29:50.985 INFO 107912 --- [ctor-http-nio-2] com.yy.gateway.TokenGlobalFilter : token is empty
Spring Cloud Gateway限流
RequestRateLimiterGatewayFilterFactory这个类,RequestRateLimiter 过滤器可以用于限流,使用RateLimiter实现来确定是否允许当前请求继续进行,如果请求太大默认会返回HTTP 429-太多请求状态。使用Redis和lua脚本实现了令牌桶的方式
pom.xml引入redis依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifatId>spring-boot-starter-data-redis-reactive</artifactId> </dependency>
application.yml
---
spring:
cloud:
gateway:
routes:
- id: limit_route
uri: http://localhost:9600/ #前面的路径替换即http://xxx:8081这段
predicates:
- Method=GET
filters:
- name: RequestRateLimiter
args:
key-resolver: "#{@hostAddrKeyResolver}" #用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象
redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充平均速率, 即平均访问速率每秒
redis-rate-limiter.burstCapacity: 2 #令牌桶总容量 即每秒最大访问速率
profiles: limit_route
redis:
host: 192.168.31.211
port: 6379
KeyResolver需要实现resolve方法,比如根据Hostname进行限流,则需要用hostAddress去判断。实现完KeyResolver之后,需要将这个类的Bean注册到Ioc容器中。
/** * 根据Hostname进行限流,则需要用hostAddress去判断 */ public class HostAddrKeyResolver implements KeyResolver { Log log = LogFactory.getLog(HostAddrKeyResolver.class); @Override public Mono<String> resolve(ServerWebExchange exchange) {return Mono.just(exchange.getRequest().getRemoteAddress().getHostName()); } }
@Bean public HostAddrKeyResolver hostAddrKeyResolver(){ return new HostAddrKeyResolver(); }
多次访问http://localhost:8081,会返回429错误
可以用其他维度限流,例如用户维度
@Bean KeyResolver userKeyResolver(){ return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user")); }
三。服务的注册与发现
上面都是用硬编码方式进行路由转发,gateway可以配合注册中心进行路由转发
pom.xml
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency>
application.yml
server:
port: 8081
spring:
application:
name: sc-gateway-service
cloud:
gateway:
discovery:
locator:
enabled: true
lowerCaseServiceId: true
eureka:
client:
service-url:
defaultZone: http://localhost:8001/eureka/
spring.cloud.gateway.discovery.locator.enabled为true,表明gateway开启服务注册和发现的功能,并且自动根据服务发现为每一个服务创建了一个router,这个router将以服务名开头的请求路径转发到对应的服务。
spring.cloud.gateway.discovery.locator.lowerCaseServiceId是将请求路径上的服务名配置为小写(因为服务注册的时候,向注册中心注册时将服务名转成大写的了),
测试:http://localhost:8081/springcloud-eureka-client/hello 就被转发到http://localhost:9400/helllo
如果不想写服务名来路由,自定义请求路劲
server:
port: 8081
spring:
application:
name: sc_gateway_client
cloud:
gateway:
discovery:
locator:
enabled: false #gateway开启服务注册和发现的功能,并且自动根据服务发现为每一个服务创建了一个router
lower-case-service-id: true #请求路径上的服务名配置为小写
routes:
- id: springcloud-eureka-client
uri: lb://SPRINGCLOUD-EUREKA-CLIENT #服务名
predicates:
- Path=/client/**
filters:
- StripPrefix=1 # 在转发之前将/client去掉
eureka:
client:
service-url:
defaultZone: http://localhost:8001/eureka
现在http://localhost:8081/client/hello 会被转发到http://localhost:9400/hello