Spring Cloud OpenFeign 一種實踐方式


OpenFeign是Spring Cloud全家桶中最重要的一個RPC工具,本文想歸納一下自己兩年多來使用Feign的一些實踐經驗,希望本文能對讀者有所指引和幫助。

一、問題的提出

作為項目構建者,我們需要思考項目和開發者分別需要什么樣的一種RPC,也就是我們面對的技術需求。

站在項目的角度:

1. 請求失敗可以自動重試;

2. 重試的次數和間隔可以配置;

3. 失敗之后可以有日志記錄,可以獲取到請求參數和返回值;

站在開發者的角度:

4. 開發者不想為每一個接口的每一個方法寫一堆類似的fallback或fallbackFactory;

5. 在1的基礎上,開發者希望調用Feign失敗時沒有異常,無需try catch,但是可以通過返回值判斷調用是否成功以及具體原因。

對於大部分項目來講,請求的資源(URL)沒有backup,所以也就無法進行有效的fallback。fallback或者fallbackFactory里面大概率只是返回了空,保證不報異常,所以本文不討論熔斷降級此類需求。

6. 引入最少的第三方組件和配置。

二、閱讀官方手冊

   

 (1) Decoder用於解析http的返回值,具體來講是解析feign.Response對象,Spring Cloud提供了默認bean ResponseEntityDecoder,它可以將Json格式返回值反序列化為對象;

(2) Encoder用於請求參數到RequestBody之前的轉換,默認為SpringEncoder;

(3) Logger是日志組件,默認為Slf4jLogger;

(4) Contract用於處理annotation,默認是SpringMvcContract;

(5) Feign.Builder用於構建FeignClient組件,默認是HystrixFeign.Builder;

(6) Client,客戶端負載,環境中同時提供了spring-cloud-starter-netflix-ribbon和spring-cloud-starter-loadbalancer兩種負載,但是均未啟用,默認是FeignClient。

(7) Logger.Level是日志級別,包括:

     (a) NONE,默認,不記錄日志;

     (b) BASIC,記錄Request method,URL,Response Status code和執行時間;

     (c) HEADERS,記錄基本信息外加請求和相應頭信息;

     (d) FULL,記錄請求和相應全部信息,包括header,body和metadata。

(8) Retryer,重試器,提供了默認實現Retryer.Default,但並未注入到環境中;

(9) ErrorDecoder,錯誤解碼器,當返回狀態碼不屬於2xx號段時,該實現將會被調用。同Retryer一樣,它也有默認實現ErrorDecoder.Default,但並未注入到環境中;

(10) Request.Options,Feign的配置項;

(11) Collection<RequestInterceptor>,攔截器列表,可以定義統一的攔截器;

(12) SetterFactory,用於控制hystrix命令;

(13) QueryMapEncoder,將POJO或Map對象轉為Get參數的解析器。

標記橙色底色的項目,是本文需要用到的項目。

三、解決方案

1. 解決問題1,我們可以自己實現Retryer並在其中自定義重試的算法和規則,我們也可以直接使用Feign提供的默認Retryer。

 先來看看Feign提供的默認Retryer:

默認實現有三個構造函數參數,分別是period(重試間隔),maxPeriod(最大重試間隔)和maxAttempts(重試次數)。它的核心方法是continueOrPropagate,它決定是否繼續重試,注意它的參數類型RetryableException,凡是類型為RetryableException的異常才是值得重試的異常

continueOrPropagate的邏輯是第一次失敗后等待period開始重試,再失敗后等待period*(1.5的N次冪),其中N=重試次數-1,重試間隔小於maxPeriod;最多重試maxAttempts次。

由此可見Feign提供的默認實現Feign.Default完全能滿足我們的需求,所以注入到環境中:

    @Bean
    @ConditionalOnBean(name = "feignOptions")
    public feign.Retryer retryer(@Qualifier("feignOptions") Properties feignOptions) {
        long period = PropertiesUtils.getValue(feignOptions, "period", Long.class);
        long maxPeriod = PropertiesUtils.getValue(feignOptions, "maxPeriod", Long.class);
        int maxAttempts = PropertiesUtils.getValue(feignOptions, "maxAttempts", Integer.class);
        return new Retryer.Default(period, maxPeriod, maxAttempts);
    }

2. feignOptions這個bean便是為解決問題2而來。

首先在application.properties中定義配置:

feign.custom.config.enabled=true
feign.custom.config.period=2000
feign.custom.config.maxPeriod=4000
feign.custom.config.maxAttempts=5

讀取配置並注入到環境中,注意開關:

    @Bean
    @ConditionalOnProperty(name = "feign.custom.config.enabled", havingValue = "true")
    @ConfigurationProperties("feign.custom.config")
    public Properties feignOptions() {
        return new Properties();
    }

3. 解決問題3、4和5:

(1) 常見的RPC異常分為兩種:

     (a) 有響應的異常,出現這種異常時,客戶端到服務器的鏈接已經建立,客戶端接到了服務端而且有返回狀態碼,例如404,5xx類型的失敗;對於這種類型的失敗,我們可以實現ErrorDecoder。需要注意的是,所有進入到ErrorDecoder的請求都是有http響應的,所以對於無法解析的域名,Feign不會走到這一步。

     (b) 無響應的異常,比如java.net.UnknownHostException,即域名無法解析。因為沒有服務端的響應,這種類型的異常不會交由ErrorDecoder處理,我選擇用Spring AOP切面攔截此類異常,對返回值進行統一類型封裝。在K8S環境中,由於DNS的解析出現問題,我們的確遇到過臨時性的UnknownHostException異常。

 (2) 自定義ErrorDecoder,里面要解決的一個重要問題是定義哪些異常需要重試,對其封裝成RetryableException。 在下面的代碼里,我將404狀態列為可重試的異常:

public class CustomErrorDecoder implements ErrorDecoder {
    private final ErrorDecoder defaultErrorDecoder = new Default();

    @Override
    public Exception decode(String methodKey, Response response) {
        System.out.printf("CustomErrorDecoder, methodKey: %s, request url: %s\n", methodKey, response.request().url());
        Exception exception = defaultErrorDecoder.decode(methodKey, response);
        System.out.printf("CustomErrorDecoder, status: %d, exception: %s\n", response.status(), exception.getClass().getName());

        if (exception instanceof RetryableException) {
            return exception;
        }

        String message = String.format("CustomErrorDecoder, %d error!", response.status());

        if (response.status() == 404) {
            return new RetryableException(response.status(), message, response.request().httpMethod(), null, response.request());
        }

        return exception;
    }
}

注入自定義的ErrorDecoder:

    @Bean
    public ErrorDecoder errorDecoder() {
        return new CustomErrorDecoder();
    }

(3) 解決完有響應的異常之后,我們再來思考如何應對無響應的異常。我用Spring AOP來攔截來自FeignClient所在的包的所有異常,然后對其進行統一封裝,既然要統一封裝,那么我們就得先定義一個統一的返回值類型:

public class BaseResponse<T> {
    private boolean succeeded;
    private String message;
    private T data;
    private int code;

    public BaseResponse(boolean succeeded, String message, T data, int code) {
        this.setSucceeded(succeeded);
        this.setMessage(message);
        this.setData(data);
        this.setCode(code);
    }

    public BaseResponse(T data) {
        this(true, null, data, 0);
    }

    public BaseResponse() {
        this.setSucceeded(true);
    }

    public void setErrorMessage(String message) {
        this.setSucceeded(false);
        this.setMessage(message);
    }

    public boolean isSucceeded() {
        return succeeded;
    }

    public void setSucceeded(boolean succeeded) {
        this.succeeded = succeeded;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }
}

該類型的一個重要特點是,無論請求是否成功,我們都可以使用這個類型返回,通過isSucceeded()來判斷請求是否成功,通過getData()來獲取請求的結果,通過getMessage()來獲取異常信息。但是有幾個問題需要考慮:

(a) 如果我們調用的API是自己人提供的,我們可以讓自己人修改,以便統一返回類型;

(b) 如果我們調用的API是別人或者第三方提供的,我們無法統一其返回類型,此時如何是好?

換句話說,我們遇到的場景很可能是,有的API直接返回了BaseResponse格式的Json數據,有的API則是返回了非BaseResponse格式的業務數據。對於前者(我們稱這種接口為普通接口),如果調用期間出現Exception,我們可以在切面中將異常信息封裝到BaseResponse對象並返回,開發者可在Service或Controller中判斷調用結果,進而做相應處理;對於后者(我們稱這種接口為其他接口),如果調用期間出現Exception,我們無法在切面中將其封裝到BaseResponse對象並返回,這會導致和FeignClient接口的返回類型不一致,我們該如何處置?

為了解決這個問題,這就需要自定義Decoder接口實現,我們首先需要區分哪些接口直接返回了BaseResponse格式的Json數據,哪些接口返回了自己的業務數據。對於前者,交由Feign默認的Decoder進行解析;對於后者,Feign的返回類型需要定制,我們需要使用Feign默認的Decoder解析業務數據,然后封裝到定制類型對象中。

面向普通接口的統一返回類型:

public class ApiResponse<T> extends BaseResponse<T> {
    private int status;

    @JsonIgnore
    private HttpHeaders httpHeaders;

    @JsonIgnore
    private Throwable throwable;

    public ApiResponse() {
        this.setStatus(HttpStatus.OK.value());
    }

    public ApiResponse(T data) {
        this();
        super.setData(data);
    }

    public ApiResponse(ResponseEntity<T> responseEntity) {
        this(responseEntity.getBody());
        setHttpHeaders(responseEntity.getHeaders());
    }

    public ApiResponse(Throwable throwable) {
        super.setErrorMessage(throwable.getMessage());
        this.setThrowable(throwable);

        if (throwable instanceof UnknownHostException || throwable.getCause() instanceof UnknownHostException) {
            setStatus(HttpCode.UNKNOWHOST.getValue());
        }
    }

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public HttpHeaders getHttpHeaders() {
        return httpHeaders;
    }

    public void setHttpHeaders(HttpHeaders httpHeaders) {
        this.httpHeaders = httpHeaders;
    }

    public Throwable getThrowable() {
        return throwable;
    }

    public void setThrowable(Throwable throwable) {
        this.throwable = throwable;
    }
}

面向其他接口的統一返回類型:

public class OtherApiResponse<T> extends ApiResponse<T> {
    public OtherApiResponse(T data) {
        super(data);
    }

    public OtherApiResponse(Throwable throwable) {
        super(throwable);
    }

    public OtherApiResponse() {
        super();
    }
}

定義切面,切入feign接口所在的包,攔截異常,根據接口返回類型分類封裝並返回:

@Aspect
@Component
public class RemoteAspect {
    @Around("devutility.test.springcloud.feign.aspect.Pointcuts.pointcutForRemote()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Signature signature = proceedingJoinPoint.getSignature();
        Class<?> returnType = ((MethodSignature) signature).getReturnType();

        try {
            return proceedingJoinPoint.proceed();
        } catch (Throwable e) {
            if (OtherApiResponse.class.equals(returnType)) {
                return new OtherApiResponse<>(e);
            }

            if (ApiResponse.class.equals(returnType)) {
                return new ApiResponse<>(e);
            }

            if (BaseResponse.class.isAssignableFrom(returnType)) {
                BaseResponse<Object> response = new BaseResponse<>();
                response.setErrorMessage("Failed!");
                response.setData(e);
                return response;
            }

            throw e;
        }
    }
}

(4) 自定義Decoder

在自定義之前,我們先來看看Feign自己的默認Decoder實現:

由此可見,Feign默認使用了三個Decoder實現,OptionalDecoder,ResponseEntityDecoder,SpringDecoder,並在SpringDecoder中使用了Spring Boot默認的messageConverters。

自定義我們自己的Decoder:

public class CustomDecoder implements Decoder {
    private ObjectFactory<HttpMessageConverters> messageConverters;
    private final Decoder delegate;

    public CustomDecoder(ObjectFactory<HttpMessageConverters> messageConverters) {
        this.messageConverters = messageConverters;
        Objects.requireNonNull(this.messageConverters, "Message converters must not be null. ");
        this.delegate = new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)));
    }

    @Override
    public Object decode(Response response, Type type) throws IOException, DecodeException, FeignException {
        if (type instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) type;
            Type rawType = parameterizedType.getRawType();

            if (rawType instanceof Class) {
                @SuppressWarnings("rawtypes")
                Class rawClass = (Class) rawType;

                if (rawClass.equals(OtherApiResponse.class)) {
                    Type genericType = GenericTypeUtils.getGenericType(parameterizedType);
                    Object data = delegate.decode(response, genericType);
                    return new OtherApiResponse<Object>(data);
                }
            }
        }

        return delegate.decode(response, type);
    }
}

4. 注意

先看三段源碼

 

 

 

如果FeignClient里的方法的返回類型定義成Response,且請求的URL有返回值(404也算),則Feign會直接返回封裝好的Response,這屬於一個正常響應,所以它也不會進行重試,即便它的status code != 200。

所以,FeignClient並不適合進行下載請求(返回類型必須是Response),或者你可以通過重寫Feign.Builder來實現返回類型是Response時的重試。

四、日志

1. 首先我們可以打開Feign的日志記錄。在application.properties中添加:

feign.client.config.default.logger-level=basic

2. 上文已經提到Feign默認使用Slf4jLogger作為日志組件,所以我們可以更改其實現,按需將日志持久化。

3. 對於一些特定情況下的日志,比如我們只希望記錄請求失敗的日志,可以在RemoteAspect的catch里面捕獲並異步處理。

五、示例代碼

親測可用: https://github.com/eagle6688/devutility.test.springcloud/tree/master/devutility.test.springcloud.feign 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM