SpringCloud – GateWay
官方文档位置: https://spring.io/projects/spring-cloud-gateway#learn
推荐有空看看纯英文版的
SpringCloud --Gateway 简介
1. 历史 – Zuul 和Gateway
Gateway 是springcloud旗下的一个子项目,而Zuul 是 Netflix 旗下的一个开源的项目
Spring 将 Zuul 集成在了 Spring Cloud 中,Zuul第二代项目孵化失败,断更,于是Spring 开发了自己的项目—Gateway
2. Gateway 介绍
是基于spring5、springboot2.0和Project Reactor等技术开发的网关,目的是为微服务架构系统提供高性能,且简单易用的api路由管理方式
优点:
1:性能强劲,是第一代网关zuul的1.6倍
2:功能强大,内置很多实用功能如:路由、过滤、限流、监控等
3:易于扩展
3. 网关位置及作用
- 由图可见,网关实际上就是整个微服务项目的入口,其基本的职责就是路由,转发请求到我们的微服务中,同时我们可以在网关层面做全局的限流和权限的控制,以及一些全局的认证操作,其本身的位置决定了他可以作为微服务的中心化管理者
- 网关实际上也是一个微服务,也需要注册到Nacos上,作为注册和发现,其可以动态感知我们其他微服务的上线和下线,并可以通过服务名serviceName来调用其他的微服务,避免了使用nginx反向代理的硬编码问题,同时通过我们前面学习的Nacos的知识可以推论出: 由服务名可以获取微服务的服务列表,并可以通过Ribbon来实现调用的负载均衡
- 总结: 明确两点,其一,gateway也是一个微服务 ;其二 , 他是整个微服务的唯一入口
4. Gateway的核心概念
a. Route(路由)
路由是网关构建的基本模块,它有id(一般是被调用的服务名),uri(一般采取lb协议,后跟服务名),Predicates(断言)以及Filters(一组过滤器)组成,断言为真的请求方可调用微服务,Filters可以对我们的请求做相应的处理,以实现一些功能
- 以下示例:
server:
port: 8040
spring:
application:
name: cloud-gateway
cloud:
# 网关配置路由
gateway:
routes:
# id一般就是服务名,这样的可读性较高
- id: cloud-order
# 注意点: 不要直接写 ip + 端口 写 lb协议,load balance 可实现动态感知与负载均衡
# uri: http://localhost:9002
uri: lb://cloud-order
predicates:
- Path=/order/**
- After=2021-10-05T18:26:04.344+08:00[Asia/Shanghai]
- MyHeader=token,123
- id: cloud-goods
uri: lb://cloud-goods
predicates:
- Path=/goods/**
- After=2021-10-05T18:26:04.344+08:00[Asia/Shanghai]
- MyHeader=token
filters:
- AddRequestHeader=foo,lizhimeng
- CalTime=a,b # 值必须要给,不能为空
- id: cloud-jifen
uri: lb://cloud-jifen
predicates:
- Path=/jifen/**
第一个网关路由解读:
配置的是服务名为:cloud-order的微服务
通过服务名cloud-order获取其服务列表,并通过Ribbon来实现服务调用的负载均衡
断言:
凡是请求中带有/order/ 的即可匹配,断言为真
凡是在2021.10.5号下午18:26:04秒之后发送的请求,断言为真
凡是请求头中包含键值对(“token”,123)的请求,断言为真(此为自定义断言)
以上三个断言均为真,请求方可通过,否则打回
b. Predicates(断言)
这是一个 JAVA 8 的 Predicate ,输入类型是一个 ServerWebExchange;我们可以使用它来匹配来自 HTTP 请求的任何内容,例如 headers 或参数,断言为真方的请求方可通过,反之打回
c. Filters(过滤器)
这是org.springframework.cloud.gateway.filter.GatewayFilter的实例,我们可以使用它修改请求和响应
5. 搭建Gateway网关
1. pom依赖
<!-- 网关起步依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- 端点监控 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- 网关也属于一个微服务,一样需要注册到 nacos 上 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
明确一点:
网关实际上也是一个微服务,所以其必须注册到nacos上
注意:
依赖了spring-cloud-starter-gateway,一定不要依赖spring-boot-starter-web了,否则直接报错
gateway是基于Reactor的,底层是netty,而springMVC是基于servlet的他需要tomcat容器,所以两个依赖不能同时存在
2. bootstrap.yml配置
spring:
cloud:
nacos:
# nacos配置中心
config:
# 地址,用户名,密码
server-addr: localhost:8848
username: nacos
password: nacos
# 命名空间,在父工程中properties中统一指定,直接调用
namespace: @environment@ #pro
# 分组
group: DEFAULT_GROUP
# 文件名由前置名,后缀名,和环境组成,拼起来就是: cloud-gateway-pro.yml
prefix: cloud-gateway
file-extension: yml
# 读取共享配置文件
shared-configs:
- common.yml
# 配置文件动态刷新
refreshable-dataids: commmon.yml
profiles:
active: @environment@
上面已经阐述了,我们的网关实际上也是一个微服务,所以同样需要注册到nacos中,这里的配置是从远程nacos中去获取配置文件
- nacos上生产环境的通用配置文件,可以复习复习前面的知识
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848
username: nacos
password: nacos
namespace: pro
group: pro-group
sentinel:
transport:
port: 8719
dashboard: localhost:8888
eager: true
web-context-unify: false
datasource:
flow:
nacos:
server-addr: ${nacos.server-addr}
username: ${nacos.username}
password: ${nacos.password}
namespace: ${nacos.namespace}
groupId: SENTINEL_GROUP
dataId: ${spring.application.name}-flow-rules
rule-type: flow
degrade:
nacos:
server-addr: ${nacos.server-addr}
username: ${nacos.username}
password: ${nacos.password}
namespace: ${nacos.namespace}
groupId: SENTINEL_GROUP
dataId: ${spring.application.name}-degrade-rules
rule-type: degrade
param-flow:
nacos:
server-addr: ${nacos.server-addr}
username: ${nacos.username}
password: ${nacos.password}
namespace: ${nacos.namespace}
groupId: SENTINEL_GROUP
dataId: ${spring.application.name}-param-rules
rule-type: param-flow
system:
nacos:
server-addr: ${nacos.server-addr}
username: ${nacos.username}
password: ${nacos.password}
namespace: ${nacos.namespace}
groupId: SENTINEL_GROUP
dataId: ${spring.application.name}-system-rules
rule-type: system
authority:
nacos:
server-addr: ${nacos.server-addr}
username: ${nacos.username}
password: ${nacos.password}
namespace: ${nacos.namespace}
groupId: SENTINEL_GROUP
dataId: ${spring.application.name}-authority-rules
rule-type: authority
nacos:
server-addr: localhost:8848
username: nacos
password: nacos
namespace: sentinel
3. 引导类
@SpringBootApplication
@EnableDiscoveryClient
public class GateWayApp {
public static void main(String[] args) {
SpringApplication.run(GateWayApp.class,args);
}
}
引导类与其他微服务的引导类没有什么区别
至此:一个基本的网关搭建完毕,只要在网关中配置过了的微服务,以后的访问都需要通过网关来调用,符合网关断言的,通过一系列过滤器的请求才会来到相应的微服务中
6. 路由
SpringCloud Gateway的路由配置有两种:
- 静态路由,硬编码路由的uri
- 动态路由,通过服务名去查找服务列表,动态获取uri
当然是选择动态路由拉
7. 谓词工厂
Spring Cloud提供了众多的Predicate谓词工厂,用来断言请求是否可以通过,其中的 test()方法就是专门来判断断言是否为真的,许多时候还需要我们自定义谓词来实现一些功能
自定义谓词工厂步骤
目标:yml配置中指定自定义断言,内容为:
-MyHeader=token 或者-MyHeader=token,123
token方可通过断言,要么没有值,若有值必须是123方可通过
1. 准备工作,自定义一个类用于接收数据
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MyConfig {
private String key;
private String value;
}
- 一会解释这个类的作用
2. 自定义类,继承AbstractRoutePredicateFactory,注意后缀必须是"RoutePredicateFactory"
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.GatewayPredicate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
/**
* 自定义谓词工厂 注意点: 后缀必须是RoutePredicateFactory
*/
@Component
public class MyHeaderRoutePredicateFactory extends AbstractRoutePredicateFactory<MyConfig> {
public MyHeaderRoutePredicateFactory() {
// 直接定义为我们自定义的类
super(MyConfig.class);
}
@Override
public Predicate<ServerWebExchange> apply(MyConfig config) {
// 返回true表示谓词成功,false表示不成功
return new GatewayPredicate() {
@Override
public boolean test(ServerWebExchange serverWebExchange) {
if (StringUtils.isEmpty(config.getValue())){
return serverWebExchange.getRequest().getHeaders().containsKey(config.getKey());
}
//获取header中的数据 通过交换机获取请求,从请求中获取请求头,再在请求头中获取第一个
// 用配置类中的key去获取value
String value = serverWebExchange.getRequest().getHeaders().getFirst(config.getKey());
//非空判断
if (StringUtils.isEmpty(value)){
return false;
}else {
// 不为空
// 判断value是否相等
if (value.equals(config.getValue())){
return true;
}else {
return false;
}
}
}
};
}
/**
* 之后在配置yml中写法:
* MyHeader: name,XX
* 下面的方法就是将此字符串根据逗号切割
* 将name赋值给key属性 将XX赋值给value属性
*/
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("key","value");
// 后台操作,反射
// - MyHeader=token,123
// list{"key","value"}
}
}
AbstractRoutePredicateFactory类有一个泛型,该泛型即是我们自定义的MyConfig类,在重写的shortcutFieldOrder()方法中,指定了我们在yml中定义的断言内容,通过逗号截取之后放到一个数组中,AbstractRoutePredicateFactory底层会按照数组中的内容寻找到MyConfig的字段,再将值进行映射,从而赋值
8. 过滤器
SpringCloud的Gateway提供了丰富的过滤器供我们使用,但在开发中,一般来说我们也需要根据业务来自定义过滤器来使用
如图所示为SpringCloud Gateway的流程:
根据我们的yml中的配置,可以配置多个过滤器,形成一个过滤器链
过滤器的执行有先后顺序,请求通过过滤器到达微服务,之后响应时也需要再经过一次过滤器
- 内置过滤器的使用—yml配置
spring:
cloud:
# 网关配置路由
gateway:
routes:
- id: cloud-goods
uri: lb://cloud-goods
predicates:
- Path=/goods/**
- After=2021-10-05T18:26:04.344+08:00[Asia/Shanghai]
- MyHeader=token
filters:
# 请求头过滤器,请求头放入了(foo,lizhimeng)的键值对
- AddRequestHeader=foo,lizhimeng
- 内置过滤器的使用—获取参数
@RequestMapping("info/{id}")
public Goods info(@PathVariable Integer id , @RequestHeader("foo") String foo) throws InterruptedException {
Thread.sleep(2000);
System.out.println(foo);
return new Goods(id,"小米"+port+"请求头数据为: "+foo);
}
8.1 自定义局部过滤器
目的: 统计一个微服务方法的响应时间
1. 准备工作,自定义一个类用于接收配置的数据
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MyConfig {
private String key;
private String value;
}
2. 自定义类,继承AbstractGatewayFilterFactory,注意后缀名必须是"GatewayFilterFactory"
package com.qf.filters;
import com.qf.predicates.MyConfig;
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.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.List;
public class CalTimeFilterGatewayFilterFactory extends AbstractGatewayFilterFactory {
public CalTimeFilterGatewayFilterFactory() {
super(MyConfig.class);
}
@Override
public GatewayFilter apply(Object config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 前处理
System.out.println("CalTimeGatewayFilterFactory的前处理");
// 记录开始时间
long beginTime = System.currentTimeMillis();
// 放行是不要.then , 要后面的就是编写后置处理的内容
return chain.filter(exchange).then(
// 后置处理
// 以下为React编程模型
Mono.fromRunnable(() -> {
//记录调用微服务完成之后的时间
long endTime = System.currentTimeMillis();
//将两个时间相减, 得到调用微服务的时间
System.out.println("调用时间为: " + (endTime - beginTime));
})
);
}
};
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("key","value");
}
}
- 实际上也可以继承AbstractGatewayFilterFactory的实现类AbstractNameValueGatewayFilterFactory,代码实现:
package com.qf.filters;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractNameValueGatewayFilterFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* 自定义网关过滤器 目的 :计算微服务的运行时间
* 自定义网关过滤器后缀规范: GatewayFilterFactory (与自定义路由谓词 RoutePredicateFactory一样,约定大于配置)
*/
@Component
public class CalTimeGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
@Override
public GatewayFilter apply(NameValueConfig config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 前处理
System.out.println("CalTimeGatewayFilterFactory的前处理");
String name = config.getName();
String value = config.getValue();
// 记录开始时间
long beginTime = System.currentTimeMillis();
// 放行是不要.then , 要后面的就是编写后置处理的内容
return chain.filter(exchange).then(
// 后置处理
// 以下为React编程模型
Mono.fromRunnable(() -> {
//记录调用微服务完成之后的时间
long endTime = System.currentTimeMillis();
//将两个时间相减, 得到调用微服务的时间
System.out.println("调用时间为: " + (endTime - beginTime));
})
);
}
};
}
@Override
public ShortcutType shortcutType() {
return ShortcutType.DEFAULT;
}
}
两种方法都可以实现,其中第一种与谓词工厂的自定义方式差不多一致
9. Gateway全局过滤器
Spring Cloud Gateway内置的全局过滤器。包括:
1 Combined Global Filter and GatewayFilter Ordering
2 Forward Routing Filter
3 LoadBalancerClient Filter
4 Netty Routing Filter
5 Netty Write Response Filter
6 RouteToRequestUrl Filter
7 Websocket Routing Filter
8 Gateway Metrics Filter
9 Marking An Exchange As Routed
9.1 自定义全局过滤器
使用场景 : 网关作为整个微服务的入口,不可避免的需要做一些认证的操作,例如JWT令牌的校验
如果在每一个微服务中使用自定义的局部过滤器去验证JWT令牌,不仅造成代码的冗余,更加不好管理
由此可以体现全局过滤器的重要作用,下面就来模拟实现
1. 自定义类实现GlobalFilter和Order接口
GlobalFilter接口是spring cloud gateway提供的接口,实现其中的filter方法来对请求进行验证
Order接口是spring提供的接口,用来作排序,这里用来决定咱们自定义的全局过滤器的执行优先级
- 代码实现
package com.qf.filters;
import cn.hutool.json.JSONUtil;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.Map;
/**
* 全局过滤器 与局部过滤器不同 没有后缀的强制性要求
*/
@Component
public class GlobalAuthenticateFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("全局过滤器1");
// 日后在此处验证令牌
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
// 模拟获取令牌
String jwtToken = request.getHeaders().getFirst("token");
// 非空判断
if (StringUtils.isEmpty(jwtToken)) {
//为空就不放行,直接做响应
Map res = new HashMap() {{
put("msg", "没有登录!!");
}};
return response(response, res);
} else {
// 不为空,判断令牌是否合法
if ("123".equals(jwtToken)) {
// 合法
return chain.filter(exchange);
} else {
// 不合法
Map res = new HashMap() {{
put("msg", "令牌不合法!!");
}};
return response(response, res);
}
}
}
@Override
public int getOrder() {
// 返回的数字越小,过滤器越先执行
return 0;
}
private Mono<Void> response(ServerHttpResponse response, Object msg) {
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
String resJson = JSONUtil.toJsonPrettyStr(msg);
DataBuffer dataBuffer = response.bufferFactory().wrap(resJson.getBytes());
return response.writeWith(Flux.just(dataBuffer));//响应json数据
}
}
通过上述代码可知:
- Order接口中实现的getOrder()方法返回的是数字,int类型,最小 -2147483648,最大2147483647,数值越小,优先级越高
9.2 另一种全局过滤器的定义方式
思路:
可以在引导类中直接注入一个@Bean,返回值为GlobalFilter , 直接使用匿名内部类来实现GlobalFilter接口,重写其filter方法
增加注解@Order(int) 来决定这个gateway全局过滤器的优先级
- 代码实现 :
package com.qf;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.core.annotation.Order;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@SpringBootApplication
@EnableDiscoveryClient
public class GateWayApp {
public static void main(String[] args) {
SpringApplication.run(GateWayApp.class,args);
}
@Bean
@Order(-1)
public GlobalFilter globalFilter1() {
return new GlobalFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("全局过滤器2");
return chain.filter(exchange);
}
};
}
@Bean
@Order(2)
public GlobalFilter globalFilter2() {
return new GlobalFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("全局过滤器3");
return chain.filter(exchange);
}
};
}
}
10. Gateway整合Sentinel
Gateway作为整个微服务的入口,其本身也是一个微服务,自然可以整合Sentinel,且整合方式与我们之前学习到的普通微服务整合Sentinel的方式一致
10.1 pom依赖
<!-- sentinel起步依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- gateway整合sentinel所需适配器 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
</dependency>
10.2 gateway注册到sentinel上
spring:
cloud:
nacos:
config:
server-addr: localhost:8848
username: nacos
password: nacos
namespace: @environment@
group: DEFAULT_GROUP
prefix: cloud-gateway
file-extension: yml
shared-configs:
- common.yml
refreshable-dataids: commmon.yml
profiles:
# gateway整合sentinel
sentinel:
transport:
port: 8122
dashboard: localhost:8888
eager: true
active: @environment@
至此,Gateway整合sentinel 完毕,之后我们可以在Gateway处直接对调用的资源进行流控管理,而不需要在每个微服务中去单独设置了,整体效率提升几个level!!!
10.3 Gateway整合Sentintel之后的全局异常处理
与之前微服务整合Sentinel的全局异常处理一样,Gateway原来有默认的处理方式,是给浏览器返回一句话,(Block by Sentinel)表明异常类型,而这种方式不是很友好,就需要我们自定义Sentinel的全局异常处理
-
明确几点:
-
- 实现sentinel的全局异常处理实则是实现WebExceptionHandler中的handler方法
package org.springframework.web.server; import reactor.core.publisher.Mono; public interface WebExceptionHandler { Mono<Void> handle(ServerWebExchange var1, Throwable var2); }
-
- 实际上gateway有默认的处理方式,即类SentinelGatewayBlockExceptionHandler,这个类是 adapter包提供的
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package com.alibaba.csp.sentinel.adapter.gateway.sc.exception; import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager; import com.alibaba.csp.sentinel.slots.block.BlockException; import com.alibaba.csp.sentinel.util.function.Supplier; import java.util.List; import org.springframework.http.codec.HttpMessageWriter; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.reactive.function.server.ServerResponse.Context; import org.springframework.web.reactive.result.view.ViewResolver; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebExceptionHandler; import reactor.core.publisher.Mono; public class SentinelGatewayBlockExceptionHandler implements WebExceptionHandler { private List<ViewResolver> viewResolvers; private List<HttpMessageWriter<?>> messageWriters; private final Supplier<Context> contextSupplier = () -> { return new Context() { public List<HttpMessageWriter<?>> messageWriters() { return SentinelGatewayBlockExceptionHandler.this.messageWriters; } public List<ViewResolver> viewResolvers() { return SentinelGatewayBlockExceptionHandler.this.viewResolvers; } }; }; public SentinelGatewayBlockExceptionHandler(List<ViewResolver> viewResolvers, ServerCodecConfigurer serverCodecConfigurer) { this.viewResolvers = viewResolvers; this.messageWriters = serverCodecConfigurer.getWriters(); } private Mono<Void> writeResponse(ServerResponse response, ServerWebExchange exchange) { return response.writeTo(exchange, (Context)this.contextSupplier.get()); } public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) { if (exchange.getResponse().isCommitted()) { return Mono.error(ex); } else { return !BlockException.isBlockException(ex) ? Mono.error(ex) : this.handleBlockedRequest(exchange, ex).flatMap((response) -> { return this.writeResponse(response, exchange); }); } } private Mono<ServerResponse> handleBlockedRequest(ServerWebExchange exchange, Throwable throwable) { return GatewayCallbackManager.getBlockHandler().handleRequest(exchange, throwable); } }
源码解读:
主要方法:
- handle() => 重写WebExceptionHandler中的handle核心方法,其中return !BlockException.isBlockException(ex) ? Mono.error(ex) : this.handleBlockedRequest(exchange, ex) 此方法即是通过判断异常是否是sentinel抛出的来进行处理
- 上述方法中调用了handleBlockedRequest(ServerWebExchange exchange, Throwable throwable) 的本类方法,来处理咱们sentinel抛出的异常,其中又套娃了GatewayCallbackManager.getBlockHandler().handleRequest(exchange, throwable)来处理异常
- 我们关注的是返回给客户端的内容,而自定义的内容在this.writeResponse(response, exchange)这个方法内,找到这个本类方法可以发现只要重新定义这个方法,就可以实现自定义的异常反馈(向浏览器发送json即可)
- 综上: 我们就有思路来自定义的全局异常了,代码如下展示
-
- 我们可以自定义类实现WebExceptionHandler,并copy一下SentinelGatewayBlockExceptionHandler的代码[坏笑]
package com.qf.sentinel; import cn.hutool.json.JSONUtil; import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager; import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler; import com.alibaba.csp.sentinel.slots.block.BlockException; import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException; import com.alibaba.csp.sentinel.slots.block.flow.FlowException; import com.alibaba.csp.sentinel.util.function.Supplier; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.codec.HttpMessageWriter; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.reactive.result.view.ViewResolver; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebExceptionHandler; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 自定义sentinel全局异常处理 */ public class MySentinelGatewayBlockExceptionHandler implements WebExceptionHandler { private List<ViewResolver> viewResolvers; private List<HttpMessageWriter<?>> messageWriters; private final Supplier<ServerResponse.Context> contextSupplier = () -> { return new ServerResponse.Context() { public List<HttpMessageWriter<?>> messageWriters() { return MySentinelGatewayBlockExceptionHandler.this.messageWriters; } public List<ViewResolver> viewResolvers() { return MySentinelGatewayBlockExceptionHandler.this.viewResolvers; } }; }; public MySentinelGatewayBlockExceptionHandler(List<ViewResolver> viewResolvers, ServerCodecConfigurer serverCodecConfigurer) { this.viewResolvers = viewResolvers; this.messageWriters = serverCodecConfigurer.getWriters(); } /** * 这个方法做了少许改造,将异常传了过来,进行异常类型判断 */ private Mono<Void> writeResponse(ServerResponse resp, ServerWebExchange exchange, Throwable ex) { ServerHttpResponse response = exchange.getResponse(); // 对抛出的异常做判断 if (ex instanceof FlowException) { Map res = new HashMap() {{ put("success", false); put("msg", "俺们自定义的网关流控异常"); }}; return response(response, res); } if (ex instanceof DegradeException) { Map res = new HashMap() {{ put("success", false); put("msg", "俺们自定义的网关熔断降级异常"); }}; return response(response, res); } Map res = new HashMap() {{ put("success", false); put("msg", "俺们自定义的网关其他异常"); }}; return this.response(response, res); } public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) { if (exchange.getResponse().isCommitted()) { return Mono.error(ex); } else { return !BlockException.isBlockException(ex) ? Mono.error(ex) : this.handleBlockedRequest(exchange, ex).flatMap((response) -> { return this.writeResponse(response, exchange, ex); }); } } private Mono<ServerResponse> handleBlockedRequest(ServerWebExchange exchange, Throwable throwable) { return GatewayCallbackManager.getBlockHandler().handleRequest(exchange, throwable); } private Mono<Void> response(ServerHttpResponse response, Object msg) { response.getHeaders().add("Content-Type", "application/json;charset=UTF-8"); String resJson = JSONUtil.toJsonPrettyStr(msg); DataBuffer dataBuffer = response.bufferFactory().wrap(resJson.getBytes()); return response.writeWith(Flux.just(dataBuffer));//响应json数据 } }
-
- 配置我们自定义的sentinel全局异常处理
package com.qf.sentinel; import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter; 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.codec.ServerCodecConfigurer; import org.springframework.web.reactive.result.view.ViewResolver; import java.util.Collections; import java.util.List; @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 MySentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() { // Register the block exception handler for Spring Cloud Gateway. return new MySentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer); } @Bean public GlobalFilter sentinelGatewayFilter() { // By default the order is HIGHEST_PRECEDENCE return new SentinelGatewayFilter(); } }
两个注意点:
- 这个全局异常的处理类无法通过@Component 注解直接注入到IOC容器中生效,需要进行java类的配置
- 明确此时的 IOC 容器中实则是有两个全局异常处理类的,一个是我们自定义的,一个是原来默认的,需要设置其优先级最高,保证使用我们自定义的全局异常处理
-
11. Gateway的跨域处理
明确一点,何为跨域?
跨域是浏览器的行为,而非咱们后台的行为,也就是说,当两个不同域的资源在访问的过程中,我们通过浏览器去发送请求,就会被浏览器考虑到不同域的安全性问题,从而阻止访问
而微服务中微服务之间的调用是不存在跨域问题的,因为这个过程中不是浏览器来调用,而是微服务之间的调用
**从此 @CrossOrigin的注解离我们远去了 **
还记得我们在建立网关时依赖的是什么吗?spring-cloud-starter-gateway 而非 spring-boot-starter-web
由于gateway使用的是webflux,而不是springmvc,所以需要先关闭springmvc的cors,再从gateway的filter里边设置cors就行了
代码实现:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedMethod("*");
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}
切记微服务的Controller中不要用@CrossOrigin注解了!!!否则两次跨域一样会报错的!!!