Spring Cloud Gateway
简介:
是Spring公司基于Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。
它的目标是替代Netflflix Zuul,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控,限流。
优点:
性能强劲:是第一代网关Zuul的1.6倍
功能强大:内置了很多实用的功能,例如转发、监控、限流等
设计优雅,容易扩展
缺点:
其实现依赖Netty与WebFlux,不是传统的Servlet编程模型,学习成本高
不能将其部署在Tomcat、Jetty等Servlet容器里,只能打成jar包执行
需要Spring Boot 2.0及以上的版本,才支持
笔记1:
基本概念:
路由(Route) 是 gateway 中最基本的组件之一,表示一个具体的路由信息载体。主要定义了下面的几个 信息:
id,路由标识符,区别于其他 Route。
uri,路由指向的目的地 uri,即客户端请求最终被转发到的微服务。
order,用于多个 Route 之间的排序,数值越小排序越靠前,匹配优先级越高。
predicate,断言的作用是进行条件判断,只有断言都返回真,才会真正的执行路由。
fifilter,过滤器用于修改请求和响应信息。
笔记2:
执行流程:
1. Gateway Client向Gateway Server发送请求
2. 请求首先会被HttpWebHandlerAdapter进行提取组装成网关上下文
3. 然后网关的上下文会传递到DispatcherHandler,它负责分发请求给 RoutePredicateHandlerMapping
4. RoutePredicateHandlerMapping负责路由查找,并根据路由断言判断路由是否可用
5. 如果过断言成功,由FilteringWebHandler创建过滤器链并调用
6. 请求会依次经过PreFilter--微服务--PostFilter的方法,最终返回响应
笔记3:
断言工厂:
1.内置路由断言工厂的使用:
<!--nacos客户端,引入依赖即可自动注册微服务-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery
</artifactId>
</dependency>
<!--gateway网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
</dependencies>
server:
port: 7000
spring:
application:
name: service-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway: # 配置网关
discovery:
locator:
enabled: true # 让gateway可以发现nacos中的微服务
routes: # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
- id: product_route # 当前路由的id标识, 要求唯一
# uri: http://localhost:8081 # 请求要转发到的地址(写死不好,利用nacos服务名可以实现动态转发)
uri: lb://service-product # lb指的是从nacos中按照名称获取微服务,并遵循负载均衡策略
order: 1 # 路由的优先级,数字越小级别越高
predicates: # 断言(就是路由转发要满足的条件)
- Path=/product-serv/** # 当请求路径满足Path指定的规则时,才进行路由转发
#此时http://localhost:7000/service-product/product/1或http://localhost:7000/product-serv/product/1皆可访问
# - Before=2019-11-28T00:00:00.000+08:00 #当请求时间在2019-11-28之前
# - Method=POST #当请求方式为POST
filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
- StripPrefix=1 # 转发之前去掉1层路径
# predicates:
# - Path=/product-serv/**
# - Age=18,60 # 限制年龄只有在18到60岁之间的人能访问
# filters: - Strip
测试接口:
总结规律可知:
输入(http://localhost:7000/product-serv/product/1)
满足以 /product-serv 为第一层地址:自动触发路由product_route.访问微服务service-product(http://localhost:7000/service-product);
再拼上输入的地址(http://localhost:7000/service-product+/product-serv/product/1);
并减去1层路径,最终就访问了(http://localhost:7000/service-product/product/1).
2.自定义断言工厂:
创建断言工厂类:
package com.stephen.shopgateway.impl;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
/**
* 用于接收一个配置类,配置类用于接收中配置文件中的配置
*/
@Component
public class AgeRoutePredicateFactory extends AbstractRoutePredicateFactory<AgeRoutePredicateFactory.Config> {
public AgeRoutePredicateFactory() {
super(AgeRoutePredicateFactory.Config.class);
}
//用于从配置文件中获取参数值赋值到配置类中的属性上
@Override
public List<String> shortcutFieldOrder() {
//这里的顺序要跟配置文件中的参数顺序一致
return Arrays.asList("minAge", "maxAge");
}
//断言
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return serverWebExchange -> {
//从serverWebExchange获取传入的参数
String ageParam = serverWebExchange.getRequest().getQueryParams().getFirst("age");
if( StringUtils.isNotBlank(ageParam)){
Integer age = Integer.valueOf(ageParam);
if(age>= config.getMinAge() && age<= config.getMaxAge()){
// 判断请求参数中的age 》=minAge and age <=maxAge
return true;
}
}
return false;
};
}
public static class Config {
private int minAge;
private int maxAge;
public int getMinAge() {
return minAge;
}
public void setMinAge(int minAge) {
this.minAge = minAge;
}
public int getMaxAge() {
return maxAge;
}
public void setMaxAge(int maxAge) {
this.maxAge = maxAge;
}
}
}
配置完访问的时候发生报错:
这是因为Config必须是static修饰的,才能完成初始化:
测试接口:
笔记4:
过滤器:
1 作用: 过滤器就是在请求的传递过程中,对请求和响应做一些处理
2 生命周期:
Pre: 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
Post: 这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP、Header、收集统计信息和指标、将响应从微服务发送给客户端等。
3 分类: 局部过滤器(GatewayFilter,作用在某一个/组路由上) 和 全局过滤器(GlobalFilter,作用全部路由上)
内置过滤器:只要在配置文件添加参数和值即可:
自定义局部过滤器:
package com.stephen.shopgateway.impl;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.List;
//自定义局部过滤器
@Component
public class LogGatewayFilterFactory extends AbstractGatewayFilterFactory<LogGatewayFilterFactory.Config> {
//构造函数
public LogGatewayFilterFactory() { super(LogGatewayFilterFactory.Config.class); }
//读取配置文件中的参数 赋值到 配置类中
@Override
public List<String> shortcutFieldOrder() { return Arrays.asList("consoleLog", "cacheLog"); }
//过滤器逻辑
@Override
public GatewayFilter apply(LogGatewayFilterFactory.Config config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if ( config.isConsoleLog() ) {
System.out.println("consoleLog已经开启了....");
}
if ( config.isCacheLog() ) {
System.out.println("cacheLog已经开启了....");
}
return chain.filter(exchange);
}
};
}
//配置类 接收配置参数
@Data
@NoArgsConstructor
public static class Config {
private boolean consoleLog;
private boolean cacheLog;
}
}
修改配置文件:
自定义全局过滤器:(常用语鉴权,加密或判断token)
package com.stephen.shopgateway.impl;
import org.apache.commons.lang3.StringUtils;
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.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
//自定义全局过滤器需要实现GlobalFilter和Ordered接口
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
//完成判断逻辑
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getQueryParams().getFirst("token");
if ( StringUtils.isBlank(token) ) {
System.out.println("鉴权失败");
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
//放行:调用chain.filter继续向下游执行
return chain.filter(exchange);
}
//顺序,数值越小,优先级越高
@Override
public int getOrder() {
return 0;
}
}
测试:不加token访问:
添加token:
笔记5:
网关限流:
网关是所有请求的公共入口,所以可以进行网关限流,而且限流的方式也很多,这里主要是用Sentinel组件来实现网关的限流。Sentinel支持对SpringCloud Gateway、Zuul等主流网关进 行限流。
限流的2种方式:
1.route路由:即在Spring配置文件中配置的路由条目,资源名为对应的routeId.(粗限流)
2.自定义API:用户可以利用Sentinel提供的API来自定义一些API分组.(细限流)
基于Sentinel 的Gateway限流技术是通过其提供的Filter来完成的,使用时只需注入对应的 SentinelGatewayFilter实例以及 SentinelGatewayBlockExceptionHandler 实例即可。
自定义API分组:
配置路由:
#自定义api分组路由
- id: product_api1
uri: lb://service-product
predicates:
- Path=/product-service/product/api1/**
filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
- RewritePath=/product-service/product/api1/(?<segment>.*), /product/$\{segment} # 路径重写的过滤器
- id: product_api2
uri: lb://service-product
predicates:
- Path=/product-service/product/api2/demo1/**
filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
- RewritePath=/product-service/product/api2/demo1/(?<segment>.*), /product/$\{segment} # 路径重写的过滤器
package com.stephen.shopgateway.config;
import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPathPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.GatewayApiDefinitionManager;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import java.util.*;
@Configuration
public class GatewayConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
// 初始化一个限流的过滤器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() { return new SentinelGatewayFilter(); }
// 配置初始化的限流参数
@PostConstruct
public void initGatewayRules() {
//根据路由id来限流
// Set<GatewayFlowRule> rules = new HashSet<>();
// rules.add(new GatewayFlowRule("product_route")//资源名称,对应路由id
// .setCount(1)//限流阈值
// .setIntervalSec(1)//统计时间窗口,单位是秒,默认是 1秒
// );
// GatewayRuleManager.loadRules(rules);//加载新的限流规则
//创建API分组来细化限流
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(new GatewayFlowRule("product_api1").setCount(1).setIntervalSec(1));
rules.add(new GatewayFlowRule("product_api2").setCount(1).setIntervalSec(1));
GatewayRuleManager.loadRules(rules);//加载新的限流规则
}
// 配置限流的异常处理器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
// 自定义限流异常页面
@PostConstruct
public void initBlockHandlers() {
// BlockHandler异常处理
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
Map map = new HashMap<>();
map.put("code", 0);
map.put("message", "接口被限流了");
return ServerResponse.status(HttpStatus.OK).
contentType(MediaType.APPLICATION_JSON_UTF8).body(BodyInserters.fromObject(map));
}
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
// 自定义API分组
@PostConstruct
private void initCustomizedApis() {
Set<ApiDefinition> definitions = new HashSet<>();
ApiDefinition api1 = new ApiDefinition("product_api1").setPredicateItems(new HashSet<ApiPredicateItem>() {{
// 以 / product - serv / product / api1 开头的url路径请求
//.setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX)推测是用于控制限流的,当出现限流后不再重启服务
add(new ApiPathPredicateItem().setPattern("/product-service/product/api1/**"));
}});
ApiDefinition api2 = new ApiDefinition("product_api2").setPredicateItems(new HashSet<ApiPredicateItem>() {
{
// 以 / product - serv / product / api2 / demo1 完全匹配的url路径请求
//不添加 //.setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX)则服务在时间阈值后会重启服务
add(new ApiPathPredicateItem().setPattern("/product-service/product/api2/demo1/**").setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}
});
definitions.add(api1);
definitions.add(api2);
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}
}
快速测试接口发现:
http://localhost:7000/product-service/product/api1/1?age=20&token=12345 快速访问会限流,在统计时间阈值(1s)过后会恢复服务
http://localhost:7000/product-service/product/api2/demo1/1?age=20&token=12345 快速访问会限流,且之后不再恢复服务