Spring cloud upgrade 2020.0 Version x-27 Openfeign's lifecycle - creating agents

Posted by andrew6607 on Mon, 03 Jan 2022 09:37:38 +0100

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

Next, we begin to analyze the life cycle of OpenFeign, combined with the source code of OpenFeign itself. The first is to create an OpenFeign proxy from the interface definition. We only care about the synchronous client here, because the asynchronous client is still being implemented, and in our project, the asynchronous responsive client does not use OpenFeign, but the official WebClient

Create OpenFeign proxy

Creating an OpenFeign agent is mainly divided into the following steps:

  1. Use Contract to parse each method of the interface and generate a metadata list of each method: List < methodmetadata > metadata
  2. According to each MethodMetadata, generate the corresponding request template factory requesttemplate Factory, used to generate subsequent requests. At the same time, the template factory and other configurations are used to generate the corresponding method processor MethodHandler. For synchronous OpenFeign, the MethodHandler is implemented as synchronous MethodHandler. Map the interface method and MethodHandler one by one, and the result is map < method, MethodHandler > methodtohandler. For the interface default method introduced by Java 8, you need to use a different MethodHandler, DefaultMethodHandler, because this method does not need a proxy and does not generate a corresponding http call. Instead, it is implemented by directly calling the corresponding default method code.
  3. Use the InvocationHandlerFactory factory to create an InvocationHandler for proxy calls.
  4. Call JDK dynamic proxy generation class method to create proxy class using InvocationHandler.

Create OpenFeign agent, which is mainly based on the dynamic agent implementation of JDK. Let's take a simple example to create a JDK dynamic agent for analogy.

JDK dynamic agent

Using JDK dynamic agent requires the following steps:

1. Write the interface and the corresponding proxy class. Here we write a simple interface and corresponding implementation class:

public interface TestService {
    void test();
}
public class TestServiceImpl implements TestService {
    @Override
    public void test() {
        System.out.println("TestServiceImpl#test is called");
    }
}

2. Create a proxy class to implement Java lang.reflect. InvocationHandler, and, in the core method, invoke the actual object, which is the object of the implementation class TestServiceImpl of our TestService above.

JDK has a built-in dynamic proxy API, and its core is Java lang.reflect. InvocationHandler. Let's create a simple invocationhandler implementation class:

public class SimplePrintMethodInvocationHandler implements InvocationHandler {
    private final TestService testService;
    public SimplePrintMethodInvocationHandler(TestService testService) {
        this.testService = testService;
    }
    @Override
    public Object invoke(
            //Proxy object
            Object proxy,
            //Method called
            Method method,
            //Parameters used
            Object[] args)
            throws Throwable {
        System.out.println("Invoked method: " + method.getName());
        //Make the actual call
        return method.invoke(testService, args);
    }
}

3. Create a Proxy object and call it with the Proxy object. It is generally created through the static method of Proxy, for example:

//First, create the object to proxy
TestServiceImpl testServiceImpl = new TestServiceImpl();
//Then create the corresponding InvocationHandler using the object to proxy
SimplePrintMethodInvocationHandler simplePrintMethodInvocationHandler = new SimplePrintMethodInvocationHandler(testServiceImpl);
//Create a proxy class. Because a class may implement multiple interfaces, the Object returned here is forced into the interface to be used according to your needs
Object proxyInstance = Proxy.newProxyInstance(
        TestService.class.getClassLoader(),
        testServiceImpl.getClass().getInterfaces(),
        simplePrintMethodInvocationHandler
);
//Force conversion
TestService proxied = (TestService) proxyInstance;
//Call with proxy object
proxied.test();

In this way, we use the built-in dynamic proxy mechanism of JDK to implement a simple dynamic proxy. In the use of OpenFeign, it is a little different from our example. First of all, we only need to define the interface to proxy, not the implementation class. Because what all OpenFeign interfaces need to do is actually HTTP calls, and its information can be automatically generated from the interface definition. We can use unified objects to carry the requests defined by the OpenFeign interface according to the interface definition. In OpenFeign, this is equivalent to the implementation object, that is, the MethodHandler generated according to the interface, in the synchronized OpenFeign, that is, feign SynchronousMethodHandler. After that, the InvocationHandler created by OpenFeign actually forwards the call to the corresponding method of the corresponding synchronous MethodHandler.

Detailed explanation of the process of creating OpenFeign proxy object

Using the previous example, let's take a look at the process of creating an agent:

interface GitHub {
    /**
     * Define get methods, including path parameters, and return serialized classes in response
     * @param owner
     * @param repository
     * @return
     */
    @RequestLine("GET /repos/{owner}/{repo}/contributors")
    List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repository);

    /**
     * Responder structure class
     */
    class Contributor {
        String login;
        int contributions;

        public Contributor() {
        }

        public String getLogin() {
            return login;
        }

        public void setLogin(String login) {
            this.login = login;
        }

        public int getContributions() {
            return contributions;
        }

        public void setContributions(int contributions) {
            this.contributions = contributions;
        }
    }
}

/**
 * Deserialization decoder based on fastjason
 */
static class FastJsonDecoder implements Decoder {
    @Override
    public Object decode(Response response, Type type) throws IOException, DecodeException, FeignException {
        //Read body
        byte[] body = response.body().asInputStream().readAllBytes();
        return JSON.parseObject(body, type);
    }
}
public static void main(String[] args) {
    //Create the HTTP call interface implementation of Feign proxy
    GitHub github = Feign.builder()
                        //Specify the decoder as FastJsonDecoder
                        .decoder(new FastJsonDecoder())
                        //Specify that the proxy class is GitHub and the base address is https://api.github.com
                        .target(GitHub.class, "https://api.github.com");
    List<GitHub.Contributor> contributors = github.contributors("OpenFeign", "feign");
}

What we care about here is to create the HTTP call interface of Feign proxy to realize the internal process of this step. First, let's look at the structure of Feign builder. When we initialize a Feign builder, we call Feign When Builder (), the following components will be created (it also shows that the following components can be configured. If some configurations are not mentioned before, they can be configured):

//Request interceptor list, which is empty by default
private final List<RequestInterceptor> requestInterceptors = new ArrayList();
//Log level. No logs are printed by default
private Level logLevel = Level.NONE;
//The Contract responsible for parsing class metadata is the default Contract that supports OpenFeign built-in annotations 
private Contract contract = new Contract.Default();
//The Client that hosts HTTP requests is the Default Client based on Java HttpURLConnection by default
private Client client = new feign.Client.Default((SSLSocketFactory)null, (HostnameVerifier)null);
//The retrier is also Default by Default
private Retryer retryer = new feign.Retryer.Default();
//The default log Logger does not record any logs by default
private Logger logger = new NoOpLogger();
//The encoder decoder is also the default
private Encoder encoder = new feign.codec.Encoder.Default();
private Decoder decoder = new feign.codec.Decoder.Default();
//Query parameter code, which we generally do not modify
private QueryMapEncoder queryMapEncoder = new FieldQueryMapEncoder();
//Error encoder, Default
private ErrorDecoder errorDecoder = new feign.codec.ErrorDecoder.Default();
//Default configuration for various timeout Options
private Options options = new Options();
//The Factory used to generate InvocationHandler is also the default
private InvocationHandlerFactory invocationHandlerFactory = new feign.InvocationHandlerFactory.Default();
//Is there a special resolution 404 error? Because we may not want to throw an exception for 404. The default is false
private boolean decode404 = false;
//Whether to close the Response immediately after decoding. The default is yes
private boolean closeAfterDecode = true;
//Exception propagation rule. The default is no propagation
private ExceptionPropagationPolicy propagationPolicy = ExceptionPropagationPolicy.NONE;
//Whether to force decoding is mainly to be compatible with the configuration introduced by asynchronous Feign. We directly ignore it and think it is false
private boolean forceDecoding = false;
private List<Capability> capabilities = new ArrayList();

Our code specifies that the Decoder is FastJsonDecoder, so the Decoder is FastJsonDecoder. Finally, through target (GitHub. Class“ https://api.github.com "); specify that the proxy class is GitHub and the base address is https://api.github.com At this time, Feign proxy class will be generated. The steps are as follows:

Feign

public <T> T target(Class<T> apiType, String url) {
  //HardCodedTarget is created by using proxy interface type and base address, which means hard coded Target
  return target(new HardCodedTarget<T>(apiType, url));
}

public <T> T target(Target<T> target) {
  return build().newInstance(target);
}

public Feign build() {
  //Pass all components through all capabilities. From here, we can see that we can implement the Capability interface to dynamically modify components when creating Feign agent
  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);

  //Create a Factory of SynchronousMethodHandler, which is used to generate SynchronousMethodHandler. SynchronousMethodHandler is an implementation class that actually hosts Feign proxy requests
  SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
      new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
          logLevel, decode404, closeAfterDecode, propagationPolicy, forceDecoding);
  //The metadata resolution of different interface methods is distinguished by method names, which are used to generate and route to the corresponding proxy methods
  ParseHandlersByName handlersByName =
      new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,
          errorDecoder, synchronousMethodHandlerFactory);
  //Create reflective feign
  return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
}
}

After creating the ReflectiveFeign, the newInstance method will be called:

ReflectiveFeign

public <T> T newInstance(Target<T> target) {
    //ParseHandlersByName mentioned earlier is used to parse metadata and generate methodhandlers for all methods that need proxy. We analyze synchronous Feign here, so it is synchronous MethodHandler
    Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
    Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
    List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();

    //Correspond the method to the corresponding MethodHandler one by one
    for (Method method : target.type().getMethods()) {
      if (method.getDeclaringClass() == Object.class) {
        //For Object methods, skip directly
        continue;
      } else if (Util.isDefault(method)) {
        //If it is the default method of the interface in java 8, use DefaultMethodHandler
        DefaultMethodHandler handler = new DefaultMethodHandler(method);
        defaultMethodHandlers.add(handler);
        methodToHandler.put(method, handler);
      } else {
        methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
      }
    }
    //Create an InvocationHandler using the InvocationHandlerFactory in the previous Builder
    InvocationHandler handler = factory.create(target, methodToHandler);
    //Creating a Proxy using InvocationHandler
    T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
        new Class<?>[] {target.type()}, handler);
    //Associate proxy with DefaultMethodHandler
    for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
      defaultMethodHandler.bindTo(proxy);
    }
    return proxy;
  } 

For the MethodHandler that uses the ParseHandlersByName parsing metadata mentioned earlier and generates all the methods that need the agent, this involves the metadata that is parsed by Contract, and then binds them to the corresponding encoding to use the encoding after that:

ReflectiveFeign

public Map<String, MethodHandler> apply(Target target) {
      // Use Contract to parse the metadata of all methods
      List<MethodMetadata> metadata = contract.parseAndValidateMetadata(target.type());
      Map<String, MethodHandler> result = new LinkedHashMap<String, MethodHandler>();
      // For each parsed method metadata
      for (MethodMetadata md : metadata) {
        BuildTemplateByResolvingArgs buildTemplate;
        if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) {
          //With forms
          buildTemplate =
              new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);
        } else if (md.bodyIndex() != null) {
          //With body
          buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);
        } else {
          //Other situations
          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 {
          // Use the Factory of SynchronousMethodHandler to generate SynchronousMethodHandler 
          result.put(md.configKey(),
              factory.create(target, md, buildTemplate, options, decoder, errorDecoder));
        }
      }
      return result;
    }
  }

The InvocationHandler generated by the default InvocationHandlerFactory is reflective feign FeignInvocationHandler:

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

The contents are:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  // For equals, hashCode, toString methods, call directly
  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();
  }
  //For other methods, call the corresponding SynchronousMethodHandler for processing
  return dispatch.get(method).invoke(args);
}

From here, we can see that the Proxy we generated actually proxies the request to the synchronous methodhandler.

In this section, we introduce in detail the detailed process of creating Proxy by OpenFeign. It can be seen that the Proxy generated by synchronous Feign actually proxies the method request defined by the interface HTTP request to the SynchronousMethodHandler. In the next section, we will analyze the actual HTTP call process of SynchronousMethodHandler in detail to find out how all Feign components work in coordination.