重学SpringCloud系列九微服务网关-GateWay

2022-05-10 15:18:44 浏览数 (1)

重学SpringCloud系列九微服务网关-GateWay

  • 还有必要学习Zuul么?
    • 一、什么是API网关
    • 二、聊一聊Zuul
  • 简介与非阻塞异步IO模型
    • 一、简介
    • 核心功能特性
    • 二、阻塞IO与异步非阻塞IO的区别
  • GateWay概念与流程
    • 一、Spring Cloud Gateway的处理流程
    • 二、核心概念
  • 新建一个GateWay项目
    • 一、Gateway网关搭建
    • 二、需要注意的点
    • 三、路由转发测试
  • 通用Predicate的使用
    • 一、Predicate路由判断条件介绍
    • 二、Predicate路由判断条件的使用方法
      • 2.1.通过日期时间条件匹配
      • 2.2.通过 Cookie 匹配
      • 2.3.通过 Header 属性匹配
      • 2.4.通过 Host 匹配
      • 2.5.通过请求Method匹配
      • 2.6.通过请求参数匹配
      • 2.7.通过请求 ip 地址进行匹配
      • 2.8.通过权重分流匹配
      • 2.9.组合使用
  • 自定义PredicateFactory
    • 一、自定义Predicate Factory
      • **需求**:
      • 实现步骤
  • 二、测试一下
  • 编码方式构建静态路由
    • 一、回顾一下配置文件的方式
    • 二、编码方式实现路由
  • Filter过滤器介绍与使用
    • 一、过滤器简介
    • 二、Filter的生命周期
    • 三、Filter的分类
      • 3.1.Gateway filter
      • 3.2.Global filter
  • 自定义过滤器Filter
    • 一、自定义全局过滤器-统计接口api响应时长
    • 二、以class类的形式书写全局过滤器
    • 三、自定义局部过滤器-指定IP访问
  • 网关请求转发负载均衡
    • 一、请求转发负载均衡
    • 二、集成nacos服务注册客户端
    • 三、修改Gateway路由配置
    • 四、测试
  • 结合nacos实现动态路由配置
    • 一、启动时从nacos加载路由配置
    • 二、路由配置动态更新
    • 一、Gateway网关集成sentinel
    • 二、测试一下
    • 三、深入解析
  • 跨域访问配置
    • 一、跨域访问问题的解决
    • 二、gateway网关配置跨域
    • 三、代码配置方式
    • 四、需要注意的是
    • 可参考文章
  • 网关层面全局异常处理
    • 一、为什么需要网关层面的全局异常处理
    • 二、源码分析
    • 三、自定义全局异常处理

还有必要学习Zuul么?

一、什么是API网关

在开始讲解Spring Cloud GateWay之前呢,有必要说明一下什么是API网关。网关这个词,最早是出现在网络设备中,比如在彼此隔离的两个局域网中间的起到路由功能、隔离功能、安全验证功能的网络设备,通常被称为“网关”。

在软件开发方面,网关通常是用来隔离用户端和服务端的软件应用,通常被称为API网关。

所以使用API的好处是:

  • 面向前端开发人员更加友好,前端开发人员面向的入口减少,便于维护
  • 服务访问的认证鉴权更加方便,可以放在API网关统一去做。避免分散造成的开发及维护成本。
  • 访问日志、限流等公共服务也可以在网关上集中完成。避免分散造成的开发及维护成本。

说了API网关的这么多好处,那么有没有坏处呢?也是有的,而且很重要。

  • 当你使用了API网关之后,所有的请求都要多一次转发,造成一定程度上的响应时长的延长
  • 当你使用了API网关之后,意味着网关作为流量入口需要承担比微服务更多的流量负载。所以网关本身的架构性能及稳定性非常重要。

虽然我们可以在网关的前面再去加一层nginx或者haproxy等负载均衡器,但是仍旧很难改变网关在一定程度上的流量集中的问题。

所以,笔者在很多场合下呼吁不要滥用微服务网关。你要权衡一下你当前的架构是否真的需要一个网关。衡量性能、稳定性以及维护成本之间关系,去决定要不要使用服务网关。

二、聊一聊Zuul

正如笔者所说网关本身的架构性能及稳定性非常重要。然而性能就是Zuul的短板,因为它是基于servlet的阻塞IO模型开发的(下一节我会专门介绍Zuul和Spring Cloud GateWay IO模型的差异)。

  • Zuul 1.0在netflix官方已经进入了维护阶段,netflix对Spring Cloud社区的支持也已经基本属于“88了您哎”的状态
  • 虽然Netflix很早就宣称了要对zuul进行升级改造,也就是Zuul 2.0,但是目前与Spring Cloud的整合也处于难产状态
  • 基于以上的原因,Spring 社区自己开发了Spring Cloud gateWay。采用了spring 官方的响应式非阻塞框架webflux。官网测试结果性能是Zuul的1.6倍。

综上所述:笔者觉得目前Zuul已经没有任何学习的必要了。


简介与非阻塞异步IO模型

一、简介

Spring Cloud GateWay 是由Spring 官方社区开发的API 服务网关,在新一代的开发技术中使用到了Spring WebFlux的全新的响应式的非阻塞IO框架。相对于Spring Cloud第一代的网关组件zuul,性能有了长足的进步(宣称性能提升1.6倍)。WebFlux底层是基于高性能的非阻塞IO通信框架Netty实现的。

核心功能特性

笔者在上一节已经为大家介绍过,API服务网关的主要作用有三个:

  • 统一流量入口,面向前端更加友好。减少分散入口配置,降低客户端与服务端的耦合度。
  • 统一认证鉴权,避免多个服务分散鉴权造成的维护与开发的成本升高
  • 访问日志、限流、过滤、缓存、监控等公共服务也可以在网关上集中完成。避免分散造成的开发及维护成本

二、阻塞IO与异步非阻塞IO的区别

笔者用相对通俗的话为大家说明一下阻塞IO与非阻塞IO之间的区别。我们以软件开发团队的工作方式来做一个比喻。作为软件开发人员,我们肯定知道软件开发的基本流程:

  • 项目立项与可行性研究
  • 需求分析与设计
  • 代码开发
  • 迭代测试
  • 上线及配置管理、运维

在以Spring MVC或者struct为代表的框架都是基于sevlet的,其底层IO模型是阻塞IO模型。这种模型就好像你是公司的一个开发人员,上面的所有的5项工作全都由你一个人完成。如果公司有10个人,最多就只能同时进行10个需求。客户需求增多了也没有办法,只能让他们等着。如下图:一个请求占用一个线程,当线程池内的线程都被占用后新来的请求就只能等待。

spring 社区为了解决Spring MVC的阻塞模型在高并发场景下的性能瓶颈的问题,推出了Spring WebFlux,WebFlux底层实现是久经考验的netty非阻塞IO通信框架。该框架的请求处理与线程交互关系图如下:

boosGroup用于Accetpt连接建立事件并分发请求, workerGroup用于处理I/O读写事件和业务逻辑 每个Boss NioEventLoop循环执行的任务包含3步:

  • 1 轮询accept事件
  • 2 处理accept I/O事件,与Client建立连接,生成NioSocketChannel,并将NioSocketChannel注册到某个Worker NioEventLoop的Selector上
  • 3 处理任务队列中的任务,runAllTasks。任务队列中的任务包括用户调用eventloop.execute或schedule执行的任务,或者其它线程提交到该eventloop的任务。

每个Worker NioEventLoop循环执行的任务包含3步:

  • 1 轮询read、write事件;
  • 2 处I/O事件,即read、write事件,在NioSocketChannel可读、可写事件发生时进行处理
  • 3 处理任务队列中的任务,runAllTasks。

如果通俗的将上图中的各个任务池、线程池的组合比做一个软件开发公司,那么:

  • 项目立项及可研,由公司项目经理及顾问来完成
  • 需求分析与设计,由产品经理和架构师来完成
  • 代码研发,由项目经理带领开发人员来完成
  • 迭代测试,由测试团队来完成
  • 上线及配置管理、运维,可能由专门的devops团队来完成

这样一个公司内的所有人完成分工,就能在有限的资源的情况下,去接触更多的客户,谈更多的需求,合理的分配人力资源,达到并发处理能力最大化的极限水平。相比于一个员工从头到位的负责一个项目,它的组织性更强,分工更明确,合理的利用空闲资源,专业的人最专业的事。 这种人力资源的合理利用及组织方式和非阻塞IO模型有异曲同工之处,通过合理的将请求处理线程及任务进行分类,合理的利用系统的内存、CPU资源,达到单位时间内处理能力的最大化就是异步非阻塞IO的核心用意! 所以非阻塞IO模型的核心意义在于:提高了有限资源下的服务请求的并发处理能力,而不是缩短了单个服务请求的响应时长。 由于API 服务网关集中的承载了微服务系统内的流量进行转发,所以他的并发处理能力至关重要的原因,也是netflix Zuul被淘汰的根本原因!


GateWay概念与流程

一、Spring Cloud Gateway的处理流程

Spring Cloud的工作原理图如下:

  • 客户端向Spring Cloud Gateway发送请求,当请求的路径与网关定义的路由映射规则相匹配。该请求就会被发送到网关的Web Handler进行处理,执行特定的过滤器链。
  • 大家注意图中的虚线,虚线左侧表示在请求处理之前的执行逻辑(Pre过滤器),虚线右侧表示请求处理之后的执行逻辑(post过滤器)

二、核心概念

  • route(路由):路由实网关的基础元素,由id、目标url、predicate和filter组成。当请求通过网关的时候,由Gateway Handler Mapping通过predicate判断是否与路由匹配,当predicate=true的时候,匹配到对应的路由。
  • predicate(谓词逻辑):是java8中提供的一个函数,允许开发人员根据其定义规则匹配请求。比如根据请求头、请求参数来匹配路由。可以认为它就是一个匹配条件的定义。

国内有很多的人把这个翻译成“断言”,实际上这个词作为名词是“谓词”的意思,作为动词才是断言,官网上这个词是一个名词。

  • filter(过滤器):对请求处理之前之后进行一些统一的业务处理、比如:认证、审计、日志、访问时长统计等。

新建一个GateWay项目

一、Gateway网关搭建

引入gateway依赖

代码语言:javascript复制
<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

因为spring-cloud-starter-gateway包含spring-boot-starter-webflux,所以可以将项目中spring-boot-starter-webflux的maven坐标从pom文件中删除。

如果是新建的子模块module,做好父子项目的pom文件中的module配置关系。

项目的配置文件使用方法仍然和之前一致

代码语言:javascript复制
server:
  port: 8777

spring:
  application:
    name: zimug-server-gateway
  cloud:
    gateway:
      routes:
        - id: dhy           # 路由 ID,唯一
          uri: http://baidu.com/   # 目标 URI,路由到微服务的地址
          predicates:              # 请求转发判断条件
            - Path=/baidu/**    # 匹配对应 URL 的请求,将匹配到的请求追加在目标 URI 之后
  • routes指的是配置路由转发规则,可以配置多个
  • 每一个route有一个id,标识该路由的唯一性
  • uri指的是请求转发的目标
  • predicates是请求转发的判断条件,我们的例子使用的Path条件判断

上面的路由配置的含义是当我们访问:http://<gateway-ip>:8777/baidu/的时候,请求被转发到http://baidu.com/baidu/,其中**匹配任意字符。

二、需要注意的点

我们搭建的Spring Cloud Gateway(dhy-server-gateway)项目虽然是一个web项目,但是底层已经使用的是spring-boot-starter-webflux,而不是spring-boot-starter-web。所以下面的这个坐标不要在引入gateway项目了,会报错!

代码语言:javascript复制
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>

并且之前在spring-boot-starter-web体系下做的代码集成工作,在spring-boot-starter-webflux基础上都会有所变化,涉及到我们再去讲。

三、路由转发测试

在上面的配置文件中,我们已经完成了一个简单的路由转发配置。含义是当我们访问:http://<gateway-ip>:8777/baidu/的时候,请求被转发到http://baidu.com/baidu/,其中**匹配任意字符。 下面我们就来做一下实验,我们把gateway项目在本机启动,然后浏览器访问如下网址。

代码语言:javascript复制
http://localhost:8777/baidu/course

然后请求被转发至如下的网址:

代码语言:javascript复制
http://baidu.com/baidu/course

通用Predicate的使用

一、Predicate路由判断条件介绍

Predicate 来源于 Java 8,是 Java 8 中引入的一个函数,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。

在 Spring Cloud Gateway 中 Spring 利用 Predicate 的特性实现了各种路由匹配规则,有通过 Header、请求参数等不同的条件来进行作为条件匹配到对应的路由。

说白了 Predicate 就是为了实现一组匹配规则,方便让请求过来找到对应的 Route 进行处理,接下来我们接下 Spring Cloud GateWay 内置几种 Predicate 的使用。

二、Predicate路由判断条件的使用方法

在之前的章节举例中,我们已经介绍了Path Predicate的匹配条件决定路由的转发规则。下面我们为大家介绍其他的多种 Predicate的匹配条件!

2.1.通过日期时间条件匹配

After Route Predicate Factory使用的是时间作为匹配规则,只要当前时间大于设定时间,路由才会匹配请求。以下After规则配置:在东8区的2020-05-17T16:31:47之后,所有请求都转发到dhy.com

代码语言:javascript复制
spring:
  cloud:
    gateway:
      routes:
      - id: after_route
        uri: http://www.dhy.com
        predicates:
        - After=2020-05-17T16:31:47.789 08:00

Before Route Predicate Factory也是使用时间作为匹配规则,只要当前时间小于设定时间,路由才会匹配请求。以下Before规则配置:在东8区的2020-05-17T19:53:42之前,所有请求都转发到dhy.com

代码语言:javascript复制
spring:
  cloud:
    gateway:
      routes:
      - id: before_route
        uri: http://www.dhy.com
        predicates:
        - Before=2020-05-17T19:53:42.789 08:00

Between Route Predicate Factory也是使用两个时间作为匹配规则,只要当前时间大于第一个设定时间,并小于第二个设定时间,路由才会匹配请求。以下Between规则配置:在东8区的2020-05-17T16:31:47之后,2020-05-17T19:53:42之前的时间段内,所有请求都转发到dhy.com

代码语言:javascript复制
spring:
  cloud:
    gateway:
      routes:
      - id: between_route
        uri: http://www.dhy.com
        predicates:
        - Between=2020-05-17T16:31:47.789 08:00, 2020-05-17T19:53:42.789 08:00

2.2.通过 Cookie 匹配

Cookie Route Predicate 可以接收两个参数,一个是 Cookie name ,一个是正则表达式Cookie value,路由规则会通过获取对应的 Cookie name 值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配上则不执行。

代码语言:javascript复制
spring:
  cloud:
    gateway:
      routes:
      - id: cookie_route
        uri: http://www.dhy.com
        predicates:
        - Cookie=cookiename, cookievalue

使用 curl 测试,命令行输入:curl http://localhost:8777 --cookie "cookiename=cookievalue",则会返回zimug.com页面代码。也就是说当我们的请求携带了指定的cookie键值对的时候,请求才向正确的uri地址转发。

2.3.通过 Header 属性匹配

Header Route Predicate 和 Cookie Route Predicate 一样,也是接收 2 个参数,一个 header 中属性名称和一个正则表达式value,这个属性值和正则表达式value匹配的时候才进行路由转发。

代码语言:javascript复制
spring:
  cloud:
    gateway:
      routes:
      - id: header_route
        uri: http://www.dhy.com
        predicates:
        - Header=X-Request-Id, d 

上面的配置规则表示路由匹配存在名为X-Request-Id,内容为数字的header的请求,将请求转发到 dhy.com。 使用 curl 测试,命令行输入:curl http://localhost:8777 -H "X-Request-Id:88",则返回页面代码证明匹配成功。将参数-H "X-Request-Id:88"改为-H "X-Request-Id:somestr"再次执行时返回404证明没有匹配。

2.4.通过 Host 匹配

Host Route Predicate 接收一组参数,一组匹配的域名列表,这个模板是一个 ant 风格的模板,用逗号作为分隔符。它通过参数中的主机地址作为匹配规则。

代码语言:javascript复制
spring:
  cloud:
    gateway:
      routes:
      - id: host_route
        uri: http://www.dhy.com
        predicates:
        - Host=**.somehost.org,**.anotherhost.org

路由会匹配Http的Host诸如:www.somehost.orgbeta.somehost.orgwww.anotherhost.org的请求。

2.5.通过请求Method匹配

通过HTTP的method是 POST、GET、PUT、DELETE 等不同的请求方式来进行路由。

代码语言:javascript复制
spring:
  cloud:
    gateway:
      routes:
      - id: method_route
        uri: http://www.dhy.com
        predicates:
        - Method=GET

以上规则决定:路由会匹配到所有GET方法的请求,其他的HTTP方法不做匹配。 使用 curl 测试,命令行输入:

  • curl 默认是以 GET 的方式去请求,curl http://localhost:8777,测试返回页面代码,证明匹配到路由predicate规则
  • 指定curl使用POST方法发送请求,curl -X POST http://localhost:8777,返回 404 没有找到,证明没有匹配

2.6.通过请求参数匹配

Query Route Predicate 支持传入两个参数,一个是属性名一个为属性值,属性值可以是正则表达式。

代码语言:javascript复制
spring:
  cloud:
    gateway:
      routes:
      - id: query_route
        uri: http://www.dhy.com
        predicates:
        - Query=foo, ba.

路由会匹配所有包含foo,并且foo的内容为诸如:barbaz等符合ba.正则规则的请求。

使用 curl 测试,命令行输入:curl localhost:8777?foo=bax测试可以返回页面代码,将 foo的属性值改为 bazx再次访问就会报 404,证明路由需要匹配正则表达式才会进行路由。

2.7.通过请求 ip 地址进行匹配

Predicate 也支持通过设置某个 ip 区间号段的请求才会路由,RemoteAddr Route Predicate 接受 cidr 符号(IPv4 或 IPv6 )字符串的列表(最小大小为1),例如 192.168.0.1/16 (其中 192.168.0.1 是 IP 地址,16 是子网掩码)。

代码语言:javascript复制
spring:
  cloud:
    gateway:
      routes:
      - id: remoteaddr_route
        uri: https://www.dhy.org
        predicates:
        - RemoteAddr=192.168.1.1/24

可以将此地址设置为本机的 ip (192.168.1.4)地址进行测试,curl localhost:8080,则此路由将匹配。

2.8.通过权重分流匹配

通过权重weight分流匹配的predicate有两个参数:groupweight(一个int)。权重是按组计算的。以下示例配置权重路由谓词:

代码语言:javascript复制
spring:
  cloud:
    gateway:
      routes:
      - id: weight_high
        uri: https://weighthigh.org
        predicates:
        - Weight=group1, 8
      - id: weight_low
        uri: https://weightlow.org
        predicates:
        - Weight=group1, 2

这条路线会将约80%的流量转发至weighthigh.org,并将约20%的流量转发至weightlow.org。

2.9.组合使用

代码语言:javascript复制
spring:
  cloud:
    gateway:
      routes:
        - id: multi-predicate
          uri: https://www.dhy.com
          order: 0
          predicates:
            - Host=**.foo.org
            - Path=/headers
            - Method=GET
            - Header=X-Request-Id, d 
            - Query=foo, ba.
            - Query=baz
            - Cookie=chocolate, ch.p

各种 Predicates 同时存在于同一个路由时,请求必须同时满足所有的条件才被这个路由匹配。


自定义PredicateFactory

一、自定义Predicate Factory

虽然官方为我们提供了诸多的Predicate Factory(上一节介绍的),能够满足我们大部分的场景需求。但是不排除有些情况下,Predicate路由匹配条件比较复杂,这时就需要我们来自定义实现。

需求

本节我们通过自定义实现一个简单的需求,所有Path以"/sysuser"、"/sysorg"、"/sysrole"、"/sysmenu"、"/sysdict"、"/sysapi"开头的Http请求都转发到本机的http://localhost:8401/提供的aservice-rbac服务。

实现步骤
  • 自定义路由predicate工厂需要继承 AbstractRoutePredicateFactory 类,重写 apply 方法的逻辑。
  • apply 方法中可以通过 exchange.getRequest() 拿到 ServerHttpRequest 对象,从而可以获取到请求的参数、请求方式、请求头等信息。
代码语言:javascript复制
@Component
public class RbacAuthRoutePredicateFactory
        extends AbstractRoutePredicateFactory<RbacAuthRoutePredicateFactory.Config> {


  public RbacAuthRoutePredicateFactory() {
    super(Config.class);
  }

  @Override
  public Predicate<ServerWebExchange> apply(Config config) {
    return exchange -> {
      String requestURI = exchange.getRequest().getURI().getPath();
      if (config.getFlag().equals("rbac")
        &&(requestURI.startsWith("/sysuser")
          ||requestURI.startsWith("/sysorg")
          ||requestURI.startsWith("/sysrole")
          ||requestURI.startsWith("/sysmenu")
          ||requestURI.startsWith("/sysdict")
          ||requestURI.startsWith("/sysapi"))) {

        return true;  //表示匹配成功
      }
      return false;   //表示匹配失败
    };
  }
  //自定义参数args配置类
  public static class Config {
    private String flag; //该参数对应配置文件的args

    public String getFlag() {
      return flag;
    }

    public void setFlag(String flag) {
      this.flag = flag;
    }
  }
}
  • 类的命名需要以 RoutePredicateFactory 结尾,比如 RbacAuthRoutePredicateFactory,那么在配置文件中使用该predicate的时候 RbacAuth就是这个路由predicate工厂的名称。
  • 我们还可以为apply 方法传参数,如代码中的Config,flag字段和配置文件中的args字段名称是一一对应的。
代码语言:javascript复制
spring:
  application:
    name: zimug-server-gateway
  cloud:
    gateway:
      routes:
        - id: rbsc-service
          uri: http://localhost:8401/
          predicates:
            - name: RbacAuth
              args:
                flag: rbac

二、测试一下

前提:启动本机aservice-rbac服务及与其相关的其他公共Spring Cloud组件 访问http://127.0.0.1:8777/sysuser/pwd/reset,请求正确的被gateway接收,并按照我们自定义的路由规则转发给本机的aservice-rbac服务。


编码方式构建静态路由

一、回顾一下配置文件的方式

代码语言:javascript复制
server:
  port: 8777

spring:
  application:
    name: dhy-server-gateway
  cloud:
    gateway:
      routes:
        - id: dhy           # 路由 ID,唯一
          uri: http://baidu.com/   # 目标 URI,路由到微服务的地址
          predicates:              # 请求转发判断条件
            - Path=/baidu/**    # 匹配对应 URL 的请求,将匹配到的请求追加在目标 URI 之后
  • routes指的是配置路由转发规则,可以配置多个
  • 每一个route有一个id,标识该路由的唯一性
  • uri指的是请求转发的目标
  • predicates是请求转发的判断条件,我们的例子使用的Path条件判断

上面的路由配置的含义是当我们访问:http://<gateway-ip>:8777/baidu/**的时候,请求被转发到http://www.baidu.com/baidu/**,其中**匹配任意字符。

二、编码方式实现路由

下面的代码可以实现和配置文件实现方式一样的效果,所有的在配置文件中可以实现的predicates匹配规则,RouteLocatorBuilder 都有对应的api函数提供实现方法。

代码语言:javascript复制
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
   return builder.routes()
         .route("dhy", r -> r.path("/baidu/**")
         .uri("http://baidu.com"))
         .build();
}

route方法的第一个参数是路由id,第二个参数是一个Function(函数式编程),通过传入lambda表达式来判断匹配规则。

编码方式实现Predicate路由匹配规则,比配置文件的方式更繁琐一些,但是它也不是一无是处!配置文件方式的多个predicates组合只能表达“and并且”的关系,而编码方式还可以表达“or或者”的关系。 如下图所示:

但是笔者一般工作中很少使用编码方式实现路由的配置,因为编码代表着“写死”,也就是静态的。我们更希望配置是可以动态更新的,配置文件的方式结合nacos可以实现路由配置的动态更新,后面的章节我们再去介绍!


Filter过滤器介绍与使用

一、过滤器简介

微服务网关经常需要对请求进行一些过滤操作,比如:鉴权之后添加Header携带令牌等。在过滤器中可以

  • 为为请求增加请求头、增加请求参数 、增加响应头等等功能
  • 鉴权、记录审计日志、统计请求响应时长等共性服务操作

微服务系统中有很多的服务,我们不希望在每个服务上都去开发鉴权、记录审计日志、统计请求响应时长等共性服务操作。所以对于这样的重复开发或继承类工作,放在gateway上面统一去做是最好不过了。

二、Filter的生命周期

Spring Cloud Gateway 的 Filter 的生命周期很简单,只有两个:“pre” 和 “post”。

  • PRE: 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
  • POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的 HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。

三、Filter的分类

Spring Cloud Gateway 的 Filter 从作用范围可分为:

  • GatewayFilter:应用到单个路由或者一个分组的路由上。
  • GlobalFilter:应用到所有的路由上

笔者并不建议你去花很多的时间去学习下面的这些Filter都是如何使用,下面的这些Filter笔者几乎没有用到过。因为我已经介绍过了,Filter的作用就是在某些需求场景下去修改HTTP的请求头、路径、参数等等,只要你对HTTP协议足够的熟悉,所有的过滤器需求你都可以自定义实现,比起使用内置的Filter往往更加灵活。

3.1.Gateway filter

过滤器工厂

作用

参数

AddRequestHeader

为原始请求添加Header

Header的名称及值

AddRequestParameter

为原始请求添加请求参数

参数名称及值

AddResponseHeader

为原始响应添加Header

Header的名称及值

DedupeResponseHeader

剔除响应头中重复的值

需要去重的Header名称及去重策略

Hystrix

为路由引入Hystrix的断路器保护

HystrixCommand的名称

FallbackHeaders

为fallbackUri的请求头中添加具体的异常信息

Header的名称

PrefixPath

为原始请求路径添加前缀

前缀路径

PreserveHostHeader

为请求添加一个preserveHostHeader=true的属性,路由过滤器会检查该属性以决定是否要发送原始的Host

RequestRateLimiter

用于对请求限流,限流算法为令牌桶

keyResolver、rateLimiter、statusCode、denyEmptyKey、emptyKeyStatus

RedirectTo

将原始请求重定向到指定的URL

http状态码及重定向的url

RemoveHopByHopHeadersFilter

为原始请求删除IETF组织规定的一系列Header

默认就会启用,可以通过配置指定仅删除哪些Header

RemoveRequestHeader

为原始请求删除某个Header

Header名称

RemoveResponseHeader

为原始响应删除某个Header

Header名称

RewritePath

重写原始的请求路径

原始路径正则表达式以及重写后路径的正则表达式

RewriteResponseHeader

重写原始响应中的某个Header

Header名称,值的正则表达式,重写后的值

SaveSession

在转发请求之前,强制执行WebSession::save操作

SecureHeaders

为原始响应添加一系列起安全作用的响应头

无,支持修改这些安全响应头的值

SetPath

修改原始的请求路径

修改后的路径

SetResponseHeader

修改原始响应中某个Header的值

Header名称,修改后的值

SetStatus

修改原始响应的状态码

HTTP 状态码,可以是数字,也可以是字符串

StripPrefix

用于截断原始请求的路径

使用数字表示要截断的路径的数量

Retry

针对不同的响应进行重试

retries、statuses、methods、series

RequestSize

设置允许接收最大请求包的大小。如果请求包大小超过设置的值,则返回 413 Payload Too Large

请求包大小,单位为字节,默认值为5M

ModifyRequestBody

在转发请求之前修改原始请求体内容

修改后的请求体内容

ModifyResponseBody

修改原始响应体的内容

修改后的响应体内容

Default

为所有路由添加过滤器

过滤器工厂名称及值

每个过滤器工厂都对应一个实现类,并且这些类的名称必须以GatewayFilterFactory结尾,这是Spring Cloud Gateway的一个约定,例如AddRequestHeader对应的实现类为AddRequestHeaderGatewayFilterFactory。对源码感兴趣的小伙伴就可以按照这个规律拼接出具体的类名,以此查找这些内置过滤器工厂的实现代码

Filter并不如Predicate那么常用,更多的时候我们需要自定义Filter,所以官方内置的Filter我们就不一一介绍了。我们选两个例子来说明一下:

  • AddRequestHeader GatewayFilter Factory
代码语言:javascript复制
spring:
  cloud:
    gateway:
      routes:
      - id: add_request_header_route
        uri: http://httpbin.org:80/get
        filters:
        - AddRequestHeader=X-Request-Foo, Bar
        predicates:
        - Method=GET

过滤器工厂会在匹配的HTTP的请求加上一个Header,名称为X-Request-Foo,值为Bar。

  • RewritePath GatewayFilter Factory

在Nginx服务启中有一个非常强大的功能就是重写路径,Spring Cloud Gateway默认也提供了这样的功能。

代码语言:javascript复制
spring:
  cloud:
    gateway:
      routes:
      - id: rewritepath_route
        uri: http://httpbin.org
        predicates:
        - Path=/foo/**
        filters:
        - RewritePath=/foo/(?<segment>.*), /${segment}

根绝predicates的定义所有的/foo/**开始的路径都会命中路由。 请求gateway路径http://httpbin.org/foo/get ,gateway通过过滤器对路径进行重写,将请求转发至http://httpbin.org/get

3.2.Global filter

Spring Cloud Gateway框架内置的GlobalFilter如下:

全局过滤器

作用

Forward Routing Filter

用于本地forward,也就是将请求在Gateway服务内进行转发,而不是转发到下游服务

LoadBalancerClient Filter

整合Ribbon实现负载均衡

Netty Routing Filter

使用Netty的 HttpClient 转发http、https请求

Netty Write Response Filter

将代理响应写回网关的客户端侧

RouteToRequestUrl Filter

将从request里获取的原始url转换成Gateway进行请求转发时所使用的url

Websocket Routing Filter

使用Spring Web Socket将转发 Websocket 请求

Gateway Metrics Filter

整合监控相关,提供监控指标

每种Global filter的使用需要具体问题具体分析,通常遇到特殊情况,内置Global filter满足不了我们的需求,还可以自定义GlobalFilter。下一节我们讲解。


自定义过滤器Filter

一、自定义全局过滤器-统计接口api响应时长

我们用一个常见的需求:api接口服务的响应时长的计算,这个需求的实现对请求访问链路的优化很有意义。具体实现看下文的代码及注释:

代码语言:javascript复制
@Configuration
public class GlobalGatewayFilterConfig
{
    @Bean
    @Order(-100)
    public GlobalFilter apiGlobalFilter()
    {
        return (exchange, chain) -> {
            //获取请求处理之前的时间
            Long startTime = System.currentTimeMillis();
            //请求处理完成之后
            return chain.filter(exchange).then().then(Mono.fromRunnable(() -> {
                //获取请求处理之后的时间
                Long endTime = System.currentTimeMillis();
                //这里可以将结果进行持久化存储,我们暂时简单处理打印出来
                System.out.println(
                    exchange.getRequest().getURI().getRawPath()   
                            ", cost time : "
                              (endTime - startTime)   "ms");
            }));
        };
    }
}
  • @Order注解值越小,表示过滤器执行的优先级越高

我们使用《自定义PredicateFactory》那一节同样的测试用例,进行一下测试。dhy-server-gateway后台的打印结果如下:

通过上面的方法,可以在一个配置类里面写多个函数,每一个函数代表一个全局过滤器。

二、以class类的形式书写全局过滤器

上面的方法,当过滤器函数的实现内容比较复杂的时候,会导致单个类的代码行数过多,我们可以一个类写一个过滤器。

代码语言:javascript复制
@Component
public class ApiGlobalFilter implements GlobalFilter, Ordered {

    @Override
    public int getOrder() {
        return -100;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //获取请求处理之前的时间
        Long startTime = System.currentTimeMillis();
        //请求处理完成之后
        return chain.filter(exchange).then().then(Mono.fromRunnable(() -> {
            //获取请求处理之后的时间
            Long endTime = System.currentTimeMillis();
            //这里可以将结果进行持久化存储,我们暂时简单处理打印出来
            System.out.println(
                exchange.getRequest().getURI().getRawPath()   
                        ", cost time : "
                          (endTime - startTime)   "ms");
        }));
    }
}

三、自定义局部过滤器-指定IP访问

在我们的系统中可能有几个功能是专门给系统管理员使用的,并不广泛开放。我们假设这样一个需求:只让某个ip(管理员操作的PC的IP)的客户端访问aservice-rbac权限管理服务,其他的ip不可以。

  • 自定义Filter工厂需要继承 AbstractGatewayFilterFactory类,重写 apply 方法的逻辑。
  • 在 apply 方法中可以通过 exchange.getRequest() 拿到 ServerHttpRequest 对象,从而可以获取到请求的参数、请求方式、请求头等信息。
  • 在 apply 方法中可以通过chain操作过滤器链
代码语言:javascript复制
@Component
@Order(99)
public class IPForbidGatewayFilterFactory
    extends AbstractGatewayFilterFactory<IPForbidGatewayFilterFactory.Config> {

    public IPForbidGatewayFilterFactory()
    {
        super(Config.class);
    }

    @Override
    public List<String> shortcutFieldOrder()
    {
        return Arrays.asList("permitIp");  //对应config类的参数
    }

    @Override
    public GatewayFilter apply(Config config)
    {
        return (exchange, chain) -> {
            //获取服务访问的客户端ip
            String ip = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
            if (config.getPermitIp().equals(ip)) {
                //如果客户端ip=permitIp,继续执行过滤器链允许继续访问
                return chain.filter(exchange);
            }
            //否则返回,拒绝请求
            exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
            return exchange.getResponse().setComplete();
        };
    }

    static public class Config
    {
        private String permitIp;

        public String getPermitIp() {
            return permitIp;
        }

        public void setPermitIp(String permitIp) {
            this.permitIp = permitIp;
        }
    }
}
  • 类的命名需要以 GatewayFilterFactory结尾,比如 IPForbidGatewayFilterFactory,那么在配置文件中使用该Filter的时候 IPForbid就是这个Filter工厂的名称。
  • Config类可以定义一个或多个属性,要重写List shortcutFieldOrder()这个方法指定属性名称。

配置文件,因为只有一个参数,所以下图中的192.168.1.6将赋值给config类的唯一参数:permitIp

如果我们从不是192.168.1.6的这个客户端ip进行接口访问测试,将得到如下的结果:

如何为GatewayFilterFactory配置多个参数? 首先Config要有多个成员变量,如:permitIp、xxxx,其次配置文件进行如下配置


网关请求转发负载均衡

一、请求转发负载均衡

在之前的所有章节我们实现的例子中,路由规则的uri定义都是以http地址的形式写死的,如:http://localhost:8401,网关收到请求后根据路由规则将请求转发至对应的服务。

但是我们的微服务系统内通常都是一个服务启动多个实例,如下图所示:

为了达到网关接收到的请求能够负载均衡的转发给每个微服务的实例,我们将微服务网关注册到“服务注册中心”,比如:nacos。这样:

  • gateway接收到请求后,首先从服务注册中心获取到各个微服务实例的访问地址
  • 然后gateway根据客户端负载均衡的规则,选择众多实例中的一个作为请求转发对象。

简单的说:就是将gateway作为一个“服务调用者”注册到nacos服务注册中心,实现客户端负载均衡。

二、集成nacos服务注册客户端

在微服务端怎么集成nacos服务注册中心客户端(nacos章节都讲过),在gateway应用上就怎么集成nacos。

  • 通过macen坐标引入nacos服务注册中心客户端jar包
代码语言:javascript复制
<dependency>
   <groupId>com.alibaba.cloud</groupId>
   <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
  • 在项目启动类上加上启动服务注册与发现的注解
代码语言:javascript复制
@EnableDiscoveryClient
  • 配置nacos服务的地址
  • 启动gateway项目(zimug-server-gateway),在nacos控制台可以看到已经注册成功。

三、修改Gateway路由配置

以上的步骤只是将nacos客户端集成到了gateway项目(zimug-server-gateway),如果我们希望达到请求实例转发负载均衡的效果,还需要针对gateway项目进行配置:

  • 将uri从写死的http访问地址,改为lb://<服务注册名称>。gateway会通过<服务注册名称>去nacos请求多实例的服务访问地址列表,从地址列表中选择一个实例进行请求转发。lb是LoadBalance的缩写!

四、测试

我们启动两个aservice-rbac服务实例(本机8401和本机8411端口),并对它通过gateway(8777端口)进行访问测试。

通过观察日志,说明我们网关请求转发的负载均衡效果实现了:

  • 第一次请求被转发到了本机8401的aservice-rbac服务实例
  • 第2次请求被转发到了本机8411的aservice-rbac服务实例

结合nacos实现动态路由配置

为什么要对gateway网关配置集中管理?

  • 我们可以启动多个gateway网关实例,达到流量负载分流的效果。多个gateway实例可以使用nacos中的同一个配置文件。
  • 修改配置文件中的路由配置项,影响所有的gateway网关实例,不用一个实例一个实例的修改。

一、启动时从nacos加载路由配置

Spring Cloud Gateway启动时,就将yml配置文件中的路由配置和规则加载到内存里,使用InMemoryRouteDefinitionRepository来管理。配置文件的内容,我们可以放到nacos里面统一管理。

我们就把gateway当成一个普通的服务,我们之前在《alibaba-nacos》那一章怎么做的服务配置,就怎么做gateway配置。

  • 通过maven引入nacos服务配置客户端
代码语言:javascript复制
<dependency>
   <groupId>com.alibaba.cloud</groupId>
   <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

将配置文件分成两份:

  • 新建项目本地boostrap.yml 配置应用的基础信息端口、名称及nacos discovery和config信息。
  • 在nacos上新建zimug-server-gateway.yaml配置文件,并将gateway路由相关的配置信息放进去。这部分配置在nacos的集中管理。

本地boostrap.yml

代码语言:javascript复制
server:
  port: 8777

spring:
  application:
    name: zimug-server-gateway
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.161.6:8848
      config:
        server-addr: ${spring.cloud.nacos.discovery.server-addr}
        file-extension: yaml
        group: ZIMUG_GROUP  #配置分组,默认分组是DEFAULT_GROUP

Nacos上的zimug-server-gateway.yaml配置

代码语言:javascript复制
spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
      routes:
        - id: rbsc-service
          uri: lb://aservice-rbac
          predicates:
            - name: RbacAuth
              args:
                flag: rbac
          filters:
            - name: IPForbid
              args:
                permitIp: 127.0.0.1

路由配置中仍然包含我们之前配置的Filter:IPForbid,我们在本机启动gateway网关和aservice-rbac服务。 当IPForbid是127.0.0.1的时候,我们可以正常访问接口。如果是其他的ip,我们访问失败(403 forbidden)。 说明我们的gateway项目的路由配置放到nacos进行集中管理生效了!

二、路由配置动态更新

不要重启zimug-server-gateway项目,然后我们去nacos中为gateway新增一个路由配置,如下:

然后浏览器访问如下网址。

代码语言:javascript复制
http://localhost:8777/category/course

然后请求被转发至如下的网址:

代码语言:javascript复制
http://www.zimug.com/category/course

说明笔者使用的当前版本的Spring Cloud alibaba的nacos、与Spring Cloud Gateway(二者版本的选择《Spring Boot与Cloud选型兼容》)契合的非常完美,可以实现配置的动态更新。以后我们想针对所有的网关实例进行配置更新,就再也不需要对gateway网关项目重启了,可以实现实时生效!

需要注意的是:目前网上的很多文章的内容是基于比较旧的版本实现的,需要自己去实现nacos动态路由加载的监听。比如:https://article.itxueyuan.com/EX3pLq,可以看一下,但是没有必要这么做了。官方的新版本已经可以默认支持网管路由配置的动态更新,不需要重启gateway网关应用!

https://www.cnblogs.com/jian0110/p/12862569.html


# 整合Sentinel实现资源限流

Spring Cloud Gateway默认为我们提供了一种限流方法:RequestRateLimiterGatewayFilterFactory 。但这种方法实际并不能用于生产,并不能随着持久化数据的改变而动态改变限流参数,不能做到实时根据流量来改变流量阈值。本文就不介绍RequestRateLimiterGatewayFilterFactory 了,直接为大家介绍生产中最常用的Spring Cloud Gateway结合sentinel实现限流功能。

Sentinel 从 1.6.0 版本开始提供了 Spring Cloud Gateway 的适配模块,可以提供两种资源维度的限流:

  • route 维度:即在 Spring 配置文件中配置的路由条目,资源名为对应的 routeId
  • 自定义 API 维度:用户可以利用 Sentinel 提供的 API 来自定义一些 API 分组

一、Gateway网关集成sentinel

通过maven坐标在微服务模块zimug-server-gateway中加入sentinel客户端

代码语言:javascript复制
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

在项目的配置文件中加上sentinel配置,因为我是用了nacos,所以去nacos配置中心修改配置文件。如果你的服务没有使用配置中心,在项目本地的application.yml里面配置就可以了。

代码语言:javascript复制
spring:
  cloud:
    sentinel:
      transport:
        port: 8719
        dashboard: 192.168.161.3:8774  

二、测试一下

zimug-server-gateway项目在nacos中配置文件中有如下两条网关路由

我们先对这两条路由进行一下访问(本章前面小节多次写过),然后登陆sentinel控制台,看到如下信息,说明我们的网关route已经正确的被sentinel管理,我们可以针对route资源进行相关的流控、降级等配置。

我们针对"/sysuser/pwd/reset"所代表的路由资源设置QPS=1的流控限制(每秒最大请求数1)

然后我们快速点击http://127.0.0.1:8777/sysuser/pwd/reset发送请求,得到如下结果,说明针对该路由资源的访问在网关层面就被拦截了。

三、深入解析

上面的流控回调信息是在DefaultBlockRequestHandler默认定义实现的,当被限流时会返回类似于下面的错误信息:Blocked by Sentinel: FlowException

如果你想针对限流进行定制化的信息响应(其实没有必要),您可以在WebFluxCallbackManager注册回调自定义的BlockHandler:

自定义的BlockHandler代码如下,就是实现接口返回一个自定义的限流信息返回值:“系统繁忙,请稍后再试!”。其中Mono是webflux编程场景下的返回值使用方法之一。

代码语言:javascript复制
public class MySentinelBlockHandler implements BlockRequestHandler {

  @Override
  public Mono<ServerResponse> handleRequest(ServerWebExchange exchange, Throwable t) {
    ErrorResult errorResult = new MySentinelBlockHandler.ErrorResult(
            HttpStatus.TOO_MANY_REQUESTS.value(),
            "系统繁忙,请稍后再试!");
    // JSON result by default.
    return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
            .contentType(MediaType.APPLICATION_JSON_UTF8)
            .body(fromObject(errorResult));
  }


  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;
    }
  }
}

仿造本文第二小节的测试方法,在测试一次,返回结果如下:


跨域访问配置

一、跨域访问问题的解决

首先你要知道什么是同源策略,什么是跨域访问,这些基础知识我就不细讲了。简单的说就是:浏览器出于安全考虑,不允许域名(ip)、端口、协议不一致的请求进行跨域访问。比如:不能从localhost:8080域(前端),去访问localhost:8201域(后端服务)。

解决办法:去后端服务中,把允许跨域访问的域和HTTP协议方法配置好。

二、gateway网关配置跨域

假设目前的我的前端应用是:localhost:8080。所有的服务都从gateway网关经过,我们要针对网关进行cors配置。

代码语言:javascript复制
spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]':
            allowCredentials: true
            exposedHeaders: "*"
            allowedHeaders: "*"
            allowedOrigins: "http://localhost:8080"
            allowedMethods:
            - GET
            - POST

properties格式如下配置

代码语言:javascript复制
spring.cloud.gateway.globalcors.corsConfigurations.[/**].allowedOrigins=http://localhost:8080
spring.cloud.gateway.globalcors.corsConfigurations.[/**].allowedHeaders[0]=*
spring.cloud.gateway.globalcors.corsConfigurations.[/**].allowedMethods[0]=GET
spring.cloud.gateway.globalcors.corsConfigurations.[/**].allowedMethods[1]=POST

在上面配置的示例中,从localhost:8080所有GET或POST请求都允许跨域,浏览器端不会报错。

三、代码配置方式

虽然上面的配置是官方推荐的配置,但是我配置完成之后并未生效。随后我在源码中找到了如下的注释:

也就是说通过配置文件的配置方式目前还是TODO,有人说可以生效,有人说不生效。我测试的结果是不生效,所以可能因版本不同上面的方式不一定生效。所以我们提供另外一种配置方式:写代码,效果是一样的。在Configuration配置类或者gateway应用入口加入代码配置。

代码语言:javascript复制
@Bean
public CorsWebFilter corsFilter() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedMethods(Arrays.asList(
            HttpMethod.POST.name(),
            HttpMethod.GET.name()
    ));
    config.addAllowedOrigin("Http://localhost:8080");
    config.addAllowedHeader("*");

    UrlBasedCorsConfigurationSource source
            = new UrlBasedCorsConfigurationSource(new PathPatternParser());
    source.registerCorsConfiguration("/**", config);

    return new CorsWebFilter(source);
}

同样的写法,加上注释:

代码语言:javascript复制
@Configuration
public class CrosConfig {
    @Bean
    public CorsWebFilter corsWebFilter(){
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

        CorsConfiguration corsConfiguration = new CorsConfiguration();
        //1.配置跨域
        //允许哪种请求头跨域
        corsConfiguration.addAllowedHeader("*");
        //允许哪种方法类型跨域 get post delete put
        corsConfiguration.addAllowedMethod("*");
        // 允许哪些请求源跨域
        corsConfiguration.addAllowedOrigin("*");
        // 是否携带cookie跨域
        corsConfiguration.setAllowCredentials(true);

        //允许跨域的路径
        source.registerCorsConfiguration("/**",corsConfiguration);
        return new CorsWebFilter((CorsConfigurationSource) source);
    }
}

包不要引入错了,gateway底层是webflux,不是mvc

如果你不想在代码层面把配置内容写死,仍然可以采用nacos里面的配置属性自动组装GlobalCorsProperties及CorsWebFilter 并让第二小节中的yml中的跨域配置生效,代码如下:

代码语言:javascript复制
@Configuration
@AutoConfigureAfter(GlobalCorsProperties.class)
public class CorsConfig {

  @Resource
  private  GlobalCorsProperties globalCorsProperties;

  @Bean
  public CorsWebFilter corsFilter() {
    UrlBasedCorsConfigurationSource source
            = new UrlBasedCorsConfigurationSource(new PathPatternParser());
    globalCorsProperties.getCorsConfigurations()
            .forEach(source::registerCorsConfiguration);
    return new CorsWebFilter(source);
  }

}

四、需要注意的是

在gateway网关上进行了统一的跨域cors配置,微服务端就不要开启CORS跨域访问了。画蛇添足,反而会出错!

可参考文章

SpringCloudGateway CORS方案看这篇就够了


网关层面全局异常处理

一、为什么需要网关层面的全局异常处理

下图表示的是一次请求,经由网关转发微服务并由微服务操作数据库的一次请求处理流程。在请求处理过程中包含5处可能出现异常的位置

  1. 请求到达网关,网关处理请求发生异常
  2. 网关进行请求转发到微服务,转发过程中服务发现异常或网络异常
  3. 微服务处理请求,请求处理过程发生异常
  4. 微服务调用操作数据库,数据库操作异常
  5. 数据库本身发生网络或其他异常

对于3、4、5类的异常,微服务通过ControllerAdvice ExceptionHandler进行全局异常处理,返回全局通用的请求响应数据结构。 如果不进行全局的异常处理,Spring Boot会默认响应一个WhiteLabel Error Page,这样的响应结果很不友好。

对于1、2类的异常如果我们不进行统一的处理,默认的响应方式和Spring Boot是一样的,很不友好。所以也需要在网关层面进行全局的异常处理,这样对于网关本身出现的异常和请求转发过程的异常,也能给用户一个比较友好的数据响应结果,对于异常信息本身有一个合理的日志记录。

二、源码分析

那我们该如何实现网关层面的全局异常处理呢?先不着急做,我们先来看一下GateWay默认是怎么处理的,先看ExceptionHandlingWebHandler

代码语言:javascript复制
public class ExceptionHandlingWebHandler extends WebHandlerDecorator {

   //持有若干的WebExceptionHandler
   private final List<WebExceptionHandler> exceptionHandlers;


   public ExceptionHandlingWebHandler(WebHandler delegate, List<WebExceptionHandler> handlers) {
      super(delegate);
      this.exceptionHandlers = Collections.unmodifiableList(new ArrayList<>(handlers));
   }

   public List<WebExceptionHandler> getExceptionHandlers() {
      return this.exceptionHandlers;
   }

   @Override
   public Mono<Void> handle(ServerWebExchange exchange) {

      Mono<Void> completion;
      try {
         completion = super.handle(exchange);
      }catch (Throwable ex) {
         completion = Mono.error(ex);
      }
      //当出现异常的时候onErrorResume,使用WebExceptionHandler进行异常处理
      for (WebExceptionHandler handler : this.exceptionHandlers) {
         completion = completion.onErrorResume(ex -> handler.handle(exchange, ex));
      }

      return completion;
   }
}

通过上面的代码,我们知道WebExceptionHandler是异常处理类,我们来看一下它的代码

WebExceptionHandler是一个接口,其默认生效的实现类是DefaultErrorWebExceptionHandler,其默认的处理是渲染为error html页面。

代码语言:javascript复制
@Override
protected RouterFunction<ServerResponse> getRoutingFunction(
      ErrorAttributes errorAttributes) {
   return route(acceptsTextHtml(), this::renderErrorView).andRoute(all(),
         this::renderErrorResponse);
}

三、自定义全局异常处理

通过上面的分析,我们知道:如果我们希望在网关层面进行全局的异常处理,可以实现WebExceptionHandler接口。

但在实际使用中,我们通常去实现ErrorWebExceptionHandler,ErrorWebExceptionHandler继承自WebExceptionHandler。

代码语言:javascript复制
package org.springframework.boot.web.reactive.error;
import org.springframework.web.server.WebExceptionHandler;

@FunctionalInterface
public interface ErrorWebExceptionHandler extends WebExceptionHandler {

}

ErrorWebExceptionHandler是一个函数式接口,我们只需要实现其handle方法,就可以实现全局异常处理。

代码语言:javascript复制
@Slf4j
@Order(-1)
@Component
@RequiredArgsConstructor
public class JsonExceptionHandler implements ErrorWebExceptionHandler {

    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {

        ServerHttpResponse response = exchange.getResponse();
        if (response.isCommitted()) {
            //对于已经committed(提交)的response,就不能再使用这个response向缓冲区写任何东西
            return Mono.error(ex);
        }

        // header set 响应JSON类型数据,统一响应数据结构(适用于前后端分离JSON数据交换系统)
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);

        // 按照异常类型进行翻译处理,翻译的结果易于前端理解
        String message;
        if (ex instanceof NotFoundException) {
            response.setStatusCode(HttpStatus.NOT_FOUND);
            message = "您请求的服务不存在";
        } else if (ex instanceof ResponseStatusException) {
            ResponseStatusException responseStatusException = (ResponseStatusException) ex;
            response.setStatusCode(responseStatusException.getStatus());
            message = responseStatusException.getMessage();
        } else if (ex instanceof GateWayException) {
            response.setStatusCode(HttpStatus.FORBIDDEN);
            message = ex.getMessage();
        } else {
            response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
            message = "服务器内部错误";
        }

        //全局通用响应数据结构,可以自定义。通常包含请求结果code、message、data
        AjaxResponse result = AjaxResponse.error(
                response.getStatusCode().value(),
                message);

        writeLog(exchange, ex);

        return response.writeWith(Mono.fromSupplier(() -> {
            DataBufferFactory bufferFactory = response.bufferFactory();
            return bufferFactory.wrap(JSON.toJSONBytes(result));
        }));

    }

    //将错误信息以日志的形式记录下来
    private void writeLog(ServerWebExchange exchange, Throwable ex) {
        ServerHttpRequest request = exchange.getRequest();
        URI uri = request.getURI();
        String host = uri.getHost();
        int port = uri.getPort();
        log.error("[gateway]-host:{} ,port:{},url:{},  errormessage:",
                host,
                port,
                request.getPath(),
                ex);
    }
}

0 人点赞