Spring Cloud Feign 熔斷機制填坑

cipher發表於2018-06-13

原文連結:www.ciphermagic.cn/spring-clou…

問題

最近在專案開發中,使用 Feign 呼叫服務,當觸發熔斷機制時,遇到了以下問題:

  • 異常資訊形如:TestService#addRecord(ParamVO) failed and no fallback available.
  • 獲取不到服務提供方丟擲的原始異常資訊;
  • 實現某些業務方法不進入熔斷,直接往外丟擲異常;

接下來將一一解決上述問題。

對於failed and no fallback available.這種異常資訊,是因為專案開啟了熔斷:

feign.hystrix.enabled: true
複製程式碼

當呼叫服務時丟擲了異常,卻沒有定義fallback方法,就會丟擲上述異常。由此引出了第一個解決方式。

@FeignClient加上fallback方法,並獲取異常資訊

@FeignClient修飾的介面加上fallback方法有兩種方式,由於要獲取異常資訊,所以使用fallbackFactory的方式:

@FeignClient(name = "serviceId", fallbackFactory = TestServiceFallback.class)
public interface TestService {

    @RequestMapping(value = "/get/{id}", method = RequestMethod.GET)
    Result get(@PathVariable("id") Integer id);
    
}
複製程式碼

@FeignClient註解中指定fallbackFactory,上面例子中是TestServiceFallback

import feign.hystrix.FallbackFactory;
import org.apache.commons.lang3.StringUtils;

@Component
public class TestServiceFallback implements FallbackFactory<TestService> {

    private static final Logger LOG = LoggerFactory.getLogger(TestServiceFallback.class);

    public static final String ERR_MSG = "Test介面暫時不可用: ";

    @Override
    public TestService create(Throwable throwable) {
        String msg = throwable == null ? "" : throwable.getMessage();
        if (!StringUtils.isEmpty(msg)) {
            LOG.error(msg);
        }
        return new TestService() {
            @Override
            public String get(Integer id) {
                return ResultBuilder.unsuccess(ERR_MSG + msg);
            }
        };
    }
}
複製程式碼

通過實現FallbackFactory,可以在create方法中獲取到服務丟擲的異常。但是請注意,這裡的異常是被Feign封裝過的異常,不能直接在異常資訊中看出原始方法丟擲的異常。這時得到的異常資訊形如:

status 500 reading TestService#addRecord(ParamVO); content:
{"success":false,"resultCode":null,"message":"/ by zero","model":null,"models":[],"pageInfo":null,"timelineInfo":null,"extra":null,"validationMessages":null,"valid":false}
複製程式碼

說明一下,本例子中,服務提供者的介面返回資訊會統一封裝在自定義類Result中,內容就是上述的content

{"success":false,"resultCode":null,"message":"/ by zero","model":null,"models":[],"pageInfo":null,"timelineInfo":null,"extra":null,"validationMessages":null,"valid":false}
複製程式碼

因此,異常資訊我希望是message的內容:/ by zero,這樣打日誌時能夠方便識別異常。

保留原始異常資訊

當呼叫服務時,如果服務返回的狀態碼不是200,就會進入到FeignErrorDecoder中,因此如果我們要解析異常資訊,就要重寫ErrorDecoder

import feign.Response;
import feign.Util;
import feign.codec.ErrorDecoder;

/**
 * @Author: CipherCui
 * @Description: 保留 feign 服務異常資訊
 * @Date: Created in 1:29 2018/6/2
 */
public class KeepErrMsgConfiguration {

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

    /**
     * 自定義錯誤解碼器
     */
    public class UserErrorDecoder implements ErrorDecoder {

        private Logger logger = LoggerFactory.getLogger(getClass());

        @Override
        public Exception decode(String methodKey, Response response) {
            Exception exception = null;
            try {
                // 獲取原始的返回內容
                String json = Util.toString(response.body().asReader());
                exception = new RuntimeException(json);
                // 將返回內容反序列化為Result,這裡應根據自身專案作修改
                Result result = JsonMapper.nonEmptyMapper().fromJson(json, Result.class);
                // 業務異常丟擲簡單的 RuntimeException,保留原來錯誤資訊
                if (!result.isSuccess()) {
                    exception = new RuntimeException(result.getMessage());
                }
            } catch (IOException ex) {
                logger.error(ex.getMessage(), ex);
            }
            return exception;
        }
    }

}
複製程式碼

上面是一個例子,原理是根據response.body()反序列化為自定義的Result類,提取出裡面的message資訊,然後丟擲RuntimeException,這樣當進入到熔斷方法中時,獲取到的異常就是我們處理過的RuntimeException

注意上面的例子並不是通用的,但原理是相通的,大家要結合自身的專案作相應的修改。

要使上面程式碼發揮作用,還需要在@FeignClient註解中指定configuration

@FeignClient(name = "serviceId", fallbackFactory = TestServiceFallback.class, configuration = {KeepErrMsgConfiguration.class})
public interface TestService {

    @RequestMapping(value = "/get/{id}", method = RequestMethod.GET)
    String get(@PathVariable("id") Integer id);
    
}
複製程式碼

不進入熔斷,直接丟擲異常

有時我們並不希望方法進入熔斷邏輯,只是把異常原樣往外拋。這種情況我們只需要捉住兩個點:不進入熔斷原樣

原樣就是獲取原始的異常,上面已經介紹過了,而不進入熔斷,需要把異常封裝成HystrixBadRequestException對於HystrixBadRequestExceptionFeign會直接丟擲,不進入熔斷方法。

因此我們只需要在上述KeepErrMsgConfiguration的基礎上作一點修改即可:

/**
 * @Author: CipherCui
 * @Description: feign 服務異常不進入熔斷
 * @Date: Created in 1:29 2018/6/2
 */
public class NotBreakerConfiguration {

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

    /**
     * 自定義錯誤解碼器
     */
    public class UserErrorDecoder implements ErrorDecoder {

        private Logger logger = LoggerFactory.getLogger(getClass());

        @Override
        public Exception decode(String methodKey, Response response) {
            Exception exception = null;
            try {
                String json = Util.toString(response.body().asReader());
                exception = new RuntimeException(json);
                Result result = JsonMapper.nonEmptyMapper().fromJson(json, Result.class);
                // 業務異常包裝成 HystrixBadRequestException,不進入熔斷邏輯
                if (!result.isSuccess()) {
                    exception = new HystrixBadRequestException(result.getMessage());
                }
            } catch (IOException ex) {
                logger.error(ex.getMessage(), ex);
            }
            return exception;
        }
    }

}
複製程式碼

總結

為了更好的達到熔斷效果,我們應該為每個介面指定fallback方法。而根據自身的業務特點,可以靈活的配置上述的KeepErrMsgConfigurationNotBreakerConfiguration,或自己編寫Configuration

以上例子特殊性較強,不足之處請不吝指教。希望大家可以從中獲取到有用的東西,應用到自己的專案中,感謝閱讀。

原文連結:www.ciphermagic.cn/spring-clou…

相關文章