OpenFeign学习(五):OpenFeign请求结果处理及重试控制

#说明
在上篇博文《OpenFeign学习(四):OpenFeign的方法同步请求执行》一文中,我对OpenFeign的同步请求的执行的原理进行了介绍和学习。本篇博文我将继续通过源码对请求之后结果的封装解码及失败重试进行介绍和学习。

正文

在上篇博文中提到,OpenFeign通过SynchronousMethodHandler进行同步方法请求处理,在介绍executeAndDecode方法源码时,只介绍了请求执行部分。接下来,我们继续通过源码来了解在请求结束后,OpenFeign如何对请求结果进行处理。

在阅读executeAndDecode源码时,我们再回顾下通过配置的Client进行实际请求执行的excute方法,这里我通过OkHttpClient进行请求。

Client.execute

public Response execute(feign.Request input, Options options) throws IOException {
    okhttp3.OkHttpClient requestScoped;
    if (this.delegate.connectTimeoutMillis() == options.connectTimeoutMillis() && this.delegate.readTimeoutMillis() == options.readTimeoutMillis() && this.delegate.followRedirects() == options.isFollowRedirects()) {
        requestScoped = this.delegate;
    } else {
        requestScoped = this.delegate.newBuilder().connectTimeout((long)options.connectTimeoutMillis(), TimeUnit.MILLISECONDS).readTimeout((long)options.readTimeoutMillis(), TimeUnit.MILLISECONDS).followRedirects(options.isFollowRedirects()).build();
    }

    Request request = toOkHttpRequest(input);
    okhttp3.Response response = requestScoped.newCall(request).execute();
    return toFeignResponse(response, input).toBuilder().request(input).build();
}

可以看到在请求结束后,通过toFeignResponse方法将不同客户端的请求结果统一封装为OepnFeign的Response。

toFeignResponse(response, input).toBuilder().request(input).build();

这里在阅读源码时,我不理解的是通过toFeignResonse方法已经为Response设置了请求的Request,但在后续中又通过构造器重新设置Request并重新创建了Response实例。

private static Response toFeignResponse(okhttp3.Response response, feign.Request request) throws IOException {
    return Response.builder().status(response.code()).reason(response.message()).request(request).headers(toMap(response.headers())).body(toBody(response.body())).build();
}

结果封装完毕后返回executeAndDecode方法,开始对请求结果进行处理。

executeAndDecode

Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
    Request request = this.targetRequest(template);
    if (this.logLevel != Level.NONE) {
        this.logger.logRequest(this.metadata.configKey(), this.logLevel, request);
    }

    long start = System.nanoTime();

    Response response;
    try {
        response = this.client.execute(request, options);
        // 通过Response的构造器重新设置request及设置requestTemplate
        response = response.toBuilder().request(request).requestTemplate(template).build();
    } catch (IOException var16) {
        if (this.logLevel != Level.NONE) {
            this.logger.logIOException(this.metadata.configKey(), this.logLevel, var16, this.elapsedTime(start));
        }

        throw FeignException.errorExecuting(request, var16);
    }

    long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
    boolean shouldClose = true;

    Response var11;
    try {
        if (this.logLevel != Level.NONE) {
            response = this.logger.logAndRebufferResponse(this.metadata.configKey(), this.logLevel, response, elapsedTime);
        }

        Response var19;
        // 当方法返回类型不为Response时,对结果进行解析处理
        if (Response.class != this.metadata.returnType()) {
            Object result;
            Object var21;
            if (response.status() >= 200 && response.status() < 300) {
                if (Void.TYPE == this.metadata.returnType()) {
                    var19 = null;
                    return var19;
                }
                
                // 请求成功,通过配置Decoder对结果进行解码
                result = this.decode(response);
                shouldClose = this.closeAfterDecode;
                var21 = result;
                return var21;
            }

            // 当请求响应为404并且设置了对404进行解码且方法返回类型不为Voids时,对结果进行解码返回
            if (this.decode404 && response.status() == 404 && Void.TYPE != this.metadata.returnType()) {
                result = this.decode(response);
                shouldClose = this.closeAfterDecode;
                var21 = result;
                return var21;
            }

            // 通过配置的errorDecoder对结果进行解码
            throw this.errorDecoder.decode(this.metadata.configKey(), response);
        }

        if (response.body() == null) {
            var19 = response;
            return var19;
        }

        if (response.body().length() == null || (long)response.body().length() > 8192L) {
            shouldClose = false;
            var19 = response;
            return var19;
        }
        
        byte[] bodyData = Util.toByteArray(response.body().asInputStream());
        var11 = response.toBuilder().body(bodyData).build();
    } catch (IOException var17) {
        if (this.logLevel != Level.NONE) {
            this.logger.logIOException(this.metadata.configKey(), this.logLevel, var17, elapsedTime);
        }

        throw FeignException.errorReading(request, response, var17);
    } finally {
        if (shouldClose) {
            Util.ensureClosed(response.body());
        }

    }

    return var11;
}

通过源码可以看到,在请求成功且方法的返回类型不为Void时,通过decode方法对方法进行解码。在默认情况下,OpenFeign使用的是默认的Decoder实现类Default。

public static class Default extends StringDecoder {
    public Default() {
    }

    public Object decode(Response response, Type type) throws IOException {
        if (response.status() != 404 && response.status() != 204) {
            if (response.body() == null) {
                return null;
            } else {
                return byte[].class.equals(type) ? Util.toByteArray(response.body().asInputStream()) : super.decode(response, type);
            }
        } else {
            return Util.emptyValueOf(type);
        }
    }
}

可以看到,如果方法的返回类型不为byte[]或者String类型时,并且没有显式配置返回类型对应的Decoder,该方法将抛出DecodeException异常

在针对404的响应处理时,首先判断了decode404的值,表示是否对404结果进行编码。该值的默认值为false。可以看出OpenFeign对404异常结果不会进行重试。

最后通过配置的errorDecoder对结果进行编码,该值可以创建代理对象时通过构造器进行配置,默认值为OpenFeign的ErrorDecoder的默认实现类Default。

public Exception decode(String methodKey, Response response) {
    FeignException exception = FeignException.errorStatus(methodKey, response);
    Date retryAfter = this.retryAfterDecoder.apply((String)this.firstOrNull(response.headers(), "Retry-After"));
    return (Exception)(retryAfter != null ? new RetryableException(response.status(), exception.getMessage(), response.request().httpMethod(), exception, retryAfter, response.request()) : exception);
}

该类的decode方法返回了一个异常,在方法中通过判断响应头中是否设置了Retry-After值来返回不同类型了异常。当设置了该值时,返回RetryableException异常,OpenFeign将对该异常进行重试

在官方文档中已说明,Feign默认对IOException和ErrorDecoder返回的RetryableException异常进行自动重试,其他异常则会忽略。若希望改变此规则,可以自定义实现配置Retryer。

Feign, by default, will automatically retry IOExceptions, regardless of HTTP method, treating them as transient network related exceptions, and any RetryableException thrown from an ErrorDecoder. To customize this behavior, register a custom Retryer instance via the builder.

所以,如果希望对404结果进行重试,可以实现自己的Retryer。

接下来,我们继续了解OpenFeign的Retryer是如何进行重试的。

SynchronousMethodHandler.invoke

返回到请求处理的初始方法invoke中

public Object invoke(Object[] argv) throws Throwable {
    RequestTemplate template = this.buildTemplateFromArgs.create(argv);
    Options options = this.findOptions(argv);
    Retryer retryer = this.retryer.clone();

    while(true) {
        try {
            return this.executeAndDecode(template, options);
        } catch (RetryableException var9) {
            RetryableException e = var9;

            try {
                retryer.continueOrPropagate(e);
            } catch (RetryableException var8) {
                Throwable cause = var8.getCause();
                if (this.propagationPolicy == ExceptionPropagationPolicy.UNWRAP && cause != null) {
                    throw cause;
                }

                throw var8;
            }

            if (this.logLevel != Level.NONE) {
                this.logger.logRetry(this.metadata.configKey(), this.logLevel);
            }
        }
    }
}

可以看到invoke方法使用循环处理请求,直到正常返回或者重试抛出异常。在重试时,调用了Retryer实现类的continueOrPropagate方法。OpenFeign默认使用Retryer的实现类Default。

public void continueOrPropagate(RetryableException e) {
    if (this.attempt++ >= this.maxAttempts) {
        throw e;
    } else {
        long interval;
        if (e.retryAfter() != null) {
            // 根据响应设置的Retry-After来设置间隔时间,该间隔时间不能超过最大间隔时间
            interval = e.retryAfter().getTime() - this.currentTimeMillis();
            if (interval > this.maxPeriod) {
                interval = this.maxPeriod;
            }

            if (interval < 0L) {
                return;
            }
        } else {
            // 根据重试次数获取间隔时间
            interval = this.nextMaxInterval();
        }

        try {
            Thread.sleep(interval);
        } catch (InterruptedException var5) {
            Thread.currentThread().interrupt();
            throw e;
        }

        this.sleptForMillis += interval;
    }
}

通过源码可以看到,如果重试次数超过设定值,则会抛出原异常。在设置间隔时间时,首先根据响应头设置的RetryAfter值与当前时间进行计算,所允许的间隔时间不能超过设定的最大值maxPeriod。

若没有设置RetryAfter的值,则会调用nextMaxInterval获取间隔时间。

long nextMaxInterval() {
    long interval = (long)((double)this.period * Math.pow(1.5D, (double)(this.attempt - 1)));
    return interval > this.maxPeriod ? this.maxPeriod : interval;
}

可以看到间隔时间会根据重试次数进行翻倍,但同时保证了不会超过设置的最大间隔时间

得到间隔时间后,调用Thread.sleep(interval)进行睡眠,返回后进行执行executeAndDecode方法,直到重试成功或超过重试次数。

通过以上源码,我们可以发现默认的Retryer在重试时,只是做了重试频率的控制,如果希望在重试时更新相关信息或者其他操作可以创建Retryer接口实现类,实现自己的Retryer,在实现时,clone方法必须重写,保证每个请求都是一个新的Retryer实例。

至此,我们对OpenFeign的代理对象的配置创建,方法的同步请求执行及请求结果处理和重试控制的流程已经有所了解,我对OpenFeign的源码学习也告一段落。OpenFeign框架的出色设计及其使用的方便性,灵活性都值得我们深究其源码,在源码学习中,通过了解其运行原理,使用方法,编码风格等等来提高自身的编码水平。

在微服务盛行的今天,我们避免不了使用springCloud框架,接下来我将继续学习介绍如何在微服务中使用Spring Cloud OpenFeign及SpringCloud是如何对OpenFeign进行集成支持的。


参考资料:
https://github.com/OpenFeign/feign

你可能感兴趣的:(Feign学习)