SpringCloud--Gateway的学习

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. 网关位置及作用

在这里插入图片描述

  1. 由图可见,网关实际上就是整个微服务项目的入口,其基本的职责就是路由,转发请求到我们的微服务中,同时我们可以在网关层面做全局的限流和权限的控制,以及一些全局的认证操作,其本身的位置决定了他可以作为微服务的中心化管理者
  2. 网关实际上也是一个微服务,也需要注册到Nacos上,作为注册和发现,其可以动态感知我们其他微服务的上线和下线,并可以通过服务名serviceName来调用其他的微服务,避免了使用nginx反向代理的硬编码问题,同时通过我们前面学习的Nacos的知识可以推论出: 由服务名可以获取微服务的服务列表,并可以通过Ribbon来实现调用的负载均衡
  3. 总结: 明确两点,其一,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的路由配置有两种:

  1. 静态路由,硬编码路由的uri
  2. 动态路由,通过服务名去查找服务列表,动态获取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数据
    }
}

通过上述代码可知:

  1. 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的全局异常处理

  • 明确几点:

      1. 实现sentinel的全局异常处理实则是实现WebExceptionHandler中的handler方法
    package org.springframework.web.server;
    
    import reactor.core.publisher.Mono;
    
    public interface WebExceptionHandler {
        Mono<Void> handle(ServerWebExchange var1, Throwable var2);
    }
    
      1. 实际上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);
        }
    }
    

    源码解读:

    主要方法:

    1. handle() => 重写WebExceptionHandler中的handle核心方法,其中return !BlockException.isBlockException(ex) ? Mono.error(ex) : this.handleBlockedRequest(exchange, ex) 此方法即是通过判断异常是否是sentinel抛出的来进行处理
    2. 上述方法中调用了handleBlockedRequest(ServerWebExchange exchange, Throwable throwable) 的本类方法,来处理咱们sentinel抛出的异常,其中又套娃了GatewayCallbackManager.getBlockHandler().handleRequest(exchange, throwable)来处理异常
    3. 我们关注的是返回给客户端的内容,而自定义的内容在this.writeResponse(response, exchange)这个方法内,找到这个本类方法可以发现只要重新定义这个方法,就可以实现自定义的异常反馈(向浏览器发送json即可)
    4. 综上: 我们就有思路来自定义的全局异常了,代码如下展示
      1. 我们可以自定义类实现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数据
        }
    }
    
      1. 配置我们自定义的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();
        }
    }
    

    两个注意点:

    1. 这个全局异常的处理类无法通过@Component 注解直接注入到IOC容器中生效,需要进行java类的配置
    2. 明确此时的 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注解了!!!否则两次跨域一样会报错的!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值