Spring cloud upgrade 2020.0 Version x-26 Components of openfeign

Posted by FRSH on Tue, 04 Jan 2022 22:24:25 +0100

Code address of this series: https://github.com/JoJoTec/spring-cloud-parent

First, we give the component structure diagram in the official document: [external chain picture transfer failed. The source station may have anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-jrw9fluy-1633609229588)( http://www.plantuml.com/plantuml/proxy?cache=no&src=https://raw.githubusercontent.com/OpenFeign/feign/master/src/docs/overview -mindmap. iuml)]

The components in the official document take the implementation function as the dimension, and here we take the source code implementation as the dimension (because we need to customize these components as needed when we use them later, we need to split and analyze them from the perspective of source code). There may be some small differences.

Contract responsible for parsing class metadata

OpenFeign automatically generates the HTTP API through proxy class metadata. What class metadata is parsed and which class metadata is valid is implemented by specifying a Contract. We can customize the parsing of some class metadata by implementing this Contract. For example, we customize an annotation:

//Available only on methods
@java.lang.annotation.Target(METHOD)
//Specifies that annotations remain at run time
@Retention(RUNTIME)
@interface Get {
    //Request uri
    String uri();
}

This annotation is very simple. The method marked with this annotation will be automatically encapsulated into a GET request, and the request uri is the return of uri().

Then, we customize a Contract to handle this annotation. Because MethodMetadata is final and package private, we can only inherit Contract Basecontract to customize annotation resolution:

//External customization must inherit BaseContract because the constructor of MethodMetadata generated inside is package private
static class CustomizedContract extends Contract.BaseContract {
    @Override
    protected void processAnnotationOnClass(MethodMetadata data, Class<?> clz) {
        //The annotation above the processing class is not used here
    }
    @Override
    protected void processAnnotationOnMethod(MethodMetadata data, Annotation annotation, Method method) {
        //Comments on processing methods
        Get get = method.getAnnotation(Get.class);
        //If the GET annotation exists, the HTTP request method of the specified method is GET, and the URI is specified as the return of the annotation uri()
        if (get != null) {
            data.template().method(Request.HttpMethod.GET);
            data.template().uri(get.uri());
        }
    }
    @Override
    protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) {
        //The comments above the processing parameters are not used here
        return false;
    }
}

Then, let's use this Contract:

interface HttpBin {
    @Get(uri = "/get")
    String get();
}

public static void main(String[] args) {
    HttpBin httpBin = Feign.builder()
            .contract(new CustomizedContract())
            .target(HttpBin.class, "http://www.httpbin.org");
    //It's actually a call http://www.httpbin.org/get
    String s = httpBin.get();
}

Generally, we will not use this Contract because we generally do not customize annotations in business. This is the function required by the underlying framework. For example, in the spring MVC environment, we need spring MVC compatible annotations. This implementation class is SpringMvcContract.

Encoder and Decoder

Encoder and decoder interface definition:

public interface Decoder {
  Object decode(Response response, Type type) throws IOException, DecodeException, FeignException;
}
public interface Encoder {
  void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException;
}

OpenFeign can customize the codec. Here we use fastjason to customize a set of codecs and decoders to understand the principle used.

/**
 * Deserialization decoder based on fastjason
 */
static class FastJsonDecoder implements Decoder {
    @Override
    public Object decode(Response response, Type type) throws IOException, DecodeException, FeignException {
        //Read body
        byte[] body = response.body().asInputStream().readAllBytes();
        return JSON.parseObject(body, type);
    }
}

/**
 * Serialization encoder based on fastjason
 */
static class FastJsonEncoder implements Encoder {
    @Override
    public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
        if (object != null) {
            //Code body
            template.header(CONTENT_TYPE, ContentType.APPLICATION_JSON.getMimeType());
            template.body(JSON.toJSONBytes(object), StandardCharsets.UTF_8);
        }
    }
}

Then we passed http://httpbin.org/anything To test, this link will return all the elements of the request we sent.

interface HttpBin {
    @RequestLine("POST /anything")
    Object postBody(Map<String, String> body);
}

public static void main(String[] args) {
    HttpBin httpBin = Feign.builder()
            .decoder(new FastJsonDecoder())
            .encoder(new FastJsonEncoder())
            .target(HttpBin.class, "http://www.httpbin.org");
    Object o = httpBin.postBody(Map.of("key", "value"));
}

Looking at the response, we can see that the json body we sent was correctly received.

At present, the encoder and decoder in OpenFeign project mainly include:

serialize

Additional dependencies need to be added

Implementation class

Convert directly to string, default codec

nothing

feign.codec.Encoder.Default and feign codec. Decoder. Default

gson

feign-gson

feign.gson.GsonEncoder and feign gson. GsonDecoder

xml

feign-jaxb

feign.jaxb.JAXBEncoder and feign jaxb. JAXBDecoder

json (jackson)

feign-jackson

feign. jackson. Jackson encoder and feign jackson. JacksonDecoder

When we use it in the Spring Cloud environment, there is a unified encoder and decoder in Spring MVC, that is, HttpMessageConverters, which are compatible through the glue project, so we can uniformly specify a custom codec with HttpMessageConverters.

Request interceptor

Interface definition of RequestInterceptor:

public interface RequestInterceptor {
  void apply(RequestTemplate template);
}

It can be seen from the interface that the RequestInterceptor actually performs additional operations on the RequestTemplate. Each request is processed by all requestinterceptors.

For example, we can add a specific Header for each request:

interface HttpBin {
    //For all requests sent to this link, the response will return all elements in the request
    @RequestLine("GET /anything")
    String anything();
}

static class AddHeaderRequestInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        //Add header
        template.header("test-header", "test-value");
    }
}

public static void main(String[] args) {
    HttpBin httpBin = Feign.builder()
            .requestInterceptor(new AddHeaderRequestInterceptor())
            .target(HttpBin.class, "http://www.httpbin.org");
    String s = httpBin.anything();
}

Execute the program, and you can see the header added in our sending request in the response.

Http request Client

The underlying Http request Client of OpenFeign can be customized. OpenFeign has encapsulation for different Http clients. By default, it uses the built-in Http request API in Java. Let's look at the interface definition source code of the Client:

public interface Client {
  /**
   * Execute request
   * @param request HTTP request
   * @param options configuration option
   * @return
   * @throws IOException
   */
  Response execute(Request request, Options options) throws IOException;
}

Request is the definition of Http request in feign. The implementation of the Client needs to convert the request into the request corresponding to the underlying Http Client and call the appropriate method to make the request. Options are common configurations for requests, including:

public static class Options {
    //tcp connection establishment timeout
    private final long connectTimeout;
    //tcp connection establishment timeout unit
    private final TimeUnit connectTimeoutUnit;
    //Request read response timeout
    private final long readTimeout;
    //Request read response timeout unit
    private final TimeUnit readTimeoutUnit;
    //Follow redirection
    private final boolean followRedirects;
}

At present, the implementation of Client includes the following:

Underlying HTTP client

Dependencies to add

Implementation class

Java HttpURLConnection

nothing

feign.Client.Default

Java 11 HttpClient

feign-java11

feign.http2client.Http2Client

Apache HttpClient

feign-httpclient

feign.httpclient.ApacheHttpClient

Apache HttpClient 5

feign-hc5

feign.hc5.ApacheHttp5Client

Google HTTP Client

feign-googlehttpclient

feign.googlehttpclient.GoogleHttpClient

Google HTTP Client

feign-googlehttpclient

feign.googlehttpclient.GoogleHttpClient

jaxRS

feign-jaxrs2

feign.jaxrs2.JAXRSClient

OkHttp

feign-okhttp

feign.okhttp.OkHttpClient

Ribbon

feign-ribbon

feign.ribbon.RibbonClient

Error decoder correlation

You can specify the error decoder ErrorDecoder and the exception throwing policy ExceptionPropagationPolicy

ErrorDecoder is used to read the HTTP response and judge whether there is an error and throw an exception:

public interface ErrorDecoder {
    public Exception decode(String methodKey, Response response);
}

The decode method of the configured ErrorDecoder will be called only when the response code is not 2xx. The default implementation of ErrorDecoder is:

public static class Default implements ErrorDecoder {

    @Override
    public Exception decode(String methodKey, Response response) {
      //Wrap different response codes into different exceptions
      FeignException exception = errorStatus(methodKey, response);
      //Extract the HTTP response header of retry after. If this response header exists, the exception will be encapsulated as RetryableException
      //For RetryableException, we will know that if this exception is thrown, the retry of the retrier will be triggered
      Date retryAfter = retryAfterDecoder.apply(firstOrNull(response.headers(), RETRY_AFTER));
      if (retryAfter != null) {
        return new RetryableException(
            response.status(),
            exception.getMessage(),
            response.request().httpMethod(),
            exception,
            retryAfter,
            response.request());
      }
      return exception;
    }
  }

It can be seen that the ErrorDecoder may encapsulate a layer of exceptions, which sometimes affects our capture in the outer layer, so we can disassemble this layer of encapsulation by specifying ExceptionPropagationPolicy. ExceptionPropagationPolicy is an enumeration class:

public enum ExceptionPropagationPolicy {
  //Don't do anything?
  NONE, 
  //Whether to extract the original exception of RetryableException and throw it as an exception
  //At present, it only takes effect for RetryableException. Call getCause of exception. If it is not empty, return this cause. Otherwise, return the original exception
  UNWRAP,
  ;
}

Let's take an example:

interface TestHttpBin {
    //The request must return 500
    @RequestLine("GET /status/500")
    Object get();
}

static class TestErrorDecoder implements ErrorDecoder {
    @Override
    public Exception decode(String methodKey, Response response) {
        //Get FeignException corresponding to error code
        FeignException exception = errorStatus(methodKey, response);
        //Encapsulated as RetryableException
        return new RetryableException(
                response.status(),
                exception.getMessage(),
                response.request().httpMethod(),
                exception,
                new Date(),
                response.request());
    }
}

public static void main(String[] args) {
    TestHttpBin httpBin = Feign.builder()
            .errorDecoder(new TestErrorDecoder())
            //If UNWRAP is not specified here, the exception thrown below is RetryableException. Otherwise, it is the cause of RetryableException, that is, FeignException
            .exceptionPropagationPolicy(ExceptionPropagationPolicy.UNWRAP)
            .target(TestHttpBin.class, "http://httpbin.org");
    httpBin.get();
}

After execution, you can find that feign. Is thrown FeignException$InternalServerError: [500 INTERNAL SERVER ERROR] during [GET] to [ http://httpbin.org/status/500 ][testhttpbin#get()]: [] this exception.

Retryer for RetryableException

When an exception occurs in the call, we may want to retry according to a certain strategy. This abstract retry strategy generally includes:

  • Which exceptions will be retried
  • When to retry and when to end the retry, for example, after n retries

Those exceptions will be retried, which is determined by ErrorDecoder. If the exception needs to be retried, it is encapsulated as a RetryableException, so Feign will retry using retryer. Retryer needs to consider when to retry and when to end the retry:

public interface Retryer extends Cloneable {
  /**
    * Judge whether to continue to retry or throw an exception to end the retry
    */
  void continueOrPropagate(RetryableException e);
  /**
    * For each request, this method is called to create a new Retryer object with the same configuration
    */
  Retryer clone();
}

Let's take a look at the default implementation of Retryer:

class Default implements Retryer {
	//max retries 
    private final int maxAttempts;
	//Initial retry interval
    private final long period;
	//Maximum retry interval
    private final long maxPeriod;
	//Current number of retries
    int attempt;
	//The retry interval and time currently waiting
    long sleptForMillis;

    public Default() {
	  //By default, the initial retry interval is 100ms, the maximum retry interval is 1s, and the maximum number of retries is 5
      this(100, SECONDS.toMillis(1), 5);
    }

    public Default(long period, long maxPeriod, int maxAttempts) {
      this.period = period;
      this.maxPeriod = maxPeriod;
      this.maxAttempts = maxAttempts;
	  //The current number of retries starts from 1, because the call has occurred before the first entry into continueOrPropagate, but it failed and threw a RetryableException
      this.attempt = 1;
    }

    // visible for testing;
    protected long currentTimeMillis() {
      return System.currentTimeMillis();
    }

    public void continueOrPropagate(RetryableException e) {
	  //If the current number of retries is greater than the maximum number of retries, the
      if (attempt++ >= maxAttempts) {
        throw e;
      }

      long interval;
	  //If retry after is specified, the waiting time is determined based on this header
      if (e.retryAfter() != null) {
        interval = e.retryAfter().getTime() - currentTimeMillis();
        if (interval > maxPeriod) {
          interval = maxPeriod;
        }
        if (interval < 0) {
          return;
        }
      } else {
		//Otherwise, it is calculated by nextMaxInterval
        interval = nextMaxInterval();
      }
      try {
        Thread.sleep(interval);
      } catch (InterruptedException ignored) {
        Thread.currentThread().interrupt();
        throw e;
      }
	  //Record the total waiting time
      sleptForMillis += interval;
    }

	//Increase each retry interval by 50% until the maximum retry interval
    long nextMaxInterval() {
      long interval = (long) (period * Math.pow(1.5, attempt - 1));
      return interval > maxPeriod ? maxPeriod : interval;
    }

    @Override
    public Retryer clone() {
	  //Copy configuration
      return new Default(period, maxPeriod, maxAttempts);
    }
}

The default Retryer has rich functions. Users can refer to this to implement a Retryer that is more suitable for their business scenarios.

Configuration Options for each HTTP request

The following configurations are required for any HTTP client:

  • Connection timeout: This is the TCP connection establishment timeout
  • Read timeout: This is the timeout before receiving the HTTP response
  • Follow redirection OpenFeign can be configured through Options:
public static class Options {
    private final long connectTimeout;
    private final TimeUnit connectTimeoutUnit;
    private final long readTimeout;
    private final TimeUnit readTimeoutUnit;
    private final boolean followRedirects;
}

For example, we can configure a connection timeout of 500ms and a read timeout of 6s to follow the redirected Feign:

Feign.builder().options(new Request.Options(
    500, TimeUnit.MILLISECONDS, 6, TimeUnit.SECONDS, true
))

In this section, we introduced various components of OpenFeign in detail. With this knowledge, we can actually implement the glue code in spring cloud OpenFeign ourselves. In fact, spring cloud OpenFeign registers these components in the NamedContextFactory in the form of beans for different configurations of different microservices.