Spring Cloud Gateway是Spring Cloud官方推出的第二代网关框架,取代Zuul 网关。网关作为流量的,在微服务系统中有着非常作用。据说性能是第一代网关 zuul的1.5倍。(基于Netty,WebFlux), 注意点:由于不是Sevlet容器,所以他不能打成war包, 只支持SpringBoot2.X不 支持1.x
1.1)网关作用: 网关常见的功能有路由转发、权限校验、限流控制等作用。
1.2)为什么要使用SpringCloudGateWay。
①:没有网关
②:使用了网关
2.1)创建一个gateWay的工程08-ms-cloud-gateway
①添加依赖:
org.springframework.cloud
spring‐cloud‐starter‐gateway
com.alibaba.cloud
spring‐cloud‐alibaba‐nacos‐discovery
org.springframework.boot
spring‐boot‐starter‐actuator
②:写配置文件
#规划GateWay的服务端口
server:
port: 8888
#规划gateWay注册到到nacos上的服务应用名称
spring:
application:
name: api‐gateway
cloud:
nacos:
discovery:
#gateway工程注册到nacos上的地址
server‐addr: localhost:8848
gateway:
discovery:
locator:
#开启gateway从nacos上获取服务列表
enabled: true
#开启acutor端点
management:
endpoints:
web:
exposure:
include: '*'
endpoint:
health:
#打开端点详情
show‐details: always
③:写注解 服务发现的注解,gateway没有注解
@SpringBootApplication
@EnableDiscoveryClient
public class MsCloudGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(MsCloudGatewayApplication .class, args);
}
}
2.2)测试网关工程,分别启动
08-ms-cloud-gateway(8888),
08-ms-alibaba-gateway-order(8080)
08-ms-alibaba-gateway-product(8084)
通过网关地址访问订单微服务
http://localhost:8888/order-center/selectOrderInfoById/1
通过网关地址访问库存微服务
http://localhost:8888/product-center/selectProductInfoById/1
转发规则:
3.1)基本核心概念.
路由网关的基本构建模块,它是由ID、目标URl、断言集合和过滤器集合定义, 如果集合断言为真,则匹配路由。
Predicate(断言):这是java 8的一个函数式接口predicate,可以用于lambda表 达式和方法引用,输入类型是:Spring Framework ServerWebExchange,允许 开发人员匹配来自HTTP请求的任何内容,例如请求头headers和参数paramers
Filter(过滤器):这些是使用特定工厂构建的Spring Framework GatewayFilter 实例,这里可以在发送下游请求之前或之后修改请求和响应
如下配置:
含义:我们浏览器 http://localhost:8888/projects/** 都会转发到 http://spring.io/projects/**下 并且带入响应头部: X-Response-Foo=Bar
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: add_request_header_route
uri: http://spring.io
predicates:
- Path=/projects/**
filters:
- AddResponseHeader=X-Response-Foo, Bar
3.2)路由断言工厂
https://cloud.spring.io/spring-cloud-gateway/2.1.x/multi/multi_spring-cloud-gateway.html
3.3)自定义谓词工厂
第一步:写一个自定义谓词工厂,类名必须要以RoutePredicateFactory结尾 然后继承AbstractRoutePredicateFactory
@Component
@Slf4j
public class MyTimeBetweenRoutePredicateFactory extends AbstractRoute
PredicateFactory {
public MyTimeBetweenRoutePredicateFactory() {
super(MyTimeBetweenConfig.class);
}
//真正的业务判断逻辑
@Override
public Predicate apply(MyTimeBetweenConfig confi
{
LocalTime startTime = config.getStartTime();
LocalTime endTime = config.getEndTime();
return new Predicate(){
@Override
public boolean test(ServerWebExchange serverWebExchange) {
LocalTime now = LocalTime.now();
//判断当前时间是否在在配置的时间范围类
return now.isAfter(startTime) && now.isBefore(endTime);
}
};
}
//用于接受yml中的配置 ‐ MyTimeBetween=上午7:00,下午11:00
public List shortcutFieldOrder() {
return Arrays.asList("startTime", "endTime");
}
}
第二步:书写一个配置类,用于接受配置
//写一个类用于接受配置
@Data
public class MyTimeBetweenConfig {
private LocalTime startTime;
private LocalTime endTime;
}
第三步:在yml配置中
谓词配置是以我们自定义类名MYTimeBetweenRoutePredicateFactory 去除了RoutePredicateFactory接受开头MyTimeBetween
spring:
cloud:
gateway:
routes:
- id: my-timeBetween #id必须要唯一
uri: lb://product-center
predicates:
#当前请求的时间必须在早上7点到 晚上11点 http://localhost:8888/selectProduct
InfoById/1
#才会被转发
#到http://product-center/selectProductInfoById/1
- TulingTimeBetween=上午7:00,下午11:00
3.4)过滤器工厂,SpringCloudGateway 内置了很多的过滤器工厂,我 们通过一些过滤器工厂可以进行一些业务逻辑处理器,比如添加剔除响 应头,添加去除参数等.
https://cloud.spring.io/spring-cloud-gateway/2.1.x/multi/multi__gatewayfilter_factories.html#_addrequestheader_gatewayfilter_factory
这里拿出几个来演示。
1 spring:
2 cloud:
3 gateway:
4 routes:
5 ‐ id: my‐timeBetween #id必须要唯一
6 uri: lb://product‐center
7 predicates:
8 #当前请求的时间必须在早上7点到 晚上11点 http://localhost:8888/selectProduct
InfoById/1
9 #才会被转发
10 #到http://product‐center/selectProductInfoById/1
11 ‐ MyTimeBetween=上午7:00,下午11:00
12 filters:
13 ‐ AddRequestHeader=X‐Request‐Company,my
测试:http://localhost:8888/gateWay4Header
@RequestMapping("/gateWay4Header")
public Object gateWay4Header(@RequestHeader("X‐Request‐Company") String c
ompany) {
return "gateWay拿到请求头"+company;
}
②:添加请求参数
spring:
cloud:
gateway:
routes:
‐ id: tuling‐timeBetween #id必须要唯一
uri: lb://product‐center
predicates:
‐ TulingTimeBetween=上午7:00,下午11:00
filters:
‐ AddRequestParameter=company, tuling
测试地址:http://localhost:8888/gateWay4RequestParam
@RequestMapping("/gateWay4RequestParam")
public Object gateWay4RequestParam(@RequestParam("company") String compan
y) {
return "gateWay拿到请求参数"+company;
}
③:为匹配的路由统一添加前缀
spring:
cloud:
gateway:
routes:
‐ id: tuling‐timeBetween #id必须要唯一
uri: lb://product‐center
predicates:
‐ TulingTimeBetween=上午7:00,下午11:00
filters:
‐ PrefixPath=/product‐api
#比如
http://localhost:8888/selectProductInfoById/1
会转发到路径
http://product‐center/product‐api/selectProductInfoById/1
我们的product-center的需要添加一段配置:
server:
servlet:
context‐path: /product‐api
测试地址:http://localhost:8888/selectProductInfoById/2
更多的配置 具体查看官网 已经详细的列出了20多种. https://cloud.spring.io/spring-cloud-gateway/2.1.x/multi/multi__gatewayfilter_factories.html
④:自定义过滤器工厂 继承AbstractNameValueGatewayFilterFactory 且我们的自定义名称必须要以GatewayFilterFactory结尾
@Slf4j
@Component
public class TimeMonitorGatewayFilterFactory extends AbstractNameValueGat
ewayFilterFactory {
private static final String COUNT_START_TIME = "countStartTime";
@Override
public GatewayFilter apply(NameValueConfig config) {
return new GatewayFilter() {
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain
chain) {
//获取配置文件yml中的
filters:
‐ TimeMonitor=enabled,true
String name = config.getName();
String value = config.getValue();
log.info("name:{},value:{}",name,value);
if(value.equals("false")) {
return null;
}
exchange.getAttributes().put(COUNT_START_TIME,
System.currentTimeMillis());
//then方法相当于aop的后置通知一样
return chain.filter(exchange).then(Mono.fromRunnable(new Runnable() {
@Override
public void run() {
Long startTime = exchange.getAttribute(COUNT_START_TIME);
if (startTime != null) {
StringBuilder sb = new StringBuilder(exchange.getRequest().getURI().get
RawPath())
.append(": ")
.append(System.currentTimeMillis() ‐ startTime)
.append("ms");
sb.append(" params:").append(exchange.getRequest().getQueryParams());
log.info(sb.toString());
}
}
}));
}
};
}
}
访问打印的日志
缺陷: 通过自定义过滤器工程创建出来的过滤器是不能指定优先级的, 只能根据配置的先后顺序执行,若向指定优先级怎么办?
我们需要稍微改动一下代码: 写一个自定义的内部类实现 GateWayFilter接口 和ordered接口,
@Slf4j
@Component
public class TimeMonitorGatewayFilterFactory extends AbstractNameValueGat
ewayFilterFactory {
private static final String COUNT_START_TIME = "countStartTime";
@Override
public GatewayFilter apply(NameValueConfig config) {
return new TimeMonitorGatewayFilter(config);
}
/**
* 我们自己写一个静态内部类 实现GatewayFilter,Ordered 通过Orderd可以实现顺序
的控制
*/
public static class TimeMonitorGatewayFilter implements GatewayFilter,O
rdered{
private NameValueConfig nameValueConfig;
public TimeMonitorGatewayFilter(NameValueConfig nameValueConfig) {
this.nameValueConfig = nameValueConfig;
}
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain
chain) {
String name = nameValueConfig.getName();
String value = nameValueConfig.getValue();
log.info("name:{},value:{}",name,value);
if(value.equals("false")) {
return null;
}
exchange.getAttributes().put(COUNT_START_TIME,
System.currentTimeMillis());
//then方法相当于aop的后置通知一样
return chain.filter(exchange).then(Mono.fromRunnable(new Runnable() {
@Override
public void run() {
Long startTime = exchange.getAttribute(COUNT_START_TIME);
if (startTime != null) {
StringBuilder sb = new StringBuilder(exchange.getRequest().getURI().get
RawPath())
.append(": ")
.append(System.currentTimeMillis() ‐ startTime)
.append("ms");
sb.append(" params:").append(exchange.getRequest().getQueryParams());
log.info(sb.toString());
}
}
}));
}
@Override
public int getOrder() {
return ‐100;
}
}
}
⑤:自定义全局过滤器,所有的请求都会经过全局过滤器 实现GlobalGateWayFilter ,那么所有的请求都会经过gateway 业务场景中。请求中必须带入token才会被转发.
/**
* 全局过滤器校验请求头中的token
* Created by smlz on 2019/12/17.
*/
@Component
@Slf4j
public class AuthGateWayFilter implements GlobalFilter,Ordered {
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain
chain) {
List token = exchange.getRequest().getHeaders().get("token");
if(StringUtils.isEmpty(token)) {
return null;
}else {
log.info("token:{}",token);
return chain.filter(exchange);
}
}
@Override
public int getOrder() {
return 0;
}
}
⑥:SpringCloudGateWay+Sentinel1.6.3(以上版本) 解释?为啥要1.6.3版本,若低于1.6.3版本的话,需要在gateway工程 进行大量的编码进行设置流控的规则。
若1.6.3版本以上,我们就可以通过sentinel页面进行配置规则
名称解释:
GatewayFlowRule:网关限流规则,针对 API Gateway 的场景定制的限流规 则,可以针对不同 route 或自定义的 API 分组进行限流,支持针对请求中的参 数、Header、来源 IP 等进行定制化的限流
ApiDefinition:用户自定义的 API 定义分组,可以看做是一些 URL 匹配的组 合。比如我们可以定义一个 API 叫 my_api,请求 path 模式 为 /foo/** 和 /baz/** 的都归到 my_api 这个 API 分组下面。限流的时候可以 针对这个自定义的 API 分组维度进行限流
resource:资源名称,可以是网关中的 route 名称或者用户自定义的 API 分组 名称。 resourceMode:规则是针对 API Gateway 的 route(RESOURCE_MODE_ROUTE_ID)还是用户在 Sentinel 中定义的 API 分组(RESOURCE_MODE_CUSTOM_API_NAME),默认是 route。
grade:限流指标维度,同限流规则的 grade 字段。 count:限流阈值
intervalSec:统计时间窗口,单位是秒,默认是 1 秒。
controlBehavior:流量整形的控制效果,同限流规则的 controlBehavior 字 段,目前支持快速失败和匀速排队两种模式,默认是快速失败。
burst:应对突发请求时额外允许的请求数目。
axQueueingTimeoutMs:匀速排队模式下的最长排队时间,单位是毫秒,仅 在匀速排队模式下生效。
paramItem:参数限流配置。若不提供,则代表不针对参数进行限流,该网关 规则将会被转换成普通流控规则;否则会转换成热点规则。其中的字段: parseStrategy:从请求中提取参数的策略,目前支持提取来源 IP(PARAM_PARSE_STRATEGY_CLIENT_IP)、 Host(PARAM_PARSE_STRATEGY_HOST)、任意 Header(PARAM_PARSE_STRATEGY_HEADER)和任意 URL 参数 (PARAM_PARSE_STRATEGY_URL_PARAM)四种模式。
fieldName:若提取策略选择 Header 模式或 URL 参数模式,则需要指定对应 的 header 名称或 URL 参数名称。 pattern:参数值的匹配模式,只有匹配该模式的请求属性值会纳入统计和流 控;若为空则统计该请求属性的所有值。(1.6.2 版本开始支持)
matchStrategy:参数值的匹配策略,目前支持精确匹配 (PARAM_MATCH_STRATEGY_EXACT)、子串匹配 (PARAM_MATCH_STRATEGY_CONTAINS)和正则匹配 (PARAM_MATCH_STRATEGY_REGEX)。(1.6.2 版本开始支持) 用户可以通过 GatewayRuleManager.loadRules(rules) 手动加载网关规则,或 通过 GatewayRuleManager.register2Property(property) 注册动态规则源动 态推送(推荐方式)。
GateWay+Sentinel1.6.3版本整合
a)创建工程08-ms-cloud-gateway-sentinel
导入依赖:
com.alibaba.cloud
spring‐cloud‐alibaba‐nacos‐discovery
org.springframework.cloud
spring‐cloud‐starter‐gateway
org.springframework.boot
spring‐boot‐starter‐webflux
com.alibaba.csp
sentinel‐spring‐cloud‐gateway‐adapter
com.alibaba.csp
sentinel‐transport‐simple‐http
增加配置类
@Configuration
public class GatewayConfiguration {
private final List viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfiguration(ObjectProvider> viewResol
versProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::e
mptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExcepti
onHandler() {
// Register the block exception handler for Spring Cloud Gateway.
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCo
decConfigurer);
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
}
增加yml的配置
server:
port: 8888
spring:
application:
name: gateway-sentinel
cloud:
gateway:
discovery:
locator:
lower-case-service-id: true
enabled: true
routes:
- id: product_center
uri: lb://product-center
predicates:
- Path=/product/**
- id: order_center
uri: lb://order-center
predicates:
- Path=/order/**
nacos:
discovery:
server-addr: localhost:8848
打开sentinel的控制台,由于sentinel的控制台第一次打开没有,你需 要分别请求一下路径 http://localhost:8888/product/selectProductInfoById/2 http://localhost:8888/order/selectOrderInfoById/1
就会生成如下的流控节点
添加流控规则(如下三个 测试不出效果) 他的本意是是控制调用网关的 ip是指定的Ip进行控制 Cookie选项中意思就是每次请求中带入指定的cookie k v 就会被限 流,不带就会被限制流量。(但是效果测试不出) 而Header模型 :指定是请求中带入的特定的header kv指就会被限流 URL参数:同理,也会针对请求参数名称进行限制流量。
现在我们测试Header模式,如下配置
频繁的请求如下地址:
现在测试
测试:
业务场景:我们一个工程有多个请求的api,但是可能存在一种可能就 是不同的
api的请求控制不一样,怎么办,那么sentienl的routeId模式流控达 不到效果了。
比如:08-ms-alibaba-gateway-product工程中有三个api /product/selectProductInfoById/{productNo}
/product/gateWay4Header
/product/gateWay4RequestParam
若通过如下这种配置流控规则,不能做到细粒度配置,那么如何做?
自定义APi分组,我们把 如下的api进行分组
/product/selectProductInfoById/{productNo}
/product/gateWay4Header
/product/gateWay4RequestParam
通过API类型选择api分组可以做到细粒度配置.
GateWay+Sentienl全局异常处理。
Sentinel默认的情况下使用的是SentinelGatewayBlockExceptionHandler进行处理, 我们只需要 而我们的SentinelGatewayBlockExceptionHandler底层调用了我们的 BlockRequestHandler接口的实现类DefaultBlockRequestHandler,而我们只需要自己写 一个类继承父类就可以进行自定义异常处理
@Component
public class TulingBlockRequestHandler extends DefaultBlockRequestHandler
{
private static final String DEFAULT_BLOCK_MSG_PREFIX = "Blocked by Senti
nel: ";
//处理异常的
@Override
public Mono handleRequest(ServerWebExchange exchange, Th
rowable ex) {
//处理html错误类型的
if (acceptsHtml(exchange)) {
return htmlErrorResponse(ex);
}
//处理Json类型的
// JSON result by default.
return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(fromObject(buildErrorResult(ex)));
}
private Mono htmlErrorResponse(Throwable ex) {
return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
.contentType(MediaType.TEXT_PLAIN)
.syncBody(new String(JSON.toJSONString(buildErrorResult(ex))));
}
private TulingBlockRequestHandler.ErrorResult buildErrorResult(Throwabl
e ex) {
if(ex instanceof ParamFlowException) {
return new TulingBlockRequestHandler.ErrorResult(HttpStatus.TOO_MANY_RE
QUESTS.value(),"block");
}else if (ex instanceof DegradeException) {
return new TulingBlockRequestHandler.ErrorResult(HttpStatus.TOO_MANY_RE
QUESTS.value(),"fallback");
}else{
return new
TulingBlockRequestHandler.ErrorResult(HttpStatus.BAD_GATEWAY.value(),"gatew
ay error");
}
}
/**
* Reference from {@code DefaultErrorWebExceptionHandler} of Spring
Boot.
*/
private boolean acceptsHtml(ServerWebExchange exchange) {
try {
List acceptedMediaTypes =
exchange.getRequest().getHeaders().getAccept();
acceptedMediaTypes.remove(MediaType.ALL);
MediaType.sortBySpecificityAndQuality(acceptedMediaTypes);
return acceptedMediaTypes.stream()
.anyMatch(MediaType.TEXT_HTML::isCompatibleWith);
} catch (InvalidMediaTypeException ex) {
return false;
}
}
private static class ErrorResult {
private final int code;
private final String message;
ErrorResult(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}
} if (exchange.getResponse().isCommitted()) {
return Mono.error(ex);
}
// This exception handler only handles rejection by Sentinel.
if (!BlockException.isBlockException(ex)) {
return Mono.error(ex);
}
return handleBlockedRequest(exchange, ex)
.flatMap(response ‐> writeResponse(response, exchange));
}
private Mono handleBlockedRequest(ServerWebExchange exc
hange, Throwable throwable) {
return GatewayCallbackManager.getBlockHandler().handleRequest(exchange,
throwable);
}
private final Supplier contextSupplier = () ‐>
new ServerResponse.Context() {
@Override
public List> messageWriters() {
return TulingSentinelGatewayBlockExceptionHandler.this.messageWriters;
}
@Override
public List viewResolvers() {
return TulingSentinelGatewayBlockExceptionHandler.this.viewResolvers;
}
};
private Mono writeResponse(ServerResponse response, ServerWebExch
ange exchange) {
String reqPath = exchange.getRequest().getPath().value();
Map retMap = new HashMap<>();
ServerHttpResponse serverHttpResponse = exchange.getResponse();
serverHttpResponse.getHeaders().add("Content‐Type", "application/json;c
harset=UTF‐8");
retMap.put("msg","被限流拉");
retMap.put("code","‐1");
retMap.put("reqPath",reqPath);
ObjectMapper objectMapper = new ObjectMapper();
byte[] datas = new byte[0];
try {
datas = objectMapper.writeValueAsString(retMap).getBytes(StandardCharse
ts.UTF_8);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
DataBuffer buffer = serverHttpResponse.bufferFactory().wrap(datas);
return serverHttpResponse.writeWith(Mono.just(buffer));
}
}