首发于 JAVA日知录
SpringCloud微服务实战二十四 - SpringCloud Gateway的全局异常处理

SpringCloud微服务实战二十四 - SpringCloud Gateway的全局异常处理

前言

在单体SpringBoot项目中我们需要捕获全局异常只需要在项目中配置 @RestControllerAdvice @ExceptionHandler 就可以针对不同类型异常进行统一处理,统一包装后返回给前端调用方。

@Slf4j
@RestControllerAdvice
public class RestExceptionHandler {
     * 默认全局异常处理。
     * @return ResultData
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ResultData<String> exception(Exception e) {
        log.error("全局异常信息 ex={}", e.getMessage(), e);
        return ResultData.fail(ReturnCode.RC500.getCode(),e.getMessage());

但是在微服务架构下,例如网关调用业务系统失败(比如网关层jwt token解析异常、服务下线)这时候应用层的 @RestControllerAdvice 就会不生效,因为此时流量根本没到应用层。

下面我们分别模拟两种场景,让大家感受一下:

  • jwt解析异常

故意写错token让其无法解析,后端返回的数据为:

{
  "timestamp": "2020-12-22T02:32:03.143+0000",
  "path": "/account-service/account/test/jianzh5",
  "status": 500,
  "error": "Internal Server Error",
  "message": "Cannot convert access token to JSON",
  "requestId": "7043b1f8-1"
  • 服务下线

停止后端服务,后端返回的数据为:

{
  "timestamp": "2020-12-22T02:36:13.281+0000",
  "path": "/account-service/account/getByCode/jianzh5",
  "status": 503,
  "error": "Service Unavailable",
  "message": "Unable to find instance for account-service",
  "requestId": "7043b1f8-6"

在前后端分离的项目中,一般都要约定项目整体返回格式,前端需要根据返回数据确定页面逻辑。在我们项目例子中我们约定好的响应格式如下:

@Data
@ApiModel(value = "统一返回结果封装",description = "接口返回统一结果")
public class ResultData<T> {
    /** 结果状态 ,具体状态码参见ResultData.java*/
    @ApiModelProperty(value = "状态码")
    private int status;
    @ApiModelProperty(value = "响应信息")
    private String message;
    @ApiModelProperty(value = "后端返回结果")
    private T data;
    @ApiModelProperty(value = "后端响应状态")
    private boolean success;
    @ApiModelProperty(value = "响应时间戳")
    private long timestamp ;
    public ResultData (){
        this.timestamp = System.currentTimeMillis();

很显然在这些情况下返回的异常数据并不符合我们的预期格式,我们需要改造网关返回数据。

原因剖析

在SpringCloud gateway中默认使用 DefaultErrorWebExceptionHandler 来处理异常。这个可以通过配置类 ErrorWebFluxAutoConfiguration 得之。

DefaultErrorWebExceptionHandler 类中的默认异常处理逻辑如下:

public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler {
    protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
        return RouterFunctions.route(this.acceptsTextHtml(), this::renderErrorView).andRoute(RequestPredicates.all(), this::renderErrorResponse);

根据请求头确认返回什么资源格式。

返回的数据内容在 DefaultErrorAttributes 类中构建而成。

public class DefaultErrorAttributes implements ErrorAttributes {
    public Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
        Map<String, Object> errorAttributes = new LinkedHashMap();
        errorAttributes.put("timestamp", new Date());
        errorAttributes.put("path", request.path());
        Throwable error = this.getError(request);
        MergedAnnotation<ResponseStatus> responseStatusAnnotation = MergedAnnotations.from(error.getClass(), SearchStrategy.TYPE_HIERARCHY).get(ResponseStatus.class);
        HttpStatus errorStatus = this.determineHttpStatus(error, responseStatusAnnotation);
        errorAttributes.put("status", errorStatus.value());
        errorAttributes.put("error", errorStatus.getReasonPhrase());
        errorAttributes.put("message", this.determineMessage(error, responseStatusAnnotation));
        errorAttributes.put("requestId", request.exchange().getRequest().getId());
        this.handleException(errorAttributes, this.determineException(error), includeStackTrace);
        return errorAttributes;

阅读到这里就可以看到为什么上面会返回那样的数据格式,接下来我们需要改写返回格式。

解决方案

这里我们我们可以自定义一个 CustomErrorWebExceptionHandler 类用来继承 DefaultErrorWebExceptionHandler ,然后修改生成前端响应数据的逻辑。再然后定义一个配置类,写法可以参考 ErrorWebFluxAutoConfiguration ,简单将异常类替换成 CustomErrorWebExceptionHandler 类即可。

这种方法大家请自行研究,基本都是复制代码,改写不复杂,这种方法我们就不演示了,这里给大家介绍另外一种写法:

我们定义一个全局异常类 GlobalErrorWebExceptionHandler 让其直接实现顶级接口 ErrorWebExceptionHandler 重写 handler() 方法,在 handler() 方法中返回我们自定义的响应类。但是需要注意重写的实现类优先级一定要小于内置 ResponseStatusExceptionHandler 经过它处理的获取对应错误类的响应码。

代码如下:

/**
 * 网关全局异常处理
 * @author javadaily
@Slf4j
@Order(-1)
@Configuration
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class GlobalErrorWebExceptionHandler implements ErrorWebExceptionHandler {
    private final ObjectMapper objectMapper;
    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
        ServerHttpResponse response = exchange.getResponse();
        if (response.isCommitted()) {
            return Mono.error(ex);
        // 设置返回JSON
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        if (ex instanceof ResponseStatusException) {
            response.setStatusCode(((ResponseStatusException) ex).getStatus());
        return response.writeWith(Mono.fromSupplier(() -> {
            DataBufferFactory bufferFactory = response.bufferFactory();
            try {
                //返回响应结果
                return bufferFactory.wrap(objectMapper.writeValueAsBytes(ResultData.fail(500,ex.getMessage())));
            catch (JsonProcessingException e) {
                log.error("Error writing response", ex);