基于Spring Cloud Gateway 的统一异常处理

编程入门 行业动态 更新时间:2024-10-15 06:20:46

基于Spring Cloud Gateway 的统一<a href=https://www.elefans.com/category/jswz/34/1771210.html style=异常处理"/>

基于Spring Cloud Gateway 的统一异常处理

1. 背景

在分布式的大环境下,集群部署越来越多,各个服务之间调用链路越来越长,每一种服务都有自己的错误码,如果一个服务异常,很难快速明了的通过错误码定位问题。并且错误码也散落在各个服务中,无法集中化管理,若每个服务自己来处理,耦合度过高。基于此背景,我们做了增强网关BetterGateway

2. 概述

针对上述弊端,结合现在分布式流行的设计,我们决定在网关处做具体的改造,每一个请求都要经过网关才能到达业务集群,经业务集群处理之后,请求再次经过网关返回给调用者。为了实现配置文件的集中管理,放眼现在流行的配置中心,我们采用Nacos作为配置中心。将错误码存放在Nacos中,利用Nacos动态刷新的功能,实现错误码的动态不停机动态更改。同时,我们还可以支持将配置文件存放在文件系统中,利用nio来监听文件的变动,实现动态刷新,如图1.1。

为了实现通过错误码能准确快速的定位异常,我们做了如下的分析:
想要快速通过错误码能定位异常,就要规定一种特殊的错误码的组装规则,从这个错误码中,可以一眼看出是哪个服务提供商的哪个功能下的哪个接口异常。为此,我们规定了错误码的格式如图1.2。

图1.2

此外还要保证错误码的唯一性,这里的错误码是组装之后的错误码(请读者分辨每个服务的错误码,和组装之后的错误码的区别,下文中都用错误码来代替每个服务自己的错误码,如有地方需要用到组装之后的错误码,笔者会写清楚)。每个服务提供商下的错误码可能有重合。为了保证唯一性,我们针对不同的网络调用,有不同的规则:

服务之间的网络调用错综复杂,大体分为两类。

  • 1.服务内部调用。
    服务名+错误码。
  • 2.三方接口(外部网络)调用。
    请求url+错误码

对于服务内部调用,内部服务定义一堆错误码。比如1000、1001,每一个内部服务就是一个服务提供商,比如a服务调用b服务的submit接口,假如此接口异常,返回错误码为1000,即可以看作 b服务提供商下返回了1000。那么此服务就是一个服务提供商,服务名+错误码是唯一标识。对于三方接口(外部服务)调用,我们采用了请求路径+该请求返回的状态码作为唯一标志。比如 b服务去调用了xxx公司的查询接口。该接口成功的返回200,失败返回1000(参数非法)、1001(鉴权失败)那么xxx公司就是一个服务提供商,查询接口+返回状态码就是唯一标识。

3. 功能

4. 设计

4.1 总体设计

BetterGateway是基于Spring Cloud Alibaba和Spring Cloud设计。同时也支持配置文件存放本地文件系统。本文中都是基于Spring Cloud Alibaba做示例。

BetterGateway总体设计

请求先到网关,网关转给下游的系统,如上图所示。网关调用A系统,A系统去调用B系统,B系统如果异常,B系统组装好错误码(主要是服务名和错误code)此时对于B系统来说有两种选择:

  • 抛出异常,通过全局异常处理类来处理此异常,并且返回给上游系统。
  • 自己处理,处理完后,将此错误码返回给上游系统。

A系统,接收接口返回结果,判断是否失败,如果失败,对于A系统来说此时也有两种选择:

  • 抛出异常,通过全局异常处理类来处理此异常,并且返回给上游系统。
  • 自己处理,处理完后,将此错误码返回给上游系统。

在A系统中不要改变B系统返回的错误码。主要是不要改变唯一标识(服务名+错误code或三方接口路径+code),经过A系统处理后,请求来到网关,网关判断该返回值是否成功。如果失败,就会按照配置文件来组装错误码,并且将组装之后的错误码返回给上游系统。

4.2 功能组件说明

4.2.1. 配置文件

  • 服务提供商总配置

code 服务提供商编码。

name 服务提供商名称。

dataId 服务提供商配置id。

domainName 服务提供商域。

authMethod 默认认证方式。

authConfig 额外认证方式。 ignorePath 不认证的路径(全路径)。 ignorePathPrefix 不认证的路径(前缀)。

type 服务提供商类别。

version 配置版本好,服务提供商输入错误码配置。

  • url 接口地址(内部服务即服务名)。
  • featCode 功能域编码。
  • errorCodeList 错误码列表。 errorCode 错误码(内部)。 code 错误码(展示)。 type 错误类型。 tips 错误提示。 handleStrategy 处理策略。 handleParam 处理策略参数。

默认情况下,错误提示会覆盖原始响应中的 msg 属性,可以通过占位符来进行扩展,目前支持的占位符有:

#APPCODE#。用于在msg中展示转换后的错误码。

#MSG#。用于在msg中展示原始响应信息。

分发策略distribute针对的是入站请求,与错误码映射无关。因此在配置分发策略时,url为入站请求路径,其余错误码相关属性均设为空。

4.2.2 处理策略配置

支持的策略

  • distribute 分发
    • handleStrategy distribute。
    • handleParam 分发的目的地址,多个地址可用逗号隔开。
  • forward 转发 (暂不可用)
    • handleStrategy forward
    • handleParam 转发的目的地址
  • retry 重试 (暂不可用)
    • handleStrategy retry
    • handleParam 重试次数,重试间隔

4.2 相关改造

通过上面的介绍,错误码已经能快速准确地识别哪个服务异常。如果一个服务异常,此错误码就要上传到网关处。在调用链路中显示如图1.3所示。

正所谓单丝不成线,独木不成林,BetterGateway的实际应用需要系统改造配合,所以我们建议使用下面的结构体,作为服务调用之间的统一返回值、自定义的异常(ApiException)和配套的全局异常处理(APIExceptionHandler)。

4.2.1 结构体

@Data
public class ApiResult<T> implements Serializable {private static final long serialVersionUID = 0xc93480e15321b2c5L;private static final Logger logger = LoggerFactory.getLogger(ApiResult.class);/*** 状态码*/private Integer code;/*** 说明信息*/private String msg;/*** 错误code*/private String appCode;/*** 服务名/url*/private String path;/*** 返回数据*/private T data;public ApiResult() {this.path = PropertyUtils.getServiceName();}public ApiResult(ResultCode code) {this.code = code.code();this.msg = code.message();this.appCode = String.valueOf(code.code());this.path = PropertyUtils.getServiceName();}public ApiResult(Integer code, String msg) {this.code = code;this.msg = msg;this.appCode = String.valueOf(code);this.path = PropertyUtils.getServiceName();}public ApiResult(ResultCode resultCode, T data) {this.code = resultCode.getCode();this.msg = resultCode.getMsg();this.appCode = String.valueOf(resultCode.getCode());this.path = PropertyUtils.getServiceName();this.data = data;}public static <T> ApiResult<T> failure(Integer code, String path) {ApiResult<T> result = failure();result.setCode(code);result.setAppCode(String.valueOf(code));result.setPath(path);return result;}public static <T> ApiResult<T> failure(Integer code, String path, T data) {ApiResult<T> result = failure();result.setCode(code);result.setAppCode(String.valueOf(code));result.setPath(path);result.setData(data);return result;}public static ApiResult<String> failure(ResultCode code, String data, boolean isCustomErrorMessage) {ApiResult<String> result = failure(code, data);if (isCustomErrorMessage && Toolkit.isValid(data)) {result.setMsg(data);}return result;}public static <T> ApiResult<T> failure(APIException e) {ApiResult<T> result = failure();result.setAppCode(e.getAppCode());result.setPath(e.getPath());result.setMsg(e.getMsg());result.setData((T) e.getData());return result;}public static <T> ApiResult<T> success() {return new ApiResult<T>(ResultCode.SUCCESS);}public static <T> ApiResult<T> success(T data) {ApiResult<T> result = new ApiResult<T>(ResultCode.SUCCESS);result.setData(data);return result;}public static <T> ApiResult<T> success(T data, String msg) {ApiResult<T> result = new ApiResult<T>(ResultCode.SUCCESS.code(), msg);result.setData(data);return result;}public static <T> ApiResult<T> failure(ResultCode rc) {StackTraceElement se = Thread.currentThread().getStackTrace()[2];logger.error("failure result code: {}, msg: {}", rc.getCode(), rc.getMsg());logger.error("failure info: {} {} {}", se.getClassName(), se.getMethodName(), se.getLineNumber());return new ApiResult<T>(rc.getCode(), rc.getMsg());}public static <T> ApiResult<T> failure() {return failure(ResultCode.FAIL);}public static <T> ApiResult<T> failure(ResultCode code, T data) {ApiResult<T> result = failure(code);result.setData(data);return result;}
}
复制代码

也可以根据需要使用自定义的ApiResult,但需要有以下属性:

  • code:Integer类型的状态码,原始形态,不影响原功能。
  • appCode:String类型的状态码,可以存入更丰富的信息,以及第三方接口的非int状态码。转换后的状态码会覆盖此值。
  • msg:提示信息,转换后的提示信息会覆盖此值。
  • path:异常路径。一般约定第三方接口出错为接口url,内部服务调用为服务名。也可采用其他命名,在配置文件中配置即可。 区分code和appCode是为了不影响原接口,如果是ApiResult的code原本就是String,直接使用即可,无需appCode。 当code不为成功时才会进行状态码映射,成功的状态配置在common包中的ResultCode枚举类。默认为200。

4.2.2 自定义的异常(ApiException)

@Data
public class APIException extends RuntimeException {private String appCode;private String path;private String msg;private Object data;public APIException() {}public APIException(String appCode, String path) {this.appCode = appCode;this.path = path;}public APIException(String appCode, String path, Object data) {this.appCode = appCode;this.path = path;this.data = data;}public APIException(String appCode, String path, String msg, Object data) {super(msg);this.appCode = appCode;this.path = path;this.msg = msg;this.data = data;}public APIException(ApiResult apiResult) {this.appCode = apiResult.getAppCode();this.path = apiResult.getPath();this.msg = apiResult.getMsg();this.data = apiResult.getData();}
}
复制代码

4.2.3全局异常处理(APIExceptionHandler)

5. 重要的类

5.1 过滤器

  • 【CacheRequestBodyFilter】 缓存请求体。
  • 【RemoveCachedBodyFilter】 释放缓存的请求体。
  • 【GlobalExceptionHandler】 异常处理。
    • 非网关服务异常,此类异常,网关要做错误码转换。
    • 网关异常,网关也是一个java进程,自身可能也会发生异常。
  • 【ErrorCodeFilter】 错误码转换。
  • 【ErrorCodeSpringFilter】 错误码转换,和上面不同的是,此类利用了spring-cloud-gateway提供的方式。
  • 【AuthFilter】 鉴权,策略模式实现不同服务商的动态鉴权。

5.2 配置文件的变动

  • 【ConfigDataChangeContainer】 存放最新的配置文件。
  • 【CoreContainerService】 操作 ConfigDataChangeContainer 类,用于对比数据,拉取新数据。
  • 【NacosClient】 nacos客户端,从Nacos中拉取新的配置文件。
  • 【FileClient】 文件读取客户端。从文件系统中拉取配置文件。如果要启动本地文件读取,则dataId为全路径。
  • 【NacosConfChangeHandler】 Nacos监听处理类,用于接受配置中心中配置的文件。
  • 【FileConfigChangeHandler】 File监听处理类,监视文件的变动、读取文件。

5.3 几个特别的接口

  • 【DataFormatConversion】 此接口用于通知配置文件的变动,将最新的配置文件发送给实现了这个接口的类。
  • 【CompensationService】 此接口用于处理请求失败时候之后的补偿措施。
  • 【AuthService】 鉴权接口。

6. 测试

测试案例中,有四个角色,前端系统、网关、sys-service(系统服务)、api-service(外部服务接口服务)、三方数据接口。如图1.4。

下面的几个例子中,针对不同的情况,展示不同服务之间和三方接口的异常,和网关转换之后的错误码。

6.1: sys服务正常

6.2: api服务正常

6.3 sys服务异常

6.4 api服务异常

6.5 api调用外部接口异常

6.6 api调用外部接口异常,没有配置错误码。

更多推荐

基于Spring Cloud Gateway 的统一异常处理

本文发布于:2024-02-10 22:47:22,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1677745.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:异常   Spring   Cloud   Gateway

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!