Spring Cloud Gateway過濾器精確控制異常返回(實戰,完全定製返回body)

程式設計師欣宸發表於2021-12-01

歡迎訪問我的GitHub

這裡分類和彙總了欣宸的全部原創(含配套原始碼):https://github.com/zq2599/blog_demos

本篇概覽

在這裡插入圖片描述

  • 正如上圖所示,異常發生時系統固定返回8個欄位,這就有些不夠靈活了,在一些對格式和內容有嚴格要求的場景下,我們們需要能夠完全控制返回碼和返回body的內容,如下所示,只返回三個欄位,每個欄位都是完全為業務服務的:
{
    # 這是有具體業務含義的返回碼
    "code": "010020003",

    # 這是能精確描述錯誤原因的文字資訊
    "message": "請確保請求引數中的user-id欄位是有效的",
    
    # 這是常規的業務資料,發生異常時該欄位為空
    "data": null
}
  • 今天我們們的目標就是通過編碼定製異常發生時的返回資訊,具體內容就是上述JSON資料:只有code、message、data三個欄位

原始碼下載

名稱 連結 備註
專案主頁 https://github.com/zq2599/blog_demos 該專案在GitHub上的主頁
git倉庫地址(https) https://github.com/zq2599/blog_demos.git 該專案原始碼的倉庫地址,https協議
git倉庫地址(ssh) git@github.com:zq2599/blog_demos.git 該專案原始碼的倉庫地址,ssh協議
  • 這個git專案中有多個資料夾,本篇的原始碼在spring-cloud-tutorials資料夾下,如下圖紅框所示:

在這裡插入圖片描述

  • spring-cloud-tutorials資料夾下有多個子工程,本篇的程式碼是gateway-change-body,如下圖紅框所示:

在這裡插入圖片描述

為何不用常規手段

  • 提到全域性異常處理,經驗豐富的您應該想到了常用的ControllerAdvice和ExceptionHandler註解修飾的全域性異常處理類,但是Spring Cloud Gateway是基於WebFlux的,我們們之前處理異常時用到的HttpServletRequest在Spring Cloud Gateway中並不適用,因此,不能用ControllerAdvice和ExceptionHandler的手段來處理全域性異常

基本思路

  • 在動手前做好充足的理論分析,寫出的程式碼才能正常工作

  • 開啟DefaultErrorWebExceptionHandler.java,找到renderErrorResponse方法,來看看Spring Cloud Gateway原本是如何構造異常返回內容的:

在這裡插入圖片描述

  • 此刻聰明的您應該想到怎麼做了:做個新的類繼承DefaultErrorWebExceptionHandler,覆蓋其renderErrorResponse方法,新的renderErrorResponse方法中,按照實際業務需要來設定返回內容,沒錯,這就是我們們的思路,不過還要細化一下,最終具體的步驟如下:
  1. 新增一個異常類CustomizeInfoException.java,該類有三個欄位:http返回碼、業務返回碼、業務描述資訊

  2. 在返回異常的程式碼位置,使用CustomizeInfoException類來丟擲異常,按照實際業務場景設定CustomizeInfoException例項的各個欄位

  3. 新增MyErrorWebExceptionHandler.java,繼承自DefaultErrorWebExceptionHandler,重寫了renderErrorResponse方法,這裡面檢查異常例項是否是CustomizeInfoException型別,如果是,就從其中取出http返回碼、業務返回碼、業務描述資訊等欄位,構造返回body的內容,異常例項若不是CustomizeInfoException型別,就保持之前的處理邏輯不變;

  4. 新增configuration類,用於將MyErrorWebExceptionHandler例項註冊到spring環境

  • 分析完畢,開始編碼吧,為了簡單起見,本篇不再新增maven子工程,而是基於前文建立的子工程gateway-change-body,在這裡面繼續寫程式碼;

編碼

  • 新增異常類CustomizeInfoException.java
package com.bolingcavalry.changebody.exception;

import lombok.Data;
import org.springframework.http.HttpStatus;

@Data
public class CustomizeInfoException extends Exception {
    /**
     * http返回碼
     */
    private HttpStatus httpStatus;

    /**
     * body中的code欄位(業務返回碼)
     */
    private String code;

    /**
     * body中的message欄位(業務返回資訊)
     */
    private String message;
}
  • 修改RequestBodyRewrite.java的apply方法,這裡面是在處理請求body,如果檢查到沒有user-id欄位,就不將請求轉發到服務提供方provider-hello,而是返回錯誤,這裡的錯誤就用CustomizeInfoException類來處理:
@Override
    public Publisher<String> apply(ServerWebExchange exchange, String body) {
        try {
            Map<String, Object> map = objectMapper.readValue(body, Map.class);

            // 如果請求引數中不含user-id,就返回異常
            if (!map.containsKey("user-id")) {
                CustomizeInfoException customizeInfoException = new CustomizeInfoException();
                // 這裡返回406,您可以按照業務需要自行調整
                customizeInfoException.setHttpStatus(HttpStatus.NOT_ACCEPTABLE);

                // 這裡按照業務需要自行設定code
                customizeInfoException.setCode("010020003");

                // 這裡按照業務需要自行設定返回的message
                customizeInfoException.setMessage("請確保請求引數中的user-id欄位是有效的");

                return Mono.error(customizeInfoException);
            }

            // 取得id
            int userId = (Integer)map.get("user-id");

            // 得到nanme後寫入map
            map.put("user-name", mockUserName(userId));

            return Mono.just(objectMapper.writeValueAsString(map));
        } catch (Exception ex) {
            log.error("1. json process fail", ex);
            return Mono.error(new Exception("1. json process fail", ex));
        }
    }
  • 異常處理類MyErrorWebExceptionHandler.java,這裡有一處需要重點關注的是:下面的程式碼僅是參考而已,您無需拘泥於CustomizeInfoException有關的邏輯,完全能按照業務需求自由設定返回的狀態碼和body:
package com.bolingcavalry.changebody.handler;

import com.bolingcavalry.changebody.exception.CustomizeInfoException;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.autoconfigure.web.WebProperties;
import org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.context.ApplicationContext;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.Map;

public class MyErrorWebExceptionHandler extends DefaultErrorWebExceptionHandler {

    public MyErrorWebExceptionHandler(ErrorAttributes errorAttributes, WebProperties.Resources resources, ErrorProperties errorProperties, ApplicationContext applicationContext) {
        super(errorAttributes, resources, errorProperties, applicationContext);
    }

    @Override
    protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
        // 返回碼
        int status;
        // 最終是用responseBodyMap來生成響應body的
        Map<String, Object> responseBodyMap = new HashMap<>();

        // 這裡和父類的做法一樣,取得DefaultErrorAttributes整理出來的所有異常資訊
        Map<String, Object> error = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));

        // 原始的異常資訊可以用getError方法取得
        Throwable throwable = getError(request);

        // 如果異常類是我們們定製的,就定製
        if (throwable instanceof CustomizeInfoException) {
            CustomizeInfoException myGatewayException = (CustomizeInfoException) throwable;
            // http返回碼、body的code欄位、body的message欄位,這三個資訊都從CustomizeInfoException例項中獲取
            status = myGatewayException.getHttpStatus().value();
            responseBodyMap.put("code", myGatewayException.getCode());
            responseBodyMap.put("message", myGatewayException.getMessage());
            responseBodyMap.put("data", null);
        } else {
            // 如果不是我們們定製的異常,就維持和父類一樣的邏輯
            // 返回碼
            status = getHttpStatus(error);
            // body內容
            responseBodyMap.putAll(error);
        }

        return ServerResponse
                // http返回碼
                .status(status)
                // 型別和以前一樣
                .contentType(MediaType.APPLICATION_JSON)
                // 響應body的內容
                .body(BodyInserters.fromValue(responseBodyMap));
    }
}
  • 最後是配置類MyErrorWebFluxAutoConfiguration.java:
package com.bolingcavalry.changebody.config;

import com.bolingcavalry.changebody.handler.MyErrorWebExceptionHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.autoconfigure.web.WebProperties;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.config.WebFluxConfigurer;
import org.springframework.web.reactive.result.view.ViewResolver;
import java.util.stream.Collectors;

@Configuration(proxyBeanMethods = false)
@AutoConfigureBefore(WebFluxAutoConfiguration.class)
public class MyErrorWebFluxAutoConfiguration {

    private final ServerProperties serverProperties;

    public MyErrorWebFluxAutoConfiguration(ServerProperties serverProperties) {
        this.serverProperties = serverProperties;
    }

    @Bean
    @Order(-1)
    public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes,
                                                             org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties,
                                                             WebProperties webProperties, ObjectProvider<ViewResolver> viewResolvers,
                                                             ServerCodecConfigurer serverCodecConfigurer, ApplicationContext applicationContext) {

        MyErrorWebExceptionHandler exceptionHandler = new MyErrorWebExceptionHandler(errorAttributes,
                resourceProperties.hasBeenCustomized() ? resourceProperties : webProperties.getResources(),
                this.serverProperties.getError(), applicationContext);
        exceptionHandler.setViewResolvers(viewResolvers.orderedStream().collect(Collectors.toList()));
        exceptionHandler.setMessageWriters(serverCodecConfigurer.getWriters());
        exceptionHandler.setMessageReaders(serverCodecConfigurer.getReaders());
        return exceptionHandler;
    }
}
  • 編碼完成,該把程式執行起來驗證效果了;

驗證

  • 啟動應用gateway-change-body

  • 用postman發起POST請求,地址是http://localhost:8081/hello/change,如下圖,紅框2中的http返回碼是我們們程式碼裡設定的,紅框3顯示返回的內容就是我們們定製的那三個欄位:

在這裡插入圖片描述

  • 至此,控制Spring Cloud Gateway應用異常返回的實戰已經全部完成,從原始碼分析結合實戰演練,希望欣宸的文章能陪伴您深入瞭解Spring Cloud Gateway,打造出更加強大的閘道器應用;

你不孤單,欣宸原創一路相伴

  1. Java系列
  2. Spring系列
  3. Docker系列
  4. kubernetes系列
  5. 資料庫+中介軟體系列
  6. DevOps系列

歡迎關注公眾號:程式設計師欣宸

微信搜尋「程式設計師欣宸」,我是欣宸,期待與您一同暢遊Java世界...
https://github.com/zq2599/blog_demos

相關文章