Spring Could Feign design principles

What is Feign?

Feign English table means "pretending, camouflage, deformation," is a lightweight framework http request call, you can call Http request in a manner annotated Java interfaces, without packet by encapsulating the way like Java HTTP requests directly call . Feign annotation process by the request of the template, when the actual call, passing parameters, then the parameters according to the application request, and then converted into a real request, this request is relatively intuitive.
Feign is widely used in Spring Cloud solutions, learning micro-service architecture based on Spring Cloud indispensable components.
Open Source Project Address:
https://github.com/OpenFeign/feign

Feign solve the problem?

Http encapsulates the call flow, more suitable for the interface of become a habit
in the scene service call, we often call Http protocol-based services, and we often use to frame may have HttpURLConnection, Apache HttpComponnets, OkHttp3, Netty, etc. these frameworks provide their own characteristics in the focus point is based. From the point of view of roles, their function is to provide consistent Http call service. Specific process is as follows:


image.png

Feign is how to design?


image.png

PHASE 1. Generates a class-based interface for dynamic proxy mode

Feign in use, defines a corresponding interface class, the class interface used in Http associated annotation, information identifying HTTP request parameters, as follows:

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 + ")");
    }
  }
}

In Feign bottom, by generating a class that implements the interface for dynamic proxy-based way, delegating the call request to implement dynamic proxy class, the basic principle is as follows:



image.png
 public class ReflectiveFeign extends Feign{
  ///省略部分代码
  @Override
  public <T> T newInstance(Target<T> target) {
    //根据接口类和Contract协议解析方式,解析接口类上的方法和注解,转换成内部的MethodHandler处理方式
    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);
    // 基于Proxy.newProxyInstance 为接口类创建动态实现,将所有的请求转换给InvocationHandler 处理。
    T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[]{target.type()}, handler);

    for(DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
      defaultMethodHandler.bindTo(proxy);
    }
    return proxy;
  }
  //省略部分代码

PHASE 2. The Contract protocol rules, the information of the interface class annotated parse parse the internal representation:


image.png

Feign defines a protocol conversion, defined 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.
   * 传入接口定义,解析成相应的方法内部元数据表示
   * @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 achieve Contract

Feign have its own default protocol specification, a predetermined number of notes, can be mapped into a corresponding Http request, as an example of official:

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, attempts to call GitHub.getContributors ( "foo", "myrepo") of the time will be converted into the following HTTP request:

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

Feign default protocol specification

annotation Interface Target Instructions for use
@RequestLine Methodologically HttpMethod definitions and UriTemplate. UriTemplate used {}wrapped expression, may be used by automatic injector @Param method parameters on
@Param Method parameters Defined template variables, the value of the variable template way can be used to resolve names using a template injection
@Headers On the class or method Definition header template variables, use @Param annotation provides injection parameter values. If the annotation is added to the interface classes, all requests will Header carries information corresponding to; if on the method, it will only add to the request on a corresponding method
@QueryMap Methodologically Define a key-value pair or pojo, the parameter value will be converted to the query string in the URL
@HeaderMap Methodologically Define a HeaderMap, and with UrlTemplate HeaderTemplate type, annotations can be used to provide parameter values ​​@Param

How specific FeignContract is resolved, within the scope of this article describes, please reference code:
https://github.com/OpenFeign/feign/blob/master/core/src/main/java/feign/Contract.java

Specification SpringMvcContract Spring MVC-based protocols:

Micro current Spring Cloud service solutions in order to reduce learning costs, using some of the Spring MVC annotation to complete the request protocol analysis, that is, write a client request interface and like to write server-side code as: client and server can be agreed by the SDK way, clients only need to introduce the service end of the release of SDK API, you can use encoding interface for docking services:



image.png

Our internal team along those lines, binding properties Spring Boot Starter, the definition of a server-side starter,
service consumers when in use, only need to introduce Starter, you can call the service. This is more suitable platform-independent interface abstracts the benefits is that you can switch according to its own service calls implementations:

  1. It can call based on a simple Http service;
  2. Spring Cloud services can be based on micro-architecture calls;
  3. Based on Dubbo SOA service governance

This mode is more suitable for its own switching mode SaSS hybrid software services, choose the right way to deploy based on the customer's hardware capabilities, the service can also be based on their micro-cluster deployment services

As for how to achieve Spring Cloud protocol analysis, refer to the 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 annotation is not fully used, there are some notes do not support, such as @GetMapping, @PutMappingetc., only supports the use of @RequestMappingother additional notes also inherited some problems; specifically limited to the details, each version can be some out, reference may be made to implement the code, it is relatively simple.

Spring Cloud Spring MVC is not based on all the notes do Feign Notes client protocol analysis, personally I think that this is not a small pit. When just starting Spring Cloud, you encounter this problem. Later, the only solution is deep into the code of this .... Someone should write a class to handle enhancement, for the time being is not the table, first MARK bit, open source is a good opportunity to practice hand.

PHASE 3. Based RequestBean, dynamically generated Request

According to incoming Bean object and annotation information, extracted from the corresponding value, to construct Http Request objects:



image.png
PHASE 4. Use Encoder Bean converted to Http text message (message parsing and transcoding logic)

Feign will eventually Http request into a message sent, the request object passed into the message body will eventually be resolved as follows:



image.png

On the interface definition Feign do relatively simple, abstracted 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.
   *  将实体对象转换成Http请求的消息正文中
   * @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}.
   *  从Response 中提取Http消息正文,通过接口类声明的返回类型,消息自动装配
   * @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 implementation:

Encoder / Decoder achieve Explanation
JacksonEncoder, JacksonDecoder Jackson-based persistence format conversion agreement
GsonEncoder,GsonDecoder Based on the persistence format conversion agreement Google GSON
SaxEncoder,SaxDecoder Sax library XML-based format persistent protocol conversion
JAXBEncoder,JAXBDecoder JAXB library XML-based format persistent protocol conversion
ResponseEntityEncoder,ResponseEntityDecoder Spring MVC return transfer protocol format based ResponseEntity <T>
SpringEncoder,SpringDecoder Protocol conversion based Spring MVC HttpMessageConverters a mechanism to achieve, apply to Spring Cloud system
PHASE 5. interceptor is responsible for the request and returns the decorating process

In the process of the conversion request, the interceptor Feign abstract interface for the operation of the user-defined request:

public interface RequestInterceptor {

  /**
   * 可以在构造RequestTemplate 请求时,增加或者修改Header, Method, Body 等信息
   * Called for every request. Add data using methods on the supplied {@link RequestTemplate}.
   */
  void apply(RequestTemplate template);
}

For example, if desired the compressed Http message transmission process, a request interceptor can be defined:

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) {
        //  在Header 头部添加相应的数据信息
        addHeader(template, HttpEncoding.ACCEPT_ENCODING_HEADER, HttpEncoding.GZIP_ENCODING,
                HttpEncoding.DEFLATE_ENCODING);
    }
}
PHASE 6. Logging

And when the transmission request is received, the log defines a uniform Feign facade output log information, and outputs the log defines four levels:

level Explanation
NONE Without any record
BASIC Http output only recording method name, the URL requested, and return a status code execution time
HEADERS Http output recording method name, the URL requested, and return a status code and execution time information Header
FULL Recording the Request and Response Header, Body, and some request metadata
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. Transmits an HTTP request based on the retry

Feign built a retry, a HTTP request occurs when IO exception, there will be a maximum number of attempts Feign transmission request, the core is Feign
code logic:

final class SynchronousMethodHandler implements MethodHandler {

  // 省略部分代码

  @Override
  public Object invoke(Object[] argv) throws Throwable {
   //根据输入参数,构造Http 请求。
    RequestTemplate template = buildTemplateFromArgs.create(argv);
    // 克隆出一份重试器
    Retryer retryer = this.retryer.clone();
    // 尝试最大次数,如果中间有结果,直接返回
    while (true) {
      try {
        return executeAndDecode(template);
      } catch (RetryableException e) {
        retryer.continueOrPropagate(e);
        if (logLevel != Logger.Level.NONE) {
          logger.logRetry(metadata.configKey(), logLevel);
        }
        continue;
      }
    }
  }

Retry control unit has the following parameters:

Retry parameters Explanation Defaults
period The initial retry time interval, when the request fails, the device will pause the initial retry time interval (the thread sleep mode) after the start, avoiding strong brush request, waste performance 100ms
maxPeriod When the request fails continuously, the retry interval will follow: long interval = (long) (period * Math.pow(1.5, attempt - 1));calculated in accordance with proportional manner to extend, but the maximum interval is maxPeriod, this set value can avoid performing the retry period is too long too many times 1000ms
maxAttempts The maximum number of retries 5

Reference specific code can be achieved:
https://github.com/OpenFeign/feign/blob/master/core/src/main/java/feign/Retryer.java

PHASE 8. Http request transmission

Feign really send HTTP requests are delegated to feign.Clientdo is:

public interface Client {

  /**
   * Executes a request against its {@link Request#url() url} and returns a response.
   *  执行Http请求,并返回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 default by the underlying JDK java.net.HttpURLConnectionimplements feign.Clientinterface classes, each time sending a request, will create a new link HttpURLConnection, which is why poor performance Feign of default. The interface can be expanded by using a connection pool based on high-performance Http Apache HttpClient client or OkHttp3 other internal use of our project is to OkHttp3 as Http client.

Feign following is the default implementation 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();
    }
  }

Feign how about performance?

Feign 整体框架非常小巧,在处理请求转换和消息解析的过程中,基本上没什么时间消耗。真正影响性能的,是处理Http请求的环节。
如上所述,由于默认情况下,Feign采用的是JDK的HttpURLConnection,所以整体性能并不高,刚开始接触Spring Cloud 的同学,如果没注意这些细节,可能会对Spring Cloud 有很大的偏见。
我们项目内部使用的是OkHttp3 作为连接客户端。



作者:亦山札记
链接:https://www.jianshu.com/p/8c7b92b4396c
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

Guess you like

Origin www.cnblogs.com/doit8791/p/11433042.html