Feign design principle

Posted by nicandre on Wed, 05 Jan 2022 23:45:53 +0100

What is Feign?

Feign means "pretend, disguise and deform" in English. It is a lightweight framework for HTTP request calling. It can call HTTP requests in the way of java interface annotation instead of directly calling them by encapsulating HTTP request messages in Java. Feign templates the request by processing annotations. When it is actually called, it passes in parameters, which are then applied to the request according to the parameters, and then transformed into a real request. This request is relatively intuitive.
Feign is widely used in Spring Cloud solutions. It is an indispensable component for learning microservice architecture based on Spring Cloud.
Open source project address:
https://github.com/OpenFeign/feign

What problem did Feign solve?

It encapsulates the Http call process, which is more suitable for interface oriented programming
In the scenario of service invocation, we often call services based on Http protocol, and the frameworks we often use may include HttpURLConnection, Apache httpcomposites, OkHttp3, Netty, etc. these frameworks provide their own characteristics based on their own focus. From the perspective of role division, their functions are consistent, providing Http invocation services. The specific process is as follows:

 

How is Feign designed?

 PHASE 1. Generation of implementation classes based on interface oriented dynamic proxy (jdk dynamic proxy)

When feign is used, the corresponding interface class will be defined, and HTTP related annotations will be used on the interface class to identify the HTTP request parameter information, as shown below:

interface GitHub {
  @RequestLine("GET /repos/{owner}/{repo}/contributors")
  List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);
}

public static class Contributor {
  String login;
  int contributions;
}

public class MyApp {
  public static void main(String... args) {
    GitHub github = Feign.builder()
                         .decoder(new GsonDecoder())
                         .target(GitHub.class, "https://api.github.com");
  
    // Fetch and print a list of the contributors to this library.
    List<Contributor> contributors = github.contributors("OpenFeign", "feign");
    for (Contributor contributor : contributors) {
      System.out.println(contributor.login + " (" + contributor.contributions + ")");
    }
  }
}

At the bottom layer of Feign, the implementation class is generated based on the interface oriented dynamic proxy method, and the request call is delegated to the dynamic proxy implementation class. The basic principle is as follows:

 public class ReflectiveFeign extends Feign{
  ///Omit some codes
  @Override
  public <T> T newInstance(Target<T> target) {
    //According to the interface class and the Contract protocol parsing method, the methods and annotations on the interface class are parsed and converted into the internal MethodHandler processing method
    Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
    Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
    List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();

    for (Method method : target.type().getMethods()) {
      if (method.getDeclaringClass() == Object.class) {
        continue;
      } else if(Util.isDefault(method)) {
        DefaultMethodHandler handler = new DefaultMethodHandler(method);
        defaultMethodHandlers.add(handler);
        methodToHandler.put(method, handler);
      } else {
        methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
      }
    }
    InvocationHandler handler = factory.create(target, methodToHandler);
    // Based on proxy Newproxyinstance creates a dynamic implementation for the interface class and converts all requests to InvocationHandler for processing.
    T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[]{target.type()}, handler);

    for(DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
      defaultMethodHandler.bindTo(proxy);
    }
    return proxy;
  }
  //Omit some codes

 PHASE 2. According to the Contract protocol rules, the annotation information of the interface class is parsed into internal representation:

Feign defines the conversion protocol as follows:

/**
 * Defines what annotations and values are valid on interfaces.
 */
public interface Contract {

  /**
   * Called to parse the methods in the class that are linked to HTTP requests.
   * The incoming interface definition is parsed into the corresponding method internal metadata representation
   * @param targetType {@link feign.Target#type() type} of the Feign interface.
   */
  // TODO: break this and correct spelling at some point
  List<MethodMetadata> parseAndValidatateMetadata(Class<?> targetType);
}

Default Contract implementation

Feign has its own protocol specification by default, which specifies some annotations that can be mapped into corresponding Http requests, such as an official example:

public interface GitHub {
  
  @RequestLine("GET /repos/{owner}/{repo}/contributors")
  List<Contributor> getContributors(@Param("owner") String owner, @Param("repo") String repository);
  
  class Contributor {
    String login;
    int contributions;
  }
}

In the above example, try calling GitHub Getcontributors ("foo", "myrepo") will be converted into the following HTTP requests:

GET /repos/foo/myrepo/contributors
HOST XXXX.XXX.XXX

Feign default protocol specification

How FeignContract is parsed is beyond the scope of this article. For details, please refer to the code:
feign/Contract.java at master · OpenFeign/feign · GitHub

Spring MVC based protocol specification spring mvccontract:

In the current Spring Cloud microservice solution, in order to reduce the learning cost, some annotations of Spring MVC are used to complete the request protocol resolution, that is, writing the client request interface is like writing the server code: the client and the server can agree through the SDK, and the client only needs to introduce the SDK API published by the server, Interface oriented coding can be used to connect services:

According to this idea, our team defines the server starter in combination with the characteristics of Spring Boot Starter,
When using the service, the service consumer only needs to introduce Starter to call the service. This is more suitable for platform independence. The advantage of interface abstraction is that it can switch according to the implementation mode of service call:

It can be called based on simple Http service;
It can be invoked based on Spring Cloud micro service architecture;
Service governance can be based on Dubbo SOA
This mode is more suitable for self switching in the SaSS mixed software service mode. Select the appropriate deployment mode according to the customer's hardware capability, or deploy micro services based on their own service cluster

For how Spring Cloud implements protocol resolution, please refer to the following code:
https://github.com/spring-cloud/spring-cloud-openfeign/blob/master/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringMvcContract.java

Of course, the current Spring MVC annotations can not be fully used. Some annotations are not supported, such as @ GetMapping,@PutMapping, etc., and only @ RequestMapping is supported. In addition, there are some problems in annotation inheritance; The specific restrictions and details can be different in each version. You can refer to the above code implementation, which is relatively simple.

Spring Cloud does not do Feign client annotation protocol analysis based on all Spring MVC annotations. Personally, I think this is not a small pit. When I first started Spring Cloud, I encountered this problem. Later, it was solved by going deep into the code... This should be handled by someone who wrote an enhanced class. Not to mention the table, MARK first. It is a good opportunity to practice open source code.

PHASE 3. Dynamically generate Request based on RequestBean

According to the incoming Bean object and annotation information, extract the corresponding values to construct the Http Request object:

PHASE 4. Use Encoder to convert Bean into Http message body (message parsing and transcoding logic)

Feign will eventually convert the request into an Http message and send it out. The incoming request object will eventually be parsed into a message body, as shown below:

Feign makes a relatively simple interface definition and abstracts the Encoder and decoder interfaces:

public interface Encoder {
  /** Type literal for {@code Map<String, ?>}, indicating the object to encode is a form. */
  Type MAP_STRING_WILDCARD = Util.MAP_STRING_WILDCARD;

  /**
   * Converts objects to an appropriate representation in the template.
   *  Convert the entity object into the message body of the Http request
   * @param object   what to encode as the request body.
   * @param bodyType the type the object should be encoded as. {@link #MAP_STRING_WILDCARD}
   *                 indicates form encoding.
   * @param template the request template to populate.
   * @throws EncodeException when encoding failed due to a checked exception.
   */
  void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException;

  /**
   * Default implementation of {@code Encoder}.
   */
  class Default implements Encoder {

    @Override
    public void encode(Object object, Type bodyType, RequestTemplate template) {
      if (bodyType == String.class) {
        template.body(object.toString());
      } else if (bodyType == byte[].class) {
        template.body((byte[]) object, null);
      } else if (object != null) {
        throw new EncodeException(
            format("%s is not a type supported by this encoder.", object.getClass()));
      }
    }
  }
}

 

public interface Decoder {

  /**
   * Decodes an http response into an object corresponding to its {@link
   * java.lang.reflect.Method#getGenericReturnType() generic return type}. If you need to wrap
   * exceptions, please do so via {@link DecodeException}.
   *  Extract the Http message body from the Response, and automatically assemble the message through the return type declared by the interface class
   * @param response the response to decode 
   * @param type     {@link java.lang.reflect.Method#getGenericReturnType() generic return type} of
   *                 the method corresponding to this {@code response}.
   * @return instance of {@code type}
   * @throws IOException     will be propagated safely to the caller.
   * @throws DecodeException when decoding failed due to a checked exception besides IOException.
   * @throws FeignException  when decoding succeeds, but conveys the operation failed.
   */
  Object decode(Response response, Type type) throws IOException, DecodeException, FeignException;

  /** Default implementation of {@code Decoder}. */
  public class Default extends StringDecoder {

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

Feign currently has the following implementations:

Encoder/ Decoder implementationexplain
JacksonEncoder,JacksonDecoderPersistence conversion protocol based on Jackson format
GsonEncoder,GsonDecoderPersistence conversion protocol based on Google GSON format
SaxEncoder,SaxDecoderPersistence conversion protocol of Sax Library Based on XML format
JAXBEncoder,JAXBDecoderJAXB library persistence conversion protocol based on XML format
ResponseEntityEncoder,ResponseEntityDecoderSpring MVC is based on the conversion protocol of responseentity < T > return format
SpringEncoder,SpringDecoderA conversion protocol based on a set of mechanisms of Spring MVC HttpMessageConverters is applied to the Spring Cloud system

PHASE 5. Interceptors are responsible for processing requests and returns

In the process of request conversion, Feign abstracts the interceptor interface for user-defined operations on requests:

public interface RequestInterceptor {

  /**
   * When constructing a RequestTemplate request, you can add or modify Header, Method, Body and other information
   * Called for every request. Add data using methods on the supplied {@link RequestTemplate}.
   */
  void apply(RequestTemplate template);
}

For example, if you want Http messages to be compressed during delivery, you can define a request Interceptor:

public class FeignAcceptGzipEncodingInterceptor extends BaseRequestInterceptor {

	/**
	 * Creates new instance of {@link FeignAcceptGzipEncodingInterceptor}.
	 *
	 * @param properties the encoding properties
	 */
	protected FeignAcceptGzipEncodingInterceptor(FeignClientEncodingProperties properties) {
		super(properties);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void apply(RequestTemplate template) {
		//  Add the corresponding data information in the Header header
		addHeader(template, HttpEncoding.ACCEPT_ENCODING_HEADER, HttpEncoding.GZIP_ENCODING,
				HttpEncoding.DEFLATE_ENCODING);
	}
}

PHASE 6. Logging

When sending and receiving requests, Feign defines a unified log interface to output log information, and defines four levels of log output:

levelexplain
NONEDo not make any records
BASICOnly the output Http method name, request URL, return status code and execution time are recorded
HEADERSRecord the output Http method name, request URL, return status code, execution time and Header information
FULLRecord the Header, Body and some Request metadata of Request and Response
public abstract class Logger {

  protected static String methodTag(String configKey) {
    return new StringBuilder().append('[').append(configKey.substring(0, configKey.indexOf('(')))
        .append("] ").toString();
  }

  /**
   * Override to log requests and responses using your own implementation. Messages will be http
   * request and response text.
   *
   * @param configKey value of {@link Feign#configKey(Class, java.lang.reflect.Method)}
   * @param format    {@link java.util.Formatter format string}
   * @param args      arguments applied to {@code format}
   */
  protected abstract void log(String configKey, String format, Object... args);

  protected void logRequest(String configKey, Level logLevel, Request request) {
    log(configKey, "---> %s %s HTTP/1.1", request.method(), request.url());
    if (logLevel.ordinal() >= Level.HEADERS.ordinal()) {

      for (String field : request.headers().keySet()) {
        for (String value : valuesOrEmpty(request.headers(), field)) {
          log(configKey, "%s: %s", field, value);
        }
      }

      int bodyLength = 0;
      if (request.body() != null) {
        bodyLength = request.body().length;
        if (logLevel.ordinal() >= Level.FULL.ordinal()) {
          String
              bodyText =
              request.charset() != null ? new String(request.body(), request.charset()) : null;
          log(configKey, ""); // CRLF
          log(configKey, "%s", bodyText != null ? bodyText : "Binary data");
        }
      }
      log(configKey, "---> END HTTP (%s-byte body)", bodyLength);
    }
  }

  protected void logRetry(String configKey, Level logLevel) {
    log(configKey, "---> RETRYING");
  }

  protected Response logAndRebufferResponse(String configKey, Level logLevel, Response response,
                                            long elapsedTime) throws IOException {
    String reason = response.reason() != null && logLevel.compareTo(Level.NONE) > 0 ?
        " " + response.reason() : "";
    int status = response.status();
    log(configKey, "<--- HTTP/1.1 %s%s (%sms)", status, reason, elapsedTime);
    if (logLevel.ordinal() >= Level.HEADERS.ordinal()) {

      for (String field : response.headers().keySet()) {
        for (String value : valuesOrEmpty(response.headers(), field)) {
          log(configKey, "%s: %s", field, value);
        }
      }

      int bodyLength = 0;
      if (response.body() != null && !(status == 204 || status == 205)) {
        // HTTP 204 No Content "...response MUST NOT include a message-body"
        // HTTP 205 Reset Content "...response MUST NOT include an entity"
        if (logLevel.ordinal() >= Level.FULL.ordinal()) {
          log(configKey, ""); // CRLF
        }
        byte[] bodyData = Util.toByteArray(response.body().asInputStream());
        bodyLength = bodyData.length;
        if (logLevel.ordinal() >= Level.FULL.ordinal() && bodyLength > 0) {
          log(configKey, "%s", decodeOrDefault(bodyData, UTF_8, "Binary data"));
        }
        log(configKey, "<--- END HTTP (%s-byte body)", bodyLength);
        return response.toBuilder().body(bodyData).build();
      } else {
        log(configKey, "<--- END HTTP (%s-byte body)", bodyLength);
      }
    }
    return response;
  }

  protected IOException logIOException(String configKey, Level logLevel, IOException ioe, long elapsedTime) {
    log(configKey, "<--- ERROR %s: %s (%sms)", ioe.getClass().getSimpleName(), ioe.getMessage(),
        elapsedTime);
    if (logLevel.ordinal() >= Level.FULL.ordinal()) {
      StringWriter sw = new StringWriter();
      ioe.printStackTrace(new PrintWriter(sw));
      log(configKey, sw.toString());
      log(configKey, "<--- END ERROR");
    }
    return ioe;
  }

PHASE 7 . Send HTTP request based on retrier

Feign has a built-in retrier. When an IO exception occurs in an HTTP request, feign will have a maximum number of attempts to send the request. The following is the core of feign
Code logic:

final class SynchronousMethodHandler implements MethodHandler {

  // Omit some codes

  @Override
  public Object invoke(Object[] argv) throws Throwable {
   //Construct the Http request according to the input parameters.
    RequestTemplate template = buildTemplateFromArgs.create(argv);
    // Clone a retrier
    Retryer retryer = this.retryer.clone();
    // The maximum number of attempts. If there is a result in the middle, it will be returned directly
    while (true) {
      try {
        return executeAndDecode(template);
      } catch (RetryableException e) {
        retryer.continueOrPropagate(e);
        if (logLevel != Logger.Level.NONE) {
          logger.logRetry(metadata.configKey(), logLevel);
        }
        continue;
      }
    }
  }

The retrier has the following control parameters:

Retry parametersexplainDefault value
periodThe initial retry interval. When the request fails, the retrier will pause the initial interval (thread sleep) before starting, so as to avoid forcing the request and wasting performance
maxPeriodWhen the request fails continuously, the retry interval will be as follows: long interval = (long) (period * Math.pow(1.5, attempt - 1)); The calculation is extended in an equal proportion, but the maximum interval is maxPeriod. Setting this value can avoid too long execution cycle when there are too many retries
 
maxAttemptsmax retries

For specific code implementation, refer to:
https://github.com/OpenFeign/feign/blob/master/core/src/main/java/feign/Retryer.java

PHASE 8. Send Http request

Feign actually sends HTTP requests to feign Client to do:

public interface Client {

  /**
   * Executes a request against its {@link Request#url() url} and returns a response.
   *  Execute Http request and return Response
   * @param request safe to replay.
   * @param options options to apply to this request.
   * @return connected response, {@link Response.Body} is absent or unread.
   * @throws IOException on a network error connecting to {@link Request#url()}.
   */
  Response execute(Request request, Options options) throws IOException;
  }

Feign's underlying layer is Java. Java through JDK by default net. HttpURLConnection implements feign The client interface class creates a new HttpURLConnection link every time a request is sent, which is why feign has poor performance by default. By expanding this interface, high-performance Http clients based on connection pool such as Apache HttpClient or OkHttp3 can be used. OkHttp3 is used internally in our project as an Http client.

The following is the default implementation of Feign for reference:
 

public static class Default implements Client {

    private final SSLSocketFactory sslContextFactory;
    private final HostnameVerifier hostnameVerifier;

    /**
     * Null parameters imply platform defaults.
     */
    public Default(SSLSocketFactory sslContextFactory, HostnameVerifier hostnameVerifier) {
      this.sslContextFactory = sslContextFactory;
      this.hostnameVerifier = hostnameVerifier;
    }

    @Override
    public Response execute(Request request, Options options) throws IOException {
      HttpURLConnection connection = convertAndSend(request, options);
      return convertResponse(connection).toBuilder().request(request).build();
    }

    HttpURLConnection convertAndSend(Request request, Options options) throws IOException {
      final HttpURLConnection
          connection =
          (HttpURLConnection) new URL(request.url()).openConnection();
      if (connection instanceof HttpsURLConnection) {
        HttpsURLConnection sslCon = (HttpsURLConnection) connection;
        if (sslContextFactory != null) {
          sslCon.setSSLSocketFactory(sslContextFactory);
        }
        if (hostnameVerifier != null) {
          sslCon.setHostnameVerifier(hostnameVerifier);
        }
      }
      connection.setConnectTimeout(options.connectTimeoutMillis());
      connection.setReadTimeout(options.readTimeoutMillis());
      connection.setAllowUserInteraction(false);
      connection.setInstanceFollowRedirects(true);
      connection.setRequestMethod(request.method());

      Collection<String> contentEncodingValues = request.headers().get(CONTENT_ENCODING);
      boolean
          gzipEncodedRequest =
          contentEncodingValues != null && contentEncodingValues.contains(ENCODING_GZIP);
      boolean
          deflateEncodedRequest =
          contentEncodingValues != null && contentEncodingValues.contains(ENCODING_DEFLATE);

      boolean hasAcceptHeader = false;
      Integer contentLength = null;
      for (String field : request.headers().keySet()) {
        if (field.equalsIgnoreCase("Accept")) {
          hasAcceptHeader = true;
        }
        for (String value : request.headers().get(field)) {
          if (field.equals(CONTENT_LENGTH)) {
            if (!gzipEncodedRequest && !deflateEncodedRequest) {
              contentLength = Integer.valueOf(value);
              connection.addRequestProperty(field, value);
            }
          } else {
            connection.addRequestProperty(field, value);
          }
        }
      }
      // Some servers choke on the default accept string.
      if (!hasAcceptHeader) {
        connection.addRequestProperty("Accept", "*/*");
      }

      if (request.body() != null) {
        if (contentLength != null) {
          connection.setFixedLengthStreamingMode(contentLength);
        } else {
          connection.setChunkedStreamingMode(8196);
        }
        connection.setDoOutput(true);
        OutputStream out = connection.getOutputStream();
        if (gzipEncodedRequest) {
          out = new GZIPOutputStream(out);
        } else if (deflateEncodedRequest) {
          out = new DeflaterOutputStream(out);
        }
        try {
          out.write(request.body());
        } finally {
          try {
            out.close();
          } catch (IOException suppressed) { // NOPMD
          }
        }
      }
      return connection;
    }

    Response convertResponse(HttpURLConnection connection) throws IOException {
      int status = connection.getResponseCode();
      String reason = connection.getResponseMessage();

      if (status < 0) {
        throw new IOException(format("Invalid status(%s) executing %s %s", status,
            connection.getRequestMethod(), connection.getURL()));
      }

      Map<String, Collection<String>> headers = new LinkedHashMap<String, Collection<String>>();
      for (Map.Entry<String, List<String>> field : connection.getHeaderFields().entrySet()) {
        // response message
        if (field.getKey() != null) {
          headers.put(field.getKey(), field.getValue());
        }
      }

      Integer length = connection.getContentLength();
      if (length == -1) {
        length = null;
      }
      InputStream stream;
      if (status >= 400) {
        stream = connection.getErrorStream();
      } else {
        stream = connection.getInputStream();
      }
      return Response.builder()
              .status(status)
              .reason(reason)
              .headers(headers)
              .body(stream, length)
              .build();
    }
  }

How about Feign's performance?

Feign's overall framework is very compact, and there is basically no time consumption in the process of processing request transformation and message parsing. The real impact on performance is the process of handling Http requests.
As mentioned above, Feign adopts the HttpURLConnection of JDK by default, so the overall performance is not high. Students who have just come into contact with Spring Cloud may have a great prejudice against Spring Cloud if they do not pay attention to these details.
OkHttp3 is used internally in our project as the connection client.
 

--------
Copyright notice: This article is the original article of CSDN blogger "Yishan", which follows the CC 4.0 BY-SA copyright agreement. For reprint, please attach the source link of the original text and this notice.
Original link: https://blog.csdn.net/luanlouis/article/details/82821294

Topics: Java Spring Cloud Microservices