[enjoy learning Feign] II. Annotation introduction of original Feign and detailed explanation of core API

A man without foresight must have immediate worries.

- > return to the column directory
Code download address: https://github.com/f641385712/feign-learning

Catalog

Preface

Through the first article, we understand the most basic knowledge of Feign. As an attitude programmer, we need to understand the whole execution process and principle of Feign. This is the basis of personalized customization in the end, and all for "play".

This article will focus on two aspects of native Feign:

  • Primary annotation
  • Core API

Note: This article still only imports the core package feign core package, so it is about its core content.

text

The content of this article is a bit like gnawing API and source code, so it is relatively boring, you need to adhere to it. Because it's boring to learn, but it's really very useful. It's the so-called "God won't treat hard-working people badly, nor sympathize with false hard-working people".

Primary annotation

Because most people understand Feign and use Feign for Spring Cloud, they usually use Spring MVC annotations. Therefore, we don't know much about Feign's original annotation. This article will help you to eliminate illiteracy and make you more handy in the actual use.

There are not many original notes, which are introduced here one by one:

annotation Where is it marked? Effect
@RequestLine Method Define HttpMethod and UriTemplate for the request (what is marked on the method is an HttpMethod, and the URI is written. Note: domain is not specified. Expression, bracketed value {expression} will finally be filled in with the corresponding @ Param annotation (matching according to key)
@Param Parameter Define the template variable by name, whose value will be used to fill in the template above
@Headers Method, Type Marked on a class or method for passing request headers. @Headers("Accept: application/json"), of course, you can also use the form {expression}
@QueryMap Parameter Can only be marked on parameters of Map type: used to pass multiple query values, spliced after the URL
@HeaderMap Parameter Don't explain. Use when passing multiple request headers
@Body Method Label on method. For example: @ Body("{body}"), so you can pass values through the @ Param("body") String body of the method parameter. Note: this value is finally sent in the form of http body (not URL parameter). The content of body does not need to be json, which is specified by your request header. (use this annotation to specify the request header of Qinghe @ Headers)

The expression must be wrapped with {} to calculate an expression (variable). The value in the expression is name (key), which will be matched by @ Param and replaced~

Core API

The introduction of annotations is simple, and there is no threshold to use them, which may be the biggest advantage of metaprogramming. Next, we need to introduce the core API of the native Feign, which is the top priority of the whole Feign. After all, the API is parsed by them.

Template

It is used to indicate: according to RFC 6570 The standard expression template can be replaced with variables to form the final value.

Note: the standard of template string is RFC 6570. You can learn it simply without too much. (Spring MVC follows Ant style URL template, which seems easier to understand...)

public class Template {

	// Unique constructor, access right is default
	Template( ... ) { ... }
	
	// The fill parameter expression can be a feign.template.Expression object
	// A {} is an Expression object, matching by name (key)
	public String expand(Map<String, ?> variables) { ... }
	protected String resolveExpression(Expression expression, Map<String, ?> variables) { ... }
	
	// Uri encode URI encoding
	// encodeSlash true will escape even / but not by default
	private String encode(String value) { ... }

	// Get * * names of all variables under the current template**
	public List<String> getVariables() { ... }
}

This class is not an abstract class, and it has four subclasses:

UriTemplate

As the name implies, it is a template for handling URI s.

public class UriTemplate extends Template {

	public static UriTemplate create(String template, Charset charset) {
	  return new UriTemplate(template, true, charset);
	}
	... // Omit other constructors
}

The source code of this class is very simple. It does not extend on the basis of the parent class, but only provides static methods for building instances (because the parent class constructor has the default permission).

Use example:

@Test
public void fun1() {
    UriTemplate template = UriTemplate.create("http://example.com/{foo}", StandardCharsets.UTF_8);
    Map<String, Object> params = new HashMap<>();
    params.put("foo", "bar");
    String result = template.expand(params);
    System.out.println(result);

    System.out.println("=======================================");

    // Do not escape the slash
    template = UriTemplate.create("http://example.com/{empty}{foo}index.html{frag}",false, StandardCharsets.UTF_8);

    params.clear();
    // params.put("empty",null);
    params.put("foo","houses/");
    params.put("frag","?g=sec1.2");
    result = template.expand(params);
    System.out.println(result);
}

Run program, output:

http://example.com/bar
=======================================
http://example.com/houses/index.html%3Fg=sec1.2

Small details: / was not transferred, but? Was escaped to% 3. It can be seen that by default, it will escape all the special symbols on the URI. In addition, {empty} does not appear in the URI at all because it does not give a value (the effect is the same as the value assigned to null).

Note: This is a big feature of template processing -- > if the corresponding key does not exist or the value is null, this part of the expression will be ignored
It should be noted that if the empty string of values means that there is value, it is just empty string, or it is meaningful

I found an online testing tool for RFC6570 syntax format: http://www.utilities-online.info/uritemplate , you can try, familiar with feign URL template syntax.

QueryTemplate

Template of Query String parameter.

public final class QueryTemplate extends Template {

	// Here name uses the template type instead of string, because name can also be a template {}
	// In most cases, it can be a string
	private final Template name;
	// Because a key can correspond to multiple values, it must be right to use List
	private List<String> values; 
  
  	// What separator is used when a key has multiple values? It supports:, \ t |, etc
  	// By default, it is collectionformat. Expanded, which means it will be spliced in the form of foo = Bar & foo = Baz
  	private final CollectionFormat collectionFormat;

	// Please note: by default, collectionformat.expanded is used here
  	public static QueryTemplate create(String name, Iterable<String> values, Charset charset) {
    	return create(name, values, charset, CollectionFormat.EXPLODED);
  	}
	... // Omit append and other methods
	
	public List<String> getValues() {
	  return values;
	}
	public String getName() {
	  return name.toString();
	}

  	// You can see that expand is actually to expand name~~~~~
	@Override
	public String expand(Map<String, ?> variables) {
	  String name = this.name.expand(variables);
	  return this.queryString(name, super.expand(variables));
	}
  	...
}

The template is used to construct a query parameter, one at a time, and multiple key s need to be built multiple times.

Use example:

@Test
public void fun2() {
	// You can see that key s can also use templates. Of course, you can use strings directly or mix them
    QueryTemplate template = QueryTemplate.create("hobby-{arg}", Arrays.asList("basket", "foot"), StandardCharsets.UTF_8);
    Map<String, Object> params = new HashMap<>();
    // params.put("arg", "1");

    String result = template.expand(params);
    System.out.println(result);

    template = QueryTemplate.create("grade", Arrays.asList("1", "2"), StandardCharsets.UTF_8, CollectionFormat.CSV);
    System.out.println(template);
}

Output:

hobby-%7Barg%7D=basket&hobby-%7Barg%7D=foot
grade=1,2

Note: arg must be passed here and cannot be null. Otherwise, it will be output as follows: hubby -% 7barg% 7d = Basket & hubby -% 7barg% 7d = foot is output as is, and the internal expression will not be ignored.

HeaderTemplate

The template for constructing the request header is almost the same as above, and is omitted here.

BodyTemplate

Used to represent a template annotated with @ Body annotation.

public final class BodyTemplate extends Template {

  // Although it is not mandatory to be Json, it has special support
  private static final String JSON_TOKEN_START = "{";
  private static final String JSON_TOKEN_END = "}";
  private static final String JSON_TOKEN_START_ENCODED = "%7B";
  private static final String JSON_TOKEN_END_ENCODED = "%7D";
  private boolean json = false;

  public static BodyTemplate create(String template) {
     return new BodyTemplate(template, Util.UTF_8);
  }
  private BodyTemplate(String value, Charset charset) {
     super(value, ExpansionOptions.ALLOW_UNRESOLVED, EncodingOptions.NOT_REQUIRED, false, charset);
     // Judge whether it is JSON. If your template string starts with% 7B and ends with% 7D, the tag is JSON, which will be filled with special handling
     // Note: this can't be constructed manually. It can only be true after being processed by the encoder
     if (value.startsWith(JSON_TOKEN_START_ENCODED) && value.endsWith(JSON_TOKEN_END_ENCODED)) {
      this.json = true;
    }
  }

  // If it is JSON, special processing will be performed
  @Override
  public String expand(Map<String, ?> variables) {
    String expanded = super.expand(variables);
    if (this.json) {
      /* decode only the first and last character */
      StringBuilder sb = new StringBuilder();
      sb.append(JSON_TOKEN_START);
      sb.append(expanded,
          expanded.indexOf(JSON_TOKEN_START_ENCODED) + JSON_TOKEN_START_ENCODED.length(),
          expanded.lastIndexOf(JSON_TOKEN_END_ENCODED));
      sb.append(JSON_TOKEN_END);
      return sb.toString();
    }
    return expanded;
  }
}

The only feature of the template is that it is compatible with JSON format. There is not much to say.

Use example:

@Test
public void fun3(){
    BodyTemplate template = BodyTemplate.create("data:{body}");

    Map<String, Object> params = new HashMap<>();
    params.put("body", "{\"name\": \"YourBatman\",\"age\": 18}");

    String result = template.expand(params);
    System.out.println(result);
}

Output: data:{"name": "YourBatman","age": 18}

Target

It is similar to javax.ws.rs.client.WebTarget in JAXRS. It is used to convert the request template RequestTemplate to feign.Request, and then send Http request to the Client.

// It's a generic interface
public interface Target<T> {

  // The type of interface this target acts on
  Class<T> type();
  // A key configured for this target
  String name();
  // Base URL to send the request. For example: https://example/api/v1
  String url();

  // The most important method is to assemble the Request template, add Base Url, and transform it into a real Request
  // This input template contains many: QueryTemplate/HeaderTemplate/UriTemplate, etc
  Request apply(RequestTemplate input);
}

This interface has two implementation classes:


Feign's design features: it tends to gather interface default implementation classes and related classes in the form of internal classes.

EmptyTarget

As the name implies, it is "do nothing". It is characterized by Base URL.

public static final class EmptyTarget<T> implements Target<T> {
	
	// Note: it does not require a URL property
	private final Class<T> type;
    private final String name;

    public static <T> EmptyTarget<T> create(Class<T> type) {
      return new EmptyTarget<T>(type, "empty:" + type.getSimpleName());
    }
    ...
    // It doesn't need a URL because it doesn't need to do anything
    @Override
    public String url() {
      throw new UnsupportedOperationException("Empty targets don't have URLs");
    }

	// If the URL of the request template already contains http, that is to say, the absolute path, it will be thrown wrong
	// input.request() -> Request.create(this.method, this.url(), this.headers(), this.requestBody())
	// this.url() here is a relative path. For example, / api/v1/demo
    @Override
    public Request apply(RequestTemplate input) {
      if (input.url().indexOf("http") != 0) {
        throw new UnsupportedOperationException("Request with non-absolute URL not supported with empty target");
      }
      return input.request();
    }
	...
}

Its biggest feature is that it can't have Base Url, so it's like doing nothing

HardCodedTarget

Hard code the target class. None of its three parameters can be null

public static class HardCodedTarget<T> implements Target<T> {

    private final Class<T> type;
    private final String name;
    private final String url; // Compared to Empty, it must have a url

	...
	// Unlike others, its instances are created by constructors
    public HardCodedTarget(Class<T> type, String name, String url) {
      this.type = checkNotNull(type, "type");
      this.name = checkNotNull(emptyToNull(name), "name");
      this.url = checkNotNull(emptyToNull(url), "url");
    }
    ... // Omit 3 get methods

	// If the URL of the request template does not contain http, that is to say, it is a relative path, add the Base Url here
	// If the request template is already an absolute path, it doesn't matter
	@Override
    public Request apply(RequestTemplate input) {
      if (input.url().indexOf("http") != 0) {
        input.target(url());
      }
      return input.request();
    }
}

This implementation class is commonly used and default. For example, feign.Feign class is used to proxy the target interface class, which needs to be mastered.

Client

It is an interface, as the name implies, which is used to send the Http request feign.Request. Please note: the subclass implementation of this interface should ensure that it is thread safe (because it may be multi-threaded)

public interface Client {
	// Options are options parameters, such as:
	// connectTimeoutMillis link timeout, default: 10s
	// readTimeoutMillis link timeout, default: 60s
	Response execute(Request request, Options options) throws IOException;
}

The interface is simple: there is and only one method for executing http requests. It has the following two implementation classes:

Default

As the default implementation of Client interface, it sends HTTP request based on HttpURLConnection of JDK. feign.Feign is used by default to send HTTP requests.

class Default implements Client {

	// These classes are all under the java.net package
    private final SSLSocketFactory sslContextFactory;
    private final HostnameVerifier hostnameVerifier;

	// Send Request - > the final send completed through HttpURLConnection
    @Override
    public Response execute(Request request, Options options) throws IOException {
     // Build an HttpURLConnection through feign.Request 
      HttpURLConnection connection = convertAndSend(request, options);
      // Get the Response and give it to the later for analysis
      return convertResponse(connection, request);
    }
	...
}

This is Feign's default way of sending requests: the underlying uses the HttpURLConnection generated by the JDK source. For the Client generated by JDK source, please pay attention to the scenario that it automatically turns GET to POST for you to avoid stepping on the pit.

Proxied

java.net.Proxy (such as flipping over the wall) that supports JDK is rarely used.

Note: This refers to java.net.Proxy, not java.lang.reflect.Proxy

Retryer

Retry. It can keep the state clone of client ා execute() for each Http request, so as to decide whether to retry according to the configuration.

public interface Retryer extends Cloneable {

  // If retry is allowed, return returns (possibly after sleep).
  // Otherwise, this exception will continue to spread
  void continueOrPropagate(RetryableException e);
 
  @Override
  Retryer clone();

 	// Never retry instance
 	// In the production environment: it is recommended to never try again, otherwise, please do idempotent
  Retryer NEVER_RETRY = new Retryer() {
    @Override
    public void continueOrPropagate(RetryableException e) {
      throw e;
    }
    @Override
    public Retryer clone() {
      return this;
    }
  };
}

Built in has and only one implementation: feign.Retryer.Default.

Default

Feign uses the Default instance by Default instead of never trying again.

class Default implements Retryer {
	
	// Retry parameter
    private final int maxAttempts;
    private final long period;
    private final long maxPeriod;
	
	// Internal statistics index count
    int attempt; //Count: retries
    long sleptForMillis; // How long have you had a rest

	// Null construct, default retry parameter
	// period: retry once in 100ms by default
	// maxPeriod: Max retry 1 second
	// maxAttempts: 5 retries at most
	// 
    public Default() {
      this(100, SECONDS.toMillis(1), 5);
    }
    ...

	@Override
    public void continueOrPropagate(RetryableException e) {
     // If the maximum number of retries is exceeded, the exception continues to be thrown upward
      if (attempt++ >= maxAttempts) {
        throw e;
      }
      // ... / / a set of retry logic
	}
	
	// Note: Clone here is a new instance of new for use (the configuration is exactly the same)
    @Override
    public Retryer clone() {
      return new Default(period, maxPeriod, maxAttempts);
    }
}

Therefore, note: by default, Feign has a retry mechanism, and it retries once in 100ms, and 5 times by default.

Note: many partners fall here. In the production environment, please close Feign's retry mechanism to avoid unnecessary troubles

MethodHandler

Functionally, it is similar to the java.lang.reflect.invocationhandlerාinvoke() method. The interface definition is very simple:

interface MethodHandler {
	Object invoke(Object[] argv) throws Throwable;
}

This interface has two built-in implementation classes:

Note: the name of this interface is MethodHandler, and the name of JDK is MethodHandle. Don't confuse

DefaultMethodHandler

It is called by calling the interface default method (because it does not hold the proxy object of Target, so the method used in the interface can only be the default method) code to handle the method. Note: the bindTo method must be called before the invoke method.

The technology it relies on is the method handle provided by Java7, which is more efficient than reflection. The disadvantage is that the coding is slightly complex.

Note: how to use MethodHandle of JDK? Please study by yourself if you are interested

// Access is Default
final class DefaultMethodHandler implements MethodHandler {

	private final MethodHandle unboundHandle;
	private MethodHandle handle;

	// Get Method handle from Method and assign it to unboundHandle
	public DefaultMethodHandler(Method defaultMethod) { ... }


  // Bind the target object (proxy object) to the method handle
  // In this way, unboundHandle becomes a bound handle, so invoke can call
  public void bindTo(Object proxy) {
    if (handle != null) {
      throw new IllegalStateException("Attempted to rebind a default method handler that was already bound");
    }
    handle = unboundHandle.bindTo(proxy);
  }

  // Call target method
  // Before calling: make sure the target object is bound
  @Override
  public Object invoke(Object[] argv) throws Throwable {
    if (handle == null) {
      throw new IllegalStateException("Default method handler invoked before proxy has been bound.");
    }
    return handle.invokeWithArguments(argv);
  }
}

The feature of the default implementation is that the method handle method handle is used to "reflect" the execution of the target method. Obviously, it can only execute the interface default method, so there is a long-distance communication in general.

SynchronousMethodHandler

The synchronous method calls the processor, which emphasizes synchronization and remote communication.

final class SynchronousMethodHandler implements MethodHandler {
	
	// Method meta information
	private final MethodMetadata metadata;
	// The goal is to finally build an instance of Http Request, which is generally HardCodedTarget
	private final Target<?> target;
	// Responsible for sending the final request - > the default incoming request is generated based on the JDK source, which is inefficient. It is not recommended to use it directly
	private final Client client;
	// Responsible for retry -- > the Default is passed in. There is a retry mechanism. Please pay attention to the use in production
	private final Retryer retryer;

	// Request interceptor, which intercepts before target.apply(template); that is, template - > request conversion
	// Note: it's not the moment before sending the request. Please pay attention
	// Its function can only be to customize the Request template, but not the Request
	// Built in has only one implementation: BasicAuthRequestInterceptor for authentication
	private final List<RequestInterceptor> requestInterceptors;

	// If you want to see feign's request log in the console, change the log level to info (generally only info will be output to the log file)
	private final Logger.Level logLevel;
	...
	// Factory building request template
	// For the request template, there are many ways to build, and multiple encoders may be used internally, which will be explained in detail below
	private final RequestTemplate.Factory buildTemplateFromArgs;
	// Request parameters: such as link timeout, request timeout, etc
	private final Options options;

	// Decoder: used to decode Response
	private final Decoder decoder;
	// Decoder in case of error / exception
	private final ErrorDecoder errorDecoder;

	// Decode 404 status code? No decoding by default
	private final boolean decode404;
	
	// The only constructor, and it's still private (so you can only build its instance in this class)
	// All the above properties are assigned
	private SynchronousMethodHandler( ... ) { ... }

  	@Override
  	public Object invoke(Object[] argv) throws Throwable {
  		// According to the method input parameter, a request template is built in combination with the factory
		RequestTemplate template = buildTemplateFromArgs.create(argv);
		// findOptions(): if you have Options in the method input parameter, it will be found here
		// Note: if there are more than one, only the first one will take effect (no error will be reported)
		Options options = findOptions(argv);
		// Retry mechanism: note here is a clone to use
		Retryer retryer = this.retryer.clone();
		while (true) {
		     try {
		        return executeAndDecode(template, options);
		      } catch (RetryableException e) {
				
				// If an exception is thrown, the retry logic is triggered
		       try {
		       	  // The logic is: if it is not retried, the exception will continue to be thrown
		       	  // To recharge, you will go to the following continue
		          retryer.continueOrPropagate(e);
		        } catch (RetryableException th) {
		        	...
		        }
		        continue;
		      }
		}
  	}
}

The implementation of the MethodHandler is relatively complex, which is described in one sentence: after all the parameters are prepared, the Http request is sent and the result is parsed. I try to summarize its steps as follows:

  1. Through method parameters, a request template is built using the factory
    1. The @ RequestLine/@Param and other annotations will be resolved here
  2. Get the request option from the method parameter: Options (of course, there may not be this type in the parameter, which is null. If it is null, the default option will be executed.)
  3. Execute and decode (template, options) executes the Http request and completes the result decoding (including the decoding of the correct status code and error decoding). This step is relatively complex, which is divided into the following sub steps:
    1. Convert request template to request object feign.Request
      1. Execute all interceptors RequestInterceptor and complete the customization of request template
      2. Call the target, and change the Request template to Request: target.apply(template);
    2. Send Http request: client.execute(request, options), get a Response object (if IO exception occurs here, it will also be wrapped as RetryableException and thrown again)
    3. Parse the Response Object and return it (return Object: it may be a Response instance or any type after decode). There will be the following situations:
      1. Response.class == metadata.returnType(), that is to say, the return value of your method is response. If response.body() == null, that is to say, if the server returns null/void, it will directly return response; if response.body().length() == null, it will directly return response; otherwise, it will normally return the content in response. Tobuilder(). Body (bodydata). Build()
      2. If 200 < = response code < = 300, it means correct return. Then decode the return value: decoder.decode(response, metadata.returnType()) (there may be exceptions in the decoding process, which will also be wrapped as FeignException and thrown upward)
      3. If the response code is 404 and decode404 = true, the same as above
      4. In other cases (4xx or 5xx response codes), the error code is executed: errorDecoder.decode(metadata.configKey(), response)
  4. Send an http request if everything is OK, it's over. Otherwise, execute retry logic:
    1. Through retryer.continueOrPropagate(e); see if you want to execute retry mechanism after receiving this exception
    2. If you need to retry, continue
    3. If there is no need to retry (or the number of retries has reached), throw this exception again, and throw it upward
    4. Handle this exception, print log

In my opinion, this is the most core logic of Feign as an HC. Please be sure to master it.

summary

The annotation and core API of native Feign are explained first. The annotation is more application oriented, and the core API is the basis of your advancement.
This article doesn't explain all the core API s, but most of them have already cover ed. This is a very important foreshadowing for the later explanation of Feign. I hope this part of the content can help you.

statement

It's not easy to be original. Welcome to praise and pay attention. Sharing this article to the circle of friends and reprinting are allowed, but they refuse to plagiarize. You can also scan the code on the left to join the discussion and exchange of my Java senior engineer and Architect Series.

284 original articles published, 449 praised, 380000 visitors+
His message board follow

Tags: JSON JDK Java Spring

Posted on Mon, 10 Feb 2020 08:58:19 -0500 by Yetalia