头图

Spring Cloud openFeign learning [version 3.0.2]

Preface

​ The content is divided into the general use of openFeign and the personal interpretation of the source code. It refers to the content of a lot of other excellent bloggers. In many places, it is basically a bit of wisdom, but I still read the source code to deepen my understanding.

What is openFeign?

​ Feign is a declarative web service client . It makes it easier to write a web service client. To use Feign, you need to create an interface and annotate it. It has pluggable annotation support, including Feign annotations and JAX-RS annotations.

​ Feign also supports pluggable encoder and decoder . Spring Cloud adds support for Spring MVC annotations and supports the use of the same HttpMessageConverters used by default in Spring Web.

​ Spring Cloud integrates Eureka, Spring Cloud CircuitBreaker and Spring Cloud LoadBalancer, and provides a load balancing http client .

How to learn?

​ The biggest significance of the framework is to use it. In fact, the best tutorial is to learn by referring to the official documentation while doing it.

official document directory address

official openFeign document

Application scenario?

​ It can be seen that openFeign, as a service call transfer, is responsible for the connection between services and the operation of request forwarding. OpenFeign, as a component for writing service invocation support, occupies an extremely important position in spring cloud.

​ Unlike the communication framework of RPC, openFeign uses the traditional http as the transmission structure.

​ In the past when Ribbon was used, service invocation usually used manual invocation, which required a lot of manual coordination time. Now "localize" the service call through openFeign. Calling the interface API of other services is the same as calling a local method. In this way, the interface does not need to be changed frequently, and the invocation of the service can be controlled without causing the service provider to change and "fail".

The difference between Ribbon, Feign and OpenFeign

The difference between Ribbon, Feign and OpenFeign

Ribbon

​ Ribbon is Netflix open source based on HTTP and TCP and other protocol load balancing components

​ Ribbon can be used to do client load balancing , call the service of the registry

​ The use of Ribbon requires code to manually call the target service , please refer to the official example: official example

Feign

lightweight RESTful HTTP service client in the Spring Cloud component.

​ Feign built-in Ribbon, used to do client load balancing , to call service registry service .

​ The way to uses Feign's annotation define an interface, call this interface, you can call the service of the service registry.

​ For the annotations and usage supported by Feign, please refer to the official document: official document .

Feign itself does not support Spring MVC annotations, it will have its own notes .

OpenFeign

​ OpenFeign is that Spring Cloud supports Spring MVC annotations on the basis of Feign, such as @RequesMapping and so on. OpenFeign's @FeignClient can parse SpringMVC's @RequestMapping annotated interface, and generate implementation classes through dynamic proxy, implement load balancing in the class and call other services.

​ According to the above description, draw the following table content:

-RibbonFeignOpenFeign
How to useManually call the target serviceFeign's annotation defines the interface, and the registry service can be called by calling the interfaceYou can directly use the service call method to call the corresponding service
effectClient load balancing, service call of the service registryClient load balancing, service call of the service registryThe dynamic proxy method generates the implementation class, which performs load balancing in the implementation class and calls other services
DeveloperNetfixSpring CloudSpring Cloud
Features based on HTTP and TCP and other protocol load balancing components lightweight RESTful HTTP service client. Rely on self-fulfilling annotations for request processing lightweight RESTful HTTP service client supporting Spring MVC annotations
current situationIn maintenanceStop maintenanceIn maintenance

openFeign adds those functions:

  1. Pluggable annotation support, including Feign annotations and JSX-RS annotations.
  2. Support pluggable HTTP encoder and decoder.
  3. Support Hystrix and its Fallback.
  4. Support Ribbon load balancing.
  5. Supports compression of HTTP requests and responses.

The client implementation of openFeign replaces:

  1. It can be replaced by http client, and openFeign provides a good configuration, which can support the detailed configuration of httpclient.
  2. Using okHttpClient, you can implement okhttpClient to implement a custom httpclient injection mode, but certain problems will occur.

How to use:

1. Add dependencies

​ According to maven's dependency management, we need to use this method for processing

 <dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-openfeign</artifactId>
     <version>${feign.version}</version>
     <scope>compile</scope>
     <optional>true</optional>
</dependency>

2. Open the annotation @EnableFeignClients

application start classes need to add a corresponding configuration: @EnableFeignClients to allow access.

The default configuration of spring cloud feign:

Spring Cloud OpenFeign provides the following beans for camouflage by default ( BeanType beanName:) ClassName :

  • Decoder feignDecoder: ( ResponseEntityDecoder contains SpringDecoder )
  • Encoder feignEncoder: SpringEncoder
  • Logger feignLogger: Slf4jLogger
  • MicrometerCapability micrometerCapability: if feign-micrometer on the classpath and MeterRegistry available
  • Contract feignContract: SpringMvcContract
  • Feign.Builder feignBuilder: FeignCircuitBreaker.Builder
  • Client feignClient: If the class path FeignBlockingLoadBalancerClient use Spring Cloud on LoadBalancer , is used. If they are not on the classpath, the default masquerading client is used.

3. yml adds configuration:

​ The contents of the file inside the yml file are as follows:

feign:
    client:
        config:
            feignName:
                connectTimeout: 5000
                readTimeout: 5000
                loggerLevel: full
                errorDecoder: com.example.SimpleErrorDecoder
                retryer: com.example.SimpleRetryer
                defaultQueryParameters:
                    query: queryValue
                defaultRequestHeaders:
                    header: headerValue
                requestInterceptors:
                    - com.example.FooRequestInterceptor
                    - com.example.BarRequestInterceptor
                decode404: false
                encoder: com.example.SimpleEncoder
                decoder: com.example.SimpleDecoder
                contract: com.example.SimpleContract
                capabilities:
                    - com.example.FooCapability
                    - com.example.BarCapability
                metrics.enabled: false

4. Specific use:

​ For more usage, please refer to online materials or official documents. Here are some specific configurations or usage methods:

If the openFeign name conflicts, you need to use contextId to prevent bean name conflicts

@FeignClient(contextId = "fooClient", name = "stores", configuration = FooConfiguration.class)

Context inheritance

​ If you configure FeignClient to not inherit beans from the parent context, you can use the following writing:

@Configuration
public class CustomConfiguration{

    @Bean
    public FeignClientConfigurer feignClientConfigurer() {
        return new FeignClientConfigurer() {
            @Override
            public boolean inheritParentConfiguration() {
                return false;
            }
        };
    }
}
Note: By default, feign will not encode and slashes. If you want to encode slashes, you need to use the following method:

feign.client.decodeSlash:false

Log output

​ The default log output level of feign is as follows:

logging.level.project.user.UserClient: DEBUG

​ The following is the content of the log print:

  • NONE : No log is recorded by default (default setting)
  • BASIC : Only log information related to request and response time is recorded
  • HEADERS : Record basic information and request and response header
  • FULL : Record the header, body and metadata of the request and response. (All information records)

Turn on compression

​ You can start http compression through the following configuration:

feign.compression.request.enabled=true
feign.compression.response.enabled=true

​ If you need further configuration, you can use the following form to configure:

feign.compression.request.enabled=true
feign.compression.request.mime-types=text/xml,application/xml,application/json
feign.compression.request.min-request-size=2048

​ Note that 2048 is the minimum threshold for compression requests, because if gzip compression is used for all requests, the performance overhead for small files will be greater instead

​ Use the following configuration to enable gzip compression (compression encoding is UTF-8, default):

feign.compression.response.enabled=true
feign.compression.response.useGzipDecoder=true

5. Appendix:

yml related configuration table:

​ This part of the configuration can be directly referred to the official website for processing: yml related configuration table

Source code interpretation of openFeign

​ The following is a summary of understanding the source code with the help of the article. The entire calling process is relatively easy to understand. Because to put it plainly, it is just an abstraction and encapsulation of an http request. However, this part uses a lot of design patterns, such as the builder pattern and strategy pattern that can be seen everywhere. At the same time, this piece of design uses a large number of package access structure closures, so it will be a little troublesome to perform secondary development on it, but the use of reflection barriers is basically useless.

​ References: Nuggets [[Graphic] Spring Cloud OpenFeign source code analysis]: https://juejin.cn/post/6844904066229927950#heading-24

feign work flow chart

Work flow overview

This article mainly introduces the process of an openFeign request call. The part of annotation processing and component registration is put at the end of the article.

    • Feign instantiation newInstance()

      + 实例化**SyncronizedMethodHandler**以及**ParseHandlersByName**,注入到**ReflectFeign**对象。
      
    • Construct ParseHandlersByName object and transform the parameters
    • Construct Contract object, check and parse the request parameters

      • Instantiate the SpringMvcContract object (inherited from the Contract object)
      • Call parseAndValidateMetadata() process and verify the data type
    • Create a dynamic proxy object MethodInvocationHandler through the jdk dynamic proxy Proxy, and call the invoke() method of the dynamic proxy object
    • Proxy class SyncronizedInvocationHandler constructs requestTeamplate object and sends the request

      • Call create() construct the request entity object
      • encode() operation for request parameters
      • Construct client object and execute the request
      • Return request result
    • Get the result of the request, the request is complete

    Explain the openFeign workflow in detail (focus)

    1. Feign instantiation-newInstance()

    ​ When a service calls another service through feign, in the Fegin.builder object, the constructor will be called to construct a Fegin instance. The following is the code content feign.Feign.Builder#build

    public Feign build() {
          // 构建核心组件和相关内容
          Client client = Capability.enrich(this.client, capabilities);
          Retryer retryer = Capability.enrich(this.retryer, capabilities);
          List<RequestInterceptor> requestInterceptors = this.requestInterceptors.stream()
              .map(ri -> Capability.enrich(ri, capabilities))
              .collect(Collectors.toList());
          Logger logger = Capability.enrich(this.logger, capabilities);
          Contract contract = Capability.enrich(this.contract, capabilities);
          Options options = Capability.enrich(this.options, capabilities);
          Encoder encoder = Capability.enrich(this.encoder, capabilities);
          Decoder decoder = Capability.enrich(this.decoder, capabilities);
          InvocationHandlerFactory invocationHandlerFactory =
              Capability.enrich(this.invocationHandlerFactory, capabilities);
          QueryMapEncoder queryMapEncoder = Capability.enrich(this.queryMapEncoder, capabilities);
        
          // 初始化SynchronousMethodHandler.Factory工厂,后续使用该工厂生成代理对象的方法
          SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
              new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
                  logLevel, decode404, closeAfterDecode, propagationPolicy, forceDecoding);
          // 请求参数解析对象以及参数处理对象。负责根据请求类型构建对应的请求参数处理器
          ParseHandlersByName handlersByName =
              new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,
                  errorDecoder, synchronousMethodHandlerFactory);
        // 这里的 ReflectiveFeign 是整个核心的部分
          return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
        }
      }

    ​ After executing the ReflectiveFeign construction, the ReflectiveFeign#newInstance() method of the Fegin subclass will be executed immediately.

    public <T> T target(Target<T> target) {
          return build().newInstance(target);
        }
    The design here is more clever. But it's not particularly difficult to understand
     下面是`ReflectiveFeign#newInstance`方法的代码:
    
     public <T> T newInstance(Target<T> target) {
         // ParseHandlersByName::apply 方法构建请求参数解析模板和验证handler是否有效
        Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
        Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
        List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
        // 对于方法handler进行处理
        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)));
          }
        }
         // 创建接口代理对象。factory在父类build方法进行初始化
        InvocationHandler handler = factory.create(target, methodToHandler);
        T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
            new Class<?>[] {target.type()}, handler);
        // 绑定代理对象
        for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
          defaultMethodHandler.bindTo(proxy);
        }
        return proxy;
      }

    ​ The following is an in-depth analysis of the above code.

    2. ParseHandlersByName parameter analysis processing-apply()

    ReflectiveFeign#newInstance() is performed among the first feign.ReflectiveFeign.ParseHandlersByName object aplly() methods, parameters and parameter parsing parses builder construct. At the same time, it can be noticed that if it is found that the method handler does not find the corresponding configuration in feign, it will throw a IllegalStateException exception.

    public Map<String, MethodHandler> apply(Target target) {
            // 2.1 小节进行讲解
          List<MethodMetadata> metadata = contract.parseAndValidateMetadata(target.type());
          Map<String, MethodHandler> result = new LinkedHashMap<String, MethodHandler>();
          for (MethodMetadata md : metadata) {
            BuildTemplateByResolvingArgs buildTemplate;
              // 根据请求参数的类型,实例化不同的请求参数构建器
            if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) {
              // form表单提交形式
                buildTemplate =
                  new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);
            } else if (md.bodyIndex() != null) {
                // 普通编码形式处理
              buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);
            } else {
              buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder, target);
            }
            if (md.isIgnored()) {
              result.put(md.configKey(), args -> {
                throw new IllegalStateException(md.configKey() + " is not a method handled by feign");
              });
            } else {
              result.put(md.configKey(),
                  factory.create(target, md, buildTemplate, options, decoder, errorDecoder));
            }
          }
          return result;
        }
      }
    2.1 Contract method parameter annotation analysis and verification-parseAndValidateMetadata()

    ​ The role of this method is: in the class linked to the HTTP request.

    ​ The default instantiation object is: <font color='red'> SpringMvcContract </font>

     由于这部分涉及子父类的调用以及多个内部方法的调用并且方法内容较多,下面先介绍下**父类**的`parseAndValidateMetadata()`大致的代码工作流程。
    
    1. Check whether the handler is single inheritance (single implementation interface), and does not support the parameterized type . Otherwise it will throw an exception
    2. Traverse all internal methods

      1. If it is a static method, skip the current loop
      2. Get method object and target class, execute internal method parseAndValidateMetadata()
      The internal method is to deal with the annotation method and parameter content. If you are interested, you can understand the source code by yourself
    3. Check whether it is an overriding method, if it is, throw an exception Overrides unsupported

    According to the above introduction, let's take a look at the specific logic code:

    public List<MethodMetadata> parseAndValidateMetadata(Class<?> targetType) {
          checkState(targetType.getTypeParameters().length == 0, "Parameterized types unsupported: %s",
              targetType.getSimpleName());
          checkState(targetType.getInterfaces().length <= 1, "Only single inheritance supported: %s",
              targetType.getSimpleName());
          if (targetType.getInterfaces().length == 1) {
            checkState(targetType.getInterfaces()[0].getInterfaces().length == 0,
                "Only single-level inheritance supported: %s",
                targetType.getSimpleName());
          }
          final Map<String, MethodMetadata> result = new LinkedHashMap<String, MethodMetadata>();
          for (final Method method : targetType.getMethods()) {
            if (method.getDeclaringClass() == Object.class ||
                (method.getModifiers() & Modifier.STATIC) != 0 ||
                Util.isDefault(method)) {
              continue;
            }
              // 调用内部方法, 处理注解方法和参数信息
            final MethodMetadata metadata = parseAndValidateMetadata(targetType, method);
            checkState(!result.containsKey(metadata.configKey()), "Overrides unsupported: %s",
                metadata.configKey());
            result.put(metadata.configKey(), metadata);
          }
          return new ArrayList<>(result.values());
        }
    2.2 SpringMvcContract method parameter annotation analysis and verification

    ​ Since most of the detailed processing work is done by the parent class:

        public MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
            processedMethods.put(Feign.configKey(targetType, method), method);
            // 使用父类方法获取 MethodMetadata
            MethodMetadata md = super.parseAndValidateMetadata(targetType, method);
    
            RequestMapping classAnnotation = findMergedAnnotation(targetType,
                    RequestMapping.class);
            if (classAnnotation != null) {
                // produces - use from class annotation only if method has not specified this
                // produces - 只有当方法未指定时才从类注释产生
                if (!md.template().headers().containsKey(ACCEPT)) {
                    parseProduces(md, method, classAnnotation);
                }
    
                // consumes -- use from class annotation only if method has not specified this
                // consumes - 只有当method没有指定时才使用from类注释
                if (!md.template().headers().containsKey(CONTENT_TYPE)) {
                    parseConsumes(md, method, classAnnotation);
                }
    
                // headers -- class annotation is inherited to methods, always write these if
                // present
                // headers -- 类注解被继承到方法,如果有的话,一定要写下来
                parseHeaders(md, method, classAnnotation);
            }
            return md;
        }

    3. Create interface dynamic proxy

    ​ Let's understand how feign completes the creation of the proxy object of the interface based on the structure diagram of a dynamic proxy.

    ​ First of all, the target is the method of the target service we want to call. After the annotation processing of the contract, it will be handed over to the proxy object to create the proxy object:

    InvocationHandler handler = factory.create(target, methodToHandler);
        T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
            new Class<?>[] {target.type()}, handler);

    ​ The first line of code here uses the factory to build an InvocationHandler , and then uses proxy.newInstance to build an interface proxy object according to the type of the proxy target method object.

    ​ And the construction operation of invocationHandler InvocationHandlerFactory factory, and the construction details of the factory are completed ReflectiveFeign.FeignInvocationHandler Finally, it returns to FeignInvocationHandler to complete the follow-up operation of the dynamic proxy.

    static final class Default implements InvocationHandlerFactory {
    
      @Override
      public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) {
        return new ReflectiveFeign.FeignInvocationHandler(target, dispatch);
      }
    }

    ​ After the interface proxy object is created, the invoke() method of FeignInvocationHandler will be executed,

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      if ("equals".equals(method.getName())) {
        try {
          Object otherHandler =
              args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
          return equals(otherHandler);
        } catch (IllegalArgumentException e) {
          return false;
        }
      } else if ("hashCode".equals(method.getName())) {
        return hashCode();
      } else if ("toString".equals(method.getName())) {
        return toString();
      }
     // 通过dispatch 获取所有方法的handler的引用,执行具体的handler方法
      return dispatch.get(method).invoke(args);
    }
    A data structure is involved here:

    Map<Method, MethodHandler> methodToHandler , which is also the core part of the dynamic agent

    MehtodHandler is a LinkedHashMap data structure, it stores all the methods corresponding to the mapping of interface proxy objects.

    This attribute was created new ReflectiveFeign.FeignInvocationHandler(target, dispatch);

    3.1 The interface proxy object calls feign.SynchronousMethodHandler#invoke() request logic

    ​ At this step, it is the part where the proxy object executes the specific request logic. This part includes the creation of a request template, parameter analysis, configuration of the client according to the parameters, request encoding and request decoding, as well as interceptors and so on... More content. This section serves as a dividing line for the three parts 1-3.

    4. Detailed Explanation of SynchronousMethodHandler Dynamic Proxy Object Processing

    ​ First, let's look at the invoke() processing code logic of SynchronousMethodHandler

    ​ It is relatively easy to understand here. At the beginning, I occasionally saw a requestTemplate template, and at the same time constructing the related options of the request, copying a retryer configuration to the current thread. Then the core executeAndDecode() the request and returns the result. If a retry exception occurs during the entire request execution process, it will try to call the retryer for processing. If the retry still fails, an unchecked exception will be thrown or an unchecked exception will be thrown. Check the abnormality. Finally, the printing and processing of the log is judged according to the configuration of the log.

    public Object invoke(Object[] argv) throws Throwable {
        // 构建请求处理模板
        RequestTemplate template = buildTemplateFromArgs.create(argv);
        // 配置接口请求参数
        Options options = findOptions(argv);
        // 重试器创建
        Retryer retryer = this.retryer.clone();
        while (true) {
          try {
              // 执行请求
            return executeAndDecode(template, options);
          } catch (RetryableException e) {
            try {
                // 尝试重试和处理
              retryer.continueOrPropagate(e);
            } catch (RetryableException th) {
                // 受检异常处理
              Throwable cause = th.getCause();
              if (propagationPolicy == UNWRAP && cause != null) {
                throw cause;
              } else {
                throw th;
              }
            }
              // 日志打印和处理
            if (logLevel != Logger.Level.NONE) {
              logger.logRetry(metadata.configKey(), logLevel);
            }
            continue;
          }
        }
      }

    The following are some of the notes made temporarily while reading the source code, just browse it roughly.

    1. Distribute to different request implementation processors through methodHandlerMap
    2. By default, SynchronousMethodHandler handle different requests

      • Build requestTemplate template
      • Build requestOptions configuration
      • Get the Retry
    3. Use while(true) for wireless loop. Execute the request and decode processing template and request parameters

      • Call the interceptor to intercept the request (using the chain of responsibility model)

        • BasicAuthRequestInterceptor : The default call permission verification interception
        • FeignAcceptGzipEncodingInterceptor gzip encoding processing switch connector. Used to determine whether to allow gzip compression
        • FeignContentGzipEncodingInterceptor : request message content gzip compression interception processor
      If the configuration level of the log is not none, output the corresponding log level
    4. Execute client.execute() method and send http request

      • Use response.toBuilder treated built up the response content (note that the source code version will follow label inside abandoned this way? Why waste? where bad )
    5. For decoding the returned result, call AsyncResponseHandler.handlerResponse to process the result

      • There are many judgment logics here, and the order of judgment is as follows:

        • If the return type is Response.class
        • If the Body is null, execute the complete call

    CompletableFuture asynchronous call processing execution result is used here. Ensure that the entire process is executed asynchronously and returned

    • CompletableFuture.complete()、
    • CompletableFuture.completeExceptionally can only be called once. Note that.
     如果长度为空或者长度超过 **缓存结果最大长度。**需要设置` shouldClose`为**false**,并且同样执行complete调用
    
    • If the return status is greater than 200 and less than 300

      • If the return type is void, call complete
      • Otherwise, decode the returned result, whether it needs to be closed is determined according to the status of the result after decoding (didn't understand)
      • If it is 404 and the return value is not void, the error handling method
      • If none of the above is satisfied, the error result is encapsulated according to the error message of the returned result, and an error object is constructed according to the error result. Finally passed: resultFuture.completeExceptionally for processing
    Special handling: If all the above judgments have abnormal information, in addition to the io exception that requires secondary encapsulation, the default comoleteExceptionally method will be triggered to throw a call to terminate the asynchronous thread.

    ​ + Verify that the task is completed. If the task is not completed, calling the resultFuture.join() method will throw an unchecked exception in the current thread.

    1. If an exception is thrown, use retry to retry regularly

    4.1 Build RequestTemplate template

    ​ The role is to use the parameters passed to the method call to create a request template. The main content is various url processing of the request, including parameter processing, url parameter processing, expansion of iterative parameters, and so on. There are many details in this part. Due to limited space, here is the key point: RequestTemplate template = resolve(argv, mutable, varBuilder); This method will process the parameters according to the pre-defined parameter processor. The specific code is as follows:

    RequestTemplate resolve(Object[] argv,
                                          RequestTemplate mutable,
                                          Map<String, Object> variables) {
          return mutable.resolve(variables);
        }

    ​ The internal call is the resolve mutable object, so how does it handle the request?

    Request templates for processing according to different parameters:

    ​ feign provides diversified parameter request processing through different parameter request templates. Let's take a look at the specific structure diagram first:

    ​ It is obvious that the strategy mode is used here. The code first finds the specific parameter request processing object according to the parameters to customize the processing of the parameters. After the processing is completed, calls super.resolve() for unified processing of other content (template method) . The design is very good and clever, the following is the corresponding method signature:

    `feign.RequestTemplate#resolve(java.util.Map<java.lang.String,?>)`

    There may be questions here, where is BuildTemplateByResolvingArgs

    BuildTemplateByResolvingArgs buildTemplate;
    // 根据请求参数的类型,实例化不同的请求参数构建器
    if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) {
        // form表单提交形式
        buildTemplate =
            new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);
    } else if (md.bodyIndex() != null) {
        // 普通编码形式处理
        buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);
    } else {
        // 使用默认的处理模板
        buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder, target);
    }

    Answer: In fact, as early as the second step ParseHandlersByName , the entire request processing template is confirmed, and the proxy object will also use this processing template to ensure the idempotence of the request.

    Comparison of request parameter processing details:

    ​ If it is the form submitted parameters:

    Map<String, Object> formVariables = new LinkedHashMap<String, Object>();
          for (Entry<String, Object> entry : variables.entrySet()) {
            if (metadata.formParams().contains(entry.getKey())) {
              formVariables.put(entry.getKey(), entry.getValue());
            }
          }

    ​ If the form format, the map will generally be converted to formVariables format, pay attention to the internal use of linkedhashmap for processing

    If it is Body processing method:

    Object body = argv[metadata.bodyIndex()];
          checkArgument(body != null, "Body parameter %s was null", metadata.bodyIndex());

    note:

    1. Subsequent versions of this part may add more processing forms, all subject to the latest source code. Pay attention to the version of the article title statement
    2. For formatted woolen cloth
    Details about the encoding and decoding of message data:

    ​ The encryption work is completed in: requestTemplate , and is in 160b37e52ae201 BuildTemplateByResolvingArgs#resolve , according to different request parameter types for subtle encryption operation adjustments, but the code is basically similar.

    ​ The following is the default implementation 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()));
          }
        }
      }
    1. If it is a string type, call the tostring method of the object
    2. If it is a byte array, convert it to a byte array for storage
    3. If the object is empty, an encrypted encode exception is thrown

      After talking about encryption, it is natural to talk about how to deal with the decoding action. The following is the implementation of the default decoding interface <font color='gray'> (note that the parent class is StringDecoder instead of Decoder)</font>:

    public class Default extends StringDecoder {
    
        @Override
        public Object decode(Response response, Type type) throws IOException {
            // 这里的硬编码感觉挺突兀的,不知道是否为设计有失误还是单纯程序员偷懒。
            // 比较倾向于加入 if(response == null ) return null; 这一段代码
          if (response.status() == 404 || response.status() == 204)
            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);
        }
      }

    It's weird to actually use a hard-coded form here. (Foreigners are always very free to code) When the return status is 404 or 204. According to the data type of the object, the default value of the related data type is constructed. If it is an object, an empty object is returned

    • The 204 code represents a request for an empty file
    • 200 represents a successful response to the request

    StringDecoder string if the types are not consistent. If the string cannot be decoded, an exception message will be thrown. If you are interested, you can see StringDecoder#decode() , which will not be shown here.

    If an error occurs, how to encode the error message?
    public Exception decode(String methodKey, Response response) {
          FeignException exception = errorStatus(methodKey, response);
          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;
        }
    1. According to the error message and method signature, construct the exception object
    2. Use the retry code for the processing action of returning the request header, and retry the operation later after the opening fails
    3. If the retry fails later, the relevant exception is thrown
    4. Return exception information

    4.2 option configuration acquisition

    ​ The code is relatively simple, it is directly expanded here, if there is no call parameter, the default option is returned to accompany the child, otherwise the Options configuration is constructed according to the specified conditions

     Options findOptions(Object[] argv) {
        if (argv == null || argv.length == 0) {
          return this.options;
        }
        return Stream.of(argv)
            .filter(Options.class::isInstance)
            .map(Options.class::cast)
            .findFirst()
            .orElse(this.options);
      }

    4.3 Build a retryer

    ​ This part of the retryer will call a method called clone(). Note that this clone method has been overridden and uses the retryer implemented by default. In addition, I personally think that the name of this method is easy to cause misunderstandings. I personally prefer to build a constructor called new Default() .

     public Retryer clone() {
          return new Default(period, maxPeriod, maxAttempts);
        }

    ​ The more important method of the retryer is the retry operation after the exception, the following is the corresponding square code

     public void continueOrPropagate(RetryableException e) {
          if (attempt++ >= maxAttempts) {
            throw e;
          }
    
          long interval;
          if (e.retryAfter() != null) {
            interval = e.retryAfter().getTime() - currentTimeMillis();
            if (interval > maxPeriod) {
              interval = maxPeriod;
            }
            if (interval < 0) {
              return;
            }
          } else {
            interval = nextMaxInterval();
          }
          try {
            Thread.sleep(interval);
          } catch (InterruptedException ignored) {
            Thread.currentThread().interrupt();
            throw e;
          }
          sleptForMillis += interval;
        }

    Here retry interval in accordance multiples of 1.5 retry factor exceeds the maximum number of retries is set to stop the retry.

    4.4 Request sending and result processing

    ​ When the above basic configuration is performed immediately after the request is sent, there is another key operation before sending only the request: interceptor processing

    ​ Here we will traverse the pre-configured interceptors, will do the final processing operation for the request template.

    Request targetRequest(RequestTemplate template) {
      for (RequestInterceptor interceptor : requestInterceptors) {
        interceptor.apply(template);
      }
      return target.apply(template);
    }
    Regarding the control of log output level

    ​ In the part of the code that executes the request, there will be more similar codes.

    if (logLevel != Logger.Level.NONE) {
          logger.logRequest(metadata.configKey(), logLevel, request);
        }

    ​ The level of log output is based on the following:

    public enum Level {
        /**
         * No logging.
             不进行打印,也是默认配置
         */
        NONE,
        /**
         * Log only the request method and URL and the response status code and execution time.
             只记录请求方法和URL以及响应状态代码和执行时间。
         */
        BASIC,
        /**
         * Log the basic information along with request and response headers.
             记录基本信息以及请求和响应头。
         */
        HEADERS,
        /**
         * Log the headers, body, and metadata for both requests and responses.
             记录请求和响应的头、主体和元数据。
         */
        FULL
      }
    The client sends a request (emphasis)

    ​ Here is also intercepted feign.SynchronousMethodHandler#executeAndDecode , undoubtedly the most critical part is the client.execute(request, options) method. The following is the corresponding code content:

    Response response;
    long start = System.nanoTime();
    try {
      response = client.execute(request, options);
      // ensure the request is set. TODO: remove in Feign 12
      response = response.toBuilder()
          .request(request)
          .requestTemplate(template)
          .build();
    } catch (IOException e) {
      if (logLevel != Logger.Level.NONE) {
        logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
      }
      throw errorExecuting(request, e);
    }
    long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);

    ​ The following is the inheritance structure diagram client

    ​ According to the above structure diagram, briefly explain the default implementation of client

    1. The requestor strategy is implemented, and the top-level interface client . By default, the Default class is used as the implementation class. the URL request method java.net 160b37e52ae61a through the subclass proxied object. That is to say, even if there is no auxiliary third-party tool, it is possible to simulate the construction of http request through this method api.
    2. You can use okhttp and httpclient high-performance implementations instead, you need to introduce the corresponding feign access implementation.

    logic of Default code corresponding to

    • Construct request URL object HttpUrlConnection
    • If it is an Http request object, you can set ssl or domain name signature according to the conditions
    • Set http basic request parameters
    • Collect header information, set GZIP compression code
    • Set accept: */*
    • Check whether the internal buffer is turned on, if it is set, buffer according to the specified length

    ​ The core part of the code call is by java.net httpconnection . Using the original network IO stream for request processing, the efficiency is relatively low. Here is the corresponding specific implementation code:

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

    ​ After data conversion and request sending, the response content is encapsulated and processed according to the results as follows:

    // 请求结果处理
    Response convertResponse(HttpURLConnection connection, Request request) 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<>();
        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;
        // 对于状态码400以上的内容进行错误处理
        if (status >= 400) {
            stream = connection.getErrorStream();
        } else {
            stream = connection.getInputStream();
        }
        // 构建返回结果
        return Response.builder()
            .status(status)
            .reason(reason)
            .headers(headers)
            .request(request)
            .body(stream, length)
            .build();
    }

    Interlude: About the reason attribute (can be skipped)

    ​ When viewing the source code, I accidentally saw that there is a point that I am more concerned about. The following is a field called reason in respose:

    /**
     * Nullable and not set when using http/2
     * 作者如下说明 在http2中可以不设置改属性
     * See https://github.com/http2/http2-spec/issues/202
     */
    public String reason() {
      return reason;
    }

    ​ Seeing this paragraph, I was suddenly curious about does not need to set reason . Of course, there are similar questions on github.

    answered like this in 2013. The straightforward translation is: !

    However, the matter was not over, and someone asked questions in detail later

    原文
    i'm curious what was the logical reason for dropping the reason phrase?
    i was using the reason phrase as a title for messages presented to a user in the web browser client. i think most users are accustomed to such phrases, "Bad Request", "Not Found", etc. Now I will just have to write a mapping from status codes to my own reason phrases in the client.
    机翻:
    我很好奇,放弃"reason"这个词的逻辑原因是什么? 我使用“reason”作为在web浏览器客户端向用户呈现的消息的标题。我认为大多数用户习惯于这样的短语,“错误请求”,“未找到”等。现在我只需要在客户机中编写一个从状态代码到我自己的理由短语的映射。

    Then I guess I can’t stand all kinds of questions. The above mnot gave a clear answer five years later:

    原因短语——即使在HTTP/1.1中——也不能保证端到端携带;
    实现可以(也确实)忽略它并替换自己的值(例如,200总是“OK”,不管在网络上发生什么)。
    
    考虑到这一点,再加上携带额外字节的开销,将其从线路上删除是有意义的。

    In order to confirm his statement, the following description was found from the website of https://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html

    The Status-Code is intended for use by automata and the Reason-Phrase is intended for the human user. The client is not required to examine or display the Reason- Phrase.
    状态代码用于自动机,而原因短语用于人类用户。客户端不需要检查或显示原因-短语。

    This paragraph comes from the specification description of Http1.1.

    So sometimes a lot of stories can be unearthed from the source code, very interesting

    FeignBlockingLoadBalancerClient is used as load balancer:

    ​ This class is equivalent to the transfer class of openFeign and ribbon, which transfers openfeign requests to ribbon to achieve load balancing. There will be a question here: how does the client choose to use ribbon or spring cloud?

    ​ In fact, it is not difficult to understand when you think about it, load balancing must be done when the spring bean is initialized. FeignClientFactoryBean is the key to the entire implementation.

    class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware 

    ​ Below is the org.springframework.cloud.openfeign.FeignClientFactoryBean#getTarget method code

    @Override
    
      public Object getObject() **throws** Exception {
    
        return getTarget();
    
      }
    
    /**
    
       \* @param <T> the target type of the Feign client 客户端的目标类型
    
       \* @return a {@link Feign} client created with the specified data and the context 指定数据或者上下文
    
       \* information
    
       */
    
      <T> T getTarget() {
    
        FeignContext context = applicationContext.getBean(FeignContext.class);
    
        Feign.Builder builder = feign(context);
    
           // 如果URL为空,默认会尝试使用**
    
        if (!StringUtils.hasText(url)) {
    
          if (!name.startsWith("http")) {
    
            url = "http://" + name;
    
          }
    
          else {
    
            url = name;
    
          }
    
          url += cleanPath();
    
      // **默认使用ribbon作为负载均衡,如果没有找到,会抛出异常**
    
          return (T) loadBalance(builder, context,
    
              new HardCodedTarget<>(type, name, url));
    
        }
    
        if (StringUtils.hasText(url) && !url.startsWith("http")) {
    
          url = "http://" + url;
    
        }
    
        String url = this.url + cleanPath();
    
        Client client = getOptional(context, Client.class);
    
    // 根据当前的系统设置实例化不同的负载均衡器
    
        if (client != null) {
    
          if (client instanceof LoadBalancerFeignClient) {
    
            // not load balancing because we have a url,but ribbon is on the classpath, so unwrap
              // 不是负载平衡,因为我们有一个url,但是ribbon在类路径上,所以展开
            client = ((LoadBalancerFeignClient) client).getDelegate();
    
          }
    
          if (client instanceof FeignBlockingLoadBalancerClient) {
    
            // not load balancing because we have a url, but Spring Cloud LoadBalancer is on the classpath, so unwrap
              // 不是负载平衡,因为我们有一个url,但Spring Cloud LoadBalancer是在类路径上,所以展开
    
            client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
    
          }
    
          builder.client(client);
    
        }
    
        Targeter targeter = get(context, Targeter.class);
    
        return (T) targeter.target(this, builder, context,
    
            new HardCodedTarget<>(type, name, url));
    
      }

    ​ The above content describes the complete process of the initialization of a load balancer. It is also proved that spring cloud uses ribbon as the default initialization . If you are interested, you can search for this exception globally, which indirectly indicates that ribbon is used as load balancer by default:

    throw new IllegalStateException("No Feign Client for defined. Did you forget to include spring-cloud-starter-netflix-ribbon?");
    expand:

    ​ In feign.Client.Default#convertAndSend(), there is a code setting as follows

    connection.setChunkedStreamingMode(8196);

    ​ If ChunkedStreamMode is disabled in the code, what is the effect compared to the code with 4096?

    result of this is that the entire output are buffered until the off so Content-length header is first provided and may be transmitted, which increases the number and latency of memory. For large files, it is not recommended.

    Answer source: HttpUrlConnection.setChunkedStreamingMode effect

    About codec processing

    ​ For this part, please read section 4.1 about the details of message data encoding and decoding

    So far, a basic call flow is basically completed.

    OpenFeign overall call link diagram

    ​ First borrow (steal) a picture of reference materials to see the entire openFeign link call:

    ​ The following is a picture drawn by the individual based on the profile:

    openFeign annotation processing flow

    ​ Let's take a look at the comment on how to open openFeign: @EnableFeignClients

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Documented
    @Import(FeignClientsRegistrar.class)
    public @interface EnableFeignClients {}

    ​ Note a note here @Import(FeignClientsRegistrar.class) . There is no doubt that the implementation details are inside FeignClientsRegistrar.class :

    ​ Eliminate other logic and details, the key code is in this block:

    for (String basePackage : basePackages) {
             //….
    registerFeignClient(registry, annotationMetadata, attributes);
                //….
            }

    ​ Here we call registerFeignClient register feign, and scan the response basepakage according to the annotation configuration. If it is not configured, it will scan according to the path of the annotation class by default.

    ​ The following code injects relevant bean information based on the scan results, such as url, path, name, callback function, etc. Finally, use BeanDefinitionReaderUtils to inject the method and content of the bean.

    private void registerFeignClient(BeanDefinitionRegistry registry,
                AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
            String className = annotationMetadata.getClassName();
        //bean配置
        
            BeanDefinitionBuilder definition = BeanDefinitionBuilder
                    .genericBeanDefinition(FeignClientFactoryBean.class);
            validate(attributes);
            definition.addPropertyValue("url", getUrl(attributes));
            definition.addPropertyValue("path", getPath(attributes));
            String name = getName(attributes);
            definition.addPropertyValue("name", name);
            String contextId = getContextId(attributes);
            definition.addPropertyValue("contextId", contextId);
            definition.addPropertyValue("type", className);
            definition.addPropertyValue("decode404", attributes.get("decode404"));
            definition.addPropertyValue("fallback", attributes.get("fallback"));
            definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
            definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
     
            String alias = contextId + "FeignClient";
            AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
            beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
     
            // has a default, won't be null
            // 如果未配置会存在默认的配置
            boolean primary = (Boolean) attributes.get("primary");
     
            beanDefinition.setPrimary(primary);
     
            String qualifier = getQualifier(attributes);
            if (StringUtils.hasText(qualifier)) {
                alias = qualifier;
            }
     
            BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
                    new String[] { alias });
            // 注册Bean
            BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
        }

    ​ After reading the basic registration mechanism, let's take a look at how Bean completes automatic injection: here is another annotation-@FeignAutoConfiguration

    @FeignAutoConfiguration Brief Introduction

    ​ Regarding feign injection, two forms are provided in this class:

    • If HystrixFeign is present, use HystrixTargeter method.
    • If it does not exist, then a DefaultTargeter will be instantiated as the default implementer

      The specific operation code is as follows:

              @Configuration(proxyBeanMethods = false)
              @ConditionalOnClass(name = "feign.hystrix.HystrixFeign")
              protected static class HystrixFeignTargeterConfiguration {
           
                  @Bean
                  // 优先使用Hystrix
                  @ConditionalOnMissingBean
                  public Targeter feignTargeter() {
                      return new HystrixTargeter();
                  }
           
              }
           
              @Configuration(proxyBeanMethods = false)
               //如果不存在Hystrix,则使用默认的tagerter
              @ConditionalOnMissingClass("feign.hystrix.HystrixFeign")
              protected static class DefaultFeignTargeterConfiguration {
           
                  @Bean
                  @ConditionalOnMissingBean
                  public Targeter feignTargeter() {
                      return new DefaultTargeter();
                  }
           
          }

    Review the meaning of several core annotations of springboot:

    • @ConditionalOnBean // When the given bean exists, instantiate the current bean
    • @ConditionalOnMissingBean // When the given bean does not exist, instantiate the current bean
    • @ConditionalOnClass // When the given class name exists on the classpath, instantiate the current Bean
    • @ConditionalOnMissingClass // When the given class name does not exist on the classpath, instantiate the current Bea

    About the invoke method of HystrixInvocationHandler:

    Feign.hystrix.HystrixInvocationHandler which performed the Invoke fact or SyncronizedMethodHandler method

    HystrixInvocationHandler.this.dispatch.get(method).invoke(args);

    ​ The internal code also uses the command mode HystrixCommand for encapsulation. Since it is not the focus of this article, I will not expand it here.

    What is this object used for?

    Introduction: Used to package code, will perform potentially risky functions (usually referring to service calls through the network) and fault and delay tolerance, statistics and performance index capture, circuit breaker and partition functions. This command is essentially a blocking command, but if used with observe() , it provides an observable object appearance.

    Implementation interface: HystrixObservable / HystrixInvokableInfo

    HystrixInvokableInfo : Storage command interface specification, subclass requires implementation

    HystrixObservable : Become an observer to support non-blocking calls

    to sum up

    ​ Summarizing the source code for the first time, more is to refer to the information on the Internet to follow the ideas of others to read a little bit. (Haha, there is a sequence of learning, and there is a specialization in the surgery industry) If there are errors, please point them out.

    ​ Different from the complex layers of abstraction in spring, openFeign's learning and "imitation" value is more meaningful. Many codes can be seen at a glance in the shadow of the design pattern, which is more suitable for practicing and learning to improve personal programming skills.

    ​ In addition, openFeign uses a lot of package access structure, which is a headache for the second exte


    Xander
    195 声望49 粉丝