zoukankan      html  css  js  c++  java
  • 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
    ", methodKey, response.request().url());
            Exception exception = defaultErrorDecoder.decode(methodKey, response);
            System.out.printf("CustomErrorDecoder, status: %d, exception: %s
    ", 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 

  • 相关阅读:
    Vuex基本用法
    vue ui 创建项目报错: Failed to get response from https://registry.yarnpkg.com/vue-cli-version-marker
    vue+elementui {{ }} 的使用
    vue+elementui项目去除html标签
    vue+elementui 将后端传来数据编程tree结构、同一级别数据对象
    为某个table项添加slot
    vue+elementui项目,table项内容超过确定行以“...”显示剩余部分的实现
    前端对后台数据的处理
    vue+elementUI项目获取数组最后一位,防止深拷贝问题
    thinkPHP 控制器定义
  • 原文地址:https://www.cnblogs.com/eagle6688/p/13087195.html
Copyright © 2011-2022 走看看